[
  {
    "path": ".gitattributes",
    "content": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# Linux start script should use lf\n/gradlew        text eol=lf\n\n# These are Windows script files and should use crlf\n*.bat           text eol=crlf\n\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: [\"https://github.com/amir1376/ab-download-manager/blob/master/DONATE.md\"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug Report\ndescription: Report an issue or unexpected behavior in the app\ntitle: \"[Bug] Brief summary (keep it short)\"\nlabels: []\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for taking the time to report a bug!\n        **Before submitting, please [search the existing issues](./issues) to make sure this is not a duplicate.**\n  - type: textarea\n    id: description\n    attributes:\n      label: 📝 Description\n      description: What happened? What were you trying to do? Please provide a clear and concise description of the problem.\n      placeholder: Describe the bug you encountered.\n    validations:\n      required: true\n  - type: input\n    id: app-version\n    attributes:\n      label: 🏷️ App Version\n      description: e.g., 1.2.3\n      placeholder: \"1.2.3\"\n    validations:\n      required: true\n  - type: input\n    id: platform\n    attributes:\n      label: 💻 Platform\n      description: e.g., Windows 11, macOS 14.2, Ubuntu 24.04\n      placeholder: \"Windows 11\"\n    validations:\n      required: true\n  - type: input\n    id: installation-type\n    attributes:\n      label: 📦 Installation Type (optional)\n      description: e.g., .exe installer, dmg file, package manager, installation script\n      placeholder: \".exe installer\"\n    validations:\n      required: false\n  - type: input\n    id: system-device-details\n    attributes:\n      label: ⚙️ System/Device Details (optional)\n      description: e.g., CPU, RAM, device model\n      placeholder: \"CPU: i7-12700K; RAM: 32GB DDR5\"\n    validations:\n      required: false\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: 🔁 Steps to Reproduce\n      description: List all necessary steps to reproduce the bug.\n      placeholder: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: ✅ Expected Behavior\n      description: What did you expect to happen?\n      placeholder: Describe the expected behavior.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: 📷 Screenshots or Recordings (optional)\n      description: Drag and drop, paste images, or attach screen recordings here.\n      placeholder: Attach screenshots or screen recordings if possible.\n    validations:\n      required: false\n  - type: textarea\n    id: additional-info\n    attributes:\n      label: 🗒️ Additional Information (optional)\n      description: Any additional info, logs, context, or links to related issues.\n      placeholder: Add any other context about the problem here.\n    validations:\n      required: false\n  - type: textarea\n    id: possible-solution\n    attributes:\n      label: 💡 Possible Solution (optional)\n      description: Suggest a fix or reason for the bug, if you have one.\n      placeholder: Suggest a possible solution or reason for the bug.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/workflows/brew-cask-auto-bump.yml",
    "content": "name: Auto bump ab-download-manager homebrew cask\n\non:\n  release:\n    types: [released]    \n  workflow_dispatch: {}     \n  \njobs:\n  bump:\n    runs-on: macos-latest\n    env:\n      HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}\n      CASK_NAME: ab-download-manager\n      TAP_NAME: amir1376/tap\n      GH_REPO: amir1376/homebrew-tap\n\n    steps:\n      - name: Tap homebrew cask repo\n        run: |\n          brew tap ${{ env.TAP_NAME }}\n\n      - name: Extract version\n        id: ver\n        run: |\n          TAG_RAW=\"${{ github.event.release.tag_name }}\"\n          TAG_VER=\"${TAG_RAW#v}\"\n\n          if [ -n \"$TAG_VER\" ]; then\n            SRC=\"release tag\"\n            V=\"$TAG_VER\"\n          else\n            SRC=\"livecheck\"\n            OUT=\"$(brew livecheck --cask --json ${{ env.TAP_NAME }}/${{ env.CASK_NAME }} || true)\"\n            V=\"$(ruby -rjson -e '\n              j = JSON.parse(STDIN.read) rescue []\n              if j.is_a?(Array) && j[0] && j[0][\"version\"]\n                v = j[0][\"version\"]\n                if v[\"latest\"] && v[\"current\"] && v[\"latest\"] != v[\"current\"]\n                  puts v[\"latest\"]\n                end\n              end\n            ' <<< \"$OUT\" | tr -d \"[:space:]\")\"\n          fi                               \n          echo \"Source: $SRC\"\n          echo \"Version: ${V:-<none>}\"\n          echo \"version=$V\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Bump cask\n        if: ${{ steps.ver.outputs.version != '' }}\n        run: |\n          V=\"${{ steps.ver.outputs.version }}\"\n          echo \"Bumping ${{ env.CASK_NAME }} to $V ...\"\n          brew bump-cask-pr --no-browse --version \"$V\" \"${{ env.TAP_NAME }}/${{ env.CASK_NAME }}\"\n\n      - name: Skip (No new version)\n        if: ${{ steps.ver.outputs.version == '' }}\n        run: echo \"No newer version detected; nothing to bump.\"\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "on:\n  push:\n    tags:\n      - \"v[0-9]+.[0-9]+.[0-9]+*\"\n  workflow_dispatch:\n\n\npermissions:\n  contents: write\n\njobs:\n  create-packages:\n    strategy:\n      matrix:\n        os: [ \"ubuntu-latest\", \"ubuntu-24.04-arm\", \"windows-2022\", \"windows-11-arm\", \"macos-15-intel\", \"macos-15\" ]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up JBR\n        uses: actions/setup-java@v4\n        with:\n          distribution: \"jetbrains\"\n          java-package: \"jdk\"\n          java-version: \"21\"\n          check-latest: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Cache Gradle Dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: ${{ matrix.os }}-gradle\n          enableCrossOsArchive: true\n\n      - name: Skip Android build (build android only on Ubuntu)\n        if: matrix.os != 'ubuntu-latest'\n        shell: \"bash\"\n        run: |\n          echo \"SKIP_ANDROID_BUILD=true\" >> $GITHUB_ENV\n\n      - name: Use Android Secrets\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          echo \"ABDM_KEYSTORE_FILE=${{ secrets.ABDM_KEYSTORE_FILE }}\" >> $GITHUB_ENV\n          echo \"ABDM_KEYSTORE_FILE_PASSWORD=${{ secrets.ABDM_KEYSTORE_FILE_PASSWORD }}\" >> $GITHUB_ENV\n          echo \"ABDM_KEYSTORE_KEY_ALIAS=${{ secrets.ABDM_KEYSTORE_KEY_ALIAS }}\" >> $GITHUB_ENV\n          echo \"ABDM_KEYSTORE_KEY_PASSWORD=${{ secrets.ABDM_KEYSTORE_KEY_PASSWORD }}\" >> $GITHUB_ENV\n\n      # Steps specific to macOS (to create DMG for macOS)\n      - name: Install create-dmg (for macOS)\n        if: startsWith(matrix.os, 'macos-')\n        run: |\n          brew install create-dmg\n\n      - name: Gradle\n        run: |\n          ./gradlew\n        shell: \"bash\"\n\n      - name: Build package for current OS using gradle\n        shell: bash\n        run: |\n          ./gradlew createReleaseFolderForCi\n\n      - name: Release Gradle to unlock cache files\n        shell: bash\n        run: |\n          ./gradlew -stop\n\n      - name: Upload output to artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          path: ./build/ci-release\n          name: app-${{ matrix.os }}\n\n  release:\n    runs-on: \"ubuntu-latest\"\n    needs: [ \"create-packages\" ]\n    steps:\n      - uses: \"actions/download-artifact@v4\"\n        name: \"Download All Artifacts Into One Directory\"\n        with:\n          path: release\n          pattern: app-*\n          merge-multiple: true\n\n      - name: Version Info\n        id: version\n        uses: nowsprinting/check-version-format-action@v3\n        with:\n          prefix: \"v\"\n\n      - name: \"Show the output tree of release\"\n        run: |\n          tree .\n      - uses: softprops/action-gh-release@v2\n        with:\n          prerelease: ${{ !steps.version.outputs.is_stable }}\n          make_latest: legacy\n          draft: true\n          files: |\n            release/binaries/*\n          body_path: release/release-notes.md\n      - name: \"Remove artifacts to free space\"\n        uses: geekyeggo/delete-artifact@v5\n        with:\n          name: app-*\n"
  },
  {
    "path": ".github/workflows/winget.yml",
    "content": "name: Publish to Winget\r\n\r\non:\r\n  release:\r\n    types: [released]\r\n\r\njobs:\r\n  publish:\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - uses: vedantmgoyal9/winget-releaser@main\r\n        with:\r\n          identifier: amir1376.ABDownloadManager\r\n          token: ${{ secrets.WINGET_TOKEN }}\r\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore Gradle project-specific cache directory\n.gradle\n.kotlin\n\n# Ignore Gradle build output directory\nbuild\n.idea\nlocal.properties\n\n# Ignore android keystores\n*.jks\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## Unreleased\n\n### Added\n\n### Changed\n\n### Deprecated\n\n### Removed\n\n### Fixed\n\n### Security\n\n## 1.8.7\n\n### Added\n\n- Ability to change the storage root when selecting the download location on Android (e.g., save to SD card) (#1101)\n- Option to choose the render API on Desktop (#1103)\n\n### Changed\n\n- Default render API set to `SOFTWARE` on Linux\n\n### Improved\n\n- Updated translations\n- Increment/decrement buttons in number fields are now more accessible (#1104)\n- Minor UI improvements\n\n## 1.8.6\n\n### Added\n\n- Linux ARM support (#1081)\n- An option to set max concurrent downloads for manually resumed downloads (#1085)\n\n### Fixed\n\n- Do not reset the download if storage is not mounted yet (#1087)\n- Error when assembling HLS media if destination folder was not created yet (#1089)\n- Do not reset the download if server changes status code from 206 to 200 (#1088)\n\n### Improved\n\n- Updated translations\n- Queue logic improvements (#1086)\n- Linux Installation Script updated to support ARM devices (#1090)\n\n## 1.8.5\n\n### Added\n\n- Windows ARM support (#1055)\n- In-app browser bookmark feature on Android (#1072)\n- In-app browser can be added to the launcher (App Menu)\n- In-app browser can be used as the default browser\n\n### Fixed\n\n- System tray crash on some Linux environments (#1060)\n- Black screen issue on some Linux environments (#1066)\n- Notification sound playing even when notification sounds are muted on Android (#1064)\n\n### Improved\n\n- Updated translations\n- In-app browser UI/UX improvements on Android\n- System tray now uses native UI on Windows (#1060)\n- Use a default User-Agent when no value is provided by the user (#1071)\n- Small UI improvements\n\n## 1.8.4\n\n### Added\n\n- In-app browser for Android\n- The Android service now tells the user why it is running\n- The Add-Multi-Download page can now filter downloads using search and wildcards\n\n### Fixed\n\n- Random app crashes on some Android devices caused by service-related issues\n- Issues with the in-app update feature on some Android devices\n\n### Improved\n\n- Updated translations\n- The Android Foreground Service is now only used when necessary (active downloads, active queues, scheduled queues) and\n  automatically stops after inactivity\n- Add-Multi-Download page UI/UX improvements\n\n## 1.8.3\n\n### Added\n\n- Ability to sort and remove queue items on Android (#996)\n- A new shortcut to open the download list from the download progress dialog (#1001)\n\n### Fixed\n\n- Download table state not saved properly on desktop (#999)\n- Update related notifications appearing repeatedly on android (#998)\n\n### Improved\n\n- Updated translations\n- Settings Page UI improvements on android (#990)\n\n## 1.8.2\n\n### Fixed\n\n- Resolved issues with the In-App update feature on some android devices\n- Disabled notification badges on the launcher icon on android\n- The application crashes on some devices (desktops) because of an issue in system theme detection logic\n\n### Improved\n\n- Updated translations\n- Added tooltips for action buttons\n- Display selected count in the \"Add Multi Download\" page on desktop (#970)\n- Reduced battery consumption\n- Various UI/UX enhancements\n\n## 1.8.1\n\n### Fixed\n\n- Android 10 storage access issue that caused download errors (#977)\n\n### Improved\n\n- Updated translations\n- Better support for adaptive icons on Android (#978)\n- Improved directory picker on Android (#979)\n- Slightly reduced application size\n\n## 1.8.0\n\n### Added\n\n- Android Support\n- macOS users can now use homebrew to install/update the application\n\n### Fixed\n\n- Some HLS streams are not recognized properly\n\n### Improved\n\n- Updated translations\n- UI improvements\n\n## 1.7.1\n\n### Added\n\n- Support for custom data directory (#895)\n\n### Fixed\n\n- System shortcuts not working on the main page (#885)\n- Segment info display issues\n- Suggested file names from browsers are now automatically corrected before use (#896)\n\n### Improved\n\n- Translations updated\n- Download list UI improvements (#897)\n- Extra icon sizes added to the Windows `.ico` file\n\n## 1.7.0\n\n### Added\n\n- Support for downloading media files: audio, video, non encrypted HLS streams from the browser (browser extension needs\n  to be updated)\n- Option to customize each item individually in the “Add Multiple Downloads” page (by right-clicking on each item) (\n  #866)\n- “Download Page” and “Custom User-Agent” options are now available in the “Add Download” > \"Configs\" dialog\n- Ability to remove recently used save locations (#873)\n\n### Changed\n\n- Browser integration API updated; updating the browser extension is required to support new features\n\n### Improved\n\n- Updated translations\n- The download creation time is now set to when the “Add Download” dialog is opened (#846)\n\n## 1.6.14\n\n### Fixed\n\n- An issue causing slow download speeds on some websites\n\n### Improved\n\n- Updated translations\n- Download Engine improvements\n- Minor UI improvements\n\n## 1.6.13\n\n### Fixed\n\n- **Access Denied** error could sometimes happen when adding a list of downloads (#826)\n\n### Improved\n\n- Updated translations\n- Download engine improvements (#828)\n- **Customize Table Columns** popup now supports drag to reorder (#830)\n\n## 1.6.12\n\n### Added\n\n- **Per Host Settings** — save username, password, thread count, user-agent, and more for specific hosts (#820)\n- Support for using **Move** action by holding **Shift** during drag & drop (#821)\n\n### Changed\n\n- UI scale is now relative to the system scale instead of using a fixed value (#814)\n\n### Fixed\n\n- Encoding issue with the default download folder on Linux (#810)\n\n### Improved\n\n- Updated translations\n- Enhanced multi-display support (#814)\n- Main window now remembers its maximized state (#815)\n\n## 1.6.11\n\n### Added\n\n- Option to change the download size unit (#804)\n\n### Fixed\n\n- \"Permission denied\" error when starting a new download (#795)\n\n### Improved\n\n- Updated translations\n- Improved settings page (#805)\n- Automatically fix illegal characters in server-provided filenames (#781)\n- Better handling of filenames received from the server (#780)\n- Use the OS default download location on first launch (#789)\n\n## 1.6.10\n\n### Added\n\n- New Black theme (#767)\n\n### Fixed\n\n- Restored missing executable permissions for files inside archives (macOS & Linux) (#765)\n- Eliminated flickering on the \"New Update\" page (#770)\n\n### Improved\n\n- Updated translations\n- Hovering between menus now works without closing the open one (#766)\n- Better item selection and new keyboard shortcuts on the queue items page (#769)\n- Small UI improvements\n\n## 1.6.9\n\n### Fixed\n\n- \"Keep System Awake\" was not properly cancelled on Windows (#755)\n- \"Create Desktop Entry\" had issue if the path contains spaces on Linux (#733)\n- Application crash on systems that have invalid font names (#737)\n- Some settings statuses were not updating correctly (#732)\n- \"Download Dialog\" position shifted when multiple dialogs were open simultaneously (#758)\n- \"Add Download Dialog\" position shifted when multiple dialogs were open simultaneously (#761)\n\n### Improved\n\n- Translations updated\n- Better handling of filenames received from the server (#759)\n\n## 1.6.8\n\n### Fixed\n\n- Can't change Auto Shutdown option if it was enabled\n\n## 1.6.7\n\n### Added\n\n- Lithuanian language support\n- Kurdish (Sorani) language support\n- Option to automatically shut down the system when downloads or queues complete (#726)\n- Option to delete partial files on download cancellation (#724)\n\n### Changed\n\n- App icon is now hidden from the Dock at runtime on macOS when the system tray is used (#710)\n- Default max download retry count changed to 3\n\n### Fixed\n\n- Removable storages are no longer monitored on Windows (#705)\n\n### Improved\n\n- Translations updated\n- System stays awake while downloads are in progress (#725)\n- Confirm dialogs and buttons now have better focus behavior\n- Dropdowns are now searchable (#706)\n- IO Operations improvements\n\n## 1.6.6\n\n### Added\n\n- Option to create desktop entry on Linux (#698)\n\n### Changed\n\n- Renamed Linux desktop and autostart files for better compatibility (#699)\n\n### Fixed\n\n- Removed duplicate UI Scale option in the Settings (#697)\n\n### Improved\n\n- Updated translations\n- Pressing \"Stop All\" now closes all active download windows (#700)\n\n## 1.6.5\n\n### Added\n\n- New themes (Deep Ocean, new Dark, new Light)\n- Category accepted file types are now optional (#690)\n- Option to change the app font (#692)\n- Option to switch between relative and absolute date/time formats (#694)\n- Option to clear all items in the queue at once\n\n### Changed\n\n- Renamed the previous Dark theme to **Obsidian**\n- Renamed the previous Light theme to **Light Gray**\n\n### Fixed\n\n- Issue where the app wouldn’t start for some Windows users (#695)\n\n### Improved\n\n- Updated translations\n- General UI Improvements\n- Automatically scroll to new downloads on the main page (#672)\n- Improved path validation for new downloads (#693)\n\n## 1.6.4\n\n### Added\n\n- Queues are now visible on the home page, next to the categories (#661)\n- In-app update is now supported on macOS (#627)\n- New option to enable the native menu bar on macOS (#646)\n\n### Fixed\n\n- macOS: Window now activates properly when \"Show Downloads\" is clicked from the system tray (#632)\n- Linux: Startup desktop entry now includes an icon (#634)\n- An issue where the \"Edit Download\" page could unintentionally change the download status (#641)\n- Queue status not updated properly sometimes (#663)\n\n### Improved\n\n- Translations updated\n- Minor UI improvements\n\n## 1.6.3\n\n### Added\n\n- Korean Language\n- An option to append \".part\" extension to incomplete downloads (disabled by default)\n\n### Fixed\n\n- Prevent freeze when opening a file or folder\n- Some websites close the connection if we ask for resume support\n- Some non-standard links not captured correctly\n- Crash when opening browser integration links on macOS\n- Multiselect with Meta key not working as expected on macOS\n- Multiselect not stopped properly after window focus lost\n\n### Improved\n\n- Translations updated\n- Minor UI/UX improvements\n\n## 1.6.2\n\n### Added\n\n- Thai Language\n\n### Fixed\n\n- System Tray crashes sometimes in Linux\n- Icons not rendered properly sometimes in Linux\n- System Tray icon color in macOS\n- Quit handler in macOS\n\n### Improved\n\n- Translations updated\n- Respect user defined position of system buttons in Linux\n\n## 1.6.1\n\n### Fixed\n\n- Application shortcut in Windows have no icon\n\n### Improved\n\n- Translations updated\n\n## 1.6.0\n\n### Added\n\n- macOS support\n- Polish Language\n- Hungarian Language\n- Luri Bakhtiari Language\n- Silent Download option in the Browser Integration\n- Donate button in the app to support the project\n\n### Fixed\n\n- Overriding an existing download sometimes didn't work as expected.\n- \"Start Queue\" checkbox sometimes did not work as expected.\n\n### Improved\n\n- Translations updated\n- Custom Window decorations\n- Window dragging on Linux is now handled by the OS\n- Each platform now uses its own system button style\n- JVM updated to version 21\n\n## 1.5.8\n\n### Added\n\n- An option to allow update existing download from \"Add Download\" page if a duplicate is detected\n\n### Fixed\n\n- Crash when opening \"Items\" section of \"Queues\" page\n\n### Improved\n\n- Translations updated\n- Duplicate download detection\n- Minor UI/UX improvements\n\n## 1.5.7\n\n### Added\n\n- Drag and Drop files to other categories or external applications\n\n### Fixed\n\n- \"Parts Info\" section in the \"Download Progress\" window does not expand for the first time\n\n### Improved\n\n- Translations Updated\n- Improved UI rendering on Windows, resulting in higher FPS.\n- Minor UI/UX Improvements\n\n## 1.5.6\n\n### Added\n\n- Finnish Language Support\n- An option to make the start time of queues optional\n- An ability to edit saved checksums on the \"File Checksum Checker\" page'\n\n### Changed\n\n- The \"Close\" button in the \"Download Progress\" window has been renamed to \"Cancel\" (this stops the download and closes\n  the window). To close the window without stopping the download, use the \"X\" button.\n\n### Fixed\n\n- An issue where filenames in email attachments were not captured correctly\n- The updater wouldn't resume after the download was stopped\n- \"Open Folder\" doesn’t work properly on Linux when the file name contains special characters.\n- Changing settings in the 'Download Progress' window also affects other download items!\n\n### Improved\n\n- Translations updated\n- \"Download Progress\" and \"Queues\" windows UI improvements\n- Pressing \"Download Browser Integration\" the download page will be opened in the corresponding browser\n\n## 1.5.5\n\n### Added\n\n- Japanese Language\n- An option to automatically \"Retry Failed Downloads\" (Disabled by default for now)\n- An option to Import/Export download credentials as curl command\n\n### Fixed\n\n- The download progress sometimes shows incorrect speeds and ETAs\n- Some unverified hostnames can't be used when the \"Ignore SSL Certificate\" is enabled\n- Startup on Boot issue in macOS\n- Drag And Drop of links issue in macOS\n- Some shortcuts didn't work properly in macOS\n- System Tray didn't work in macOS\n\n### Improved\n\n- Translations updated\n- Minor UI improvements\n- App Icon size in macOS\n- Override \"About\" dialog in macOS\n\n## 1.5.4\n\n### Added\n\n- The app now supports full portability by creating an empty `.abdm` directory in the installation folder.\n- An option to delete user data (configuration files) when using Windows Uninstaller.\n\n### Fixed\n\n- Unchecked \"Use Category\" didn't work as expected.\n\n### Improved\n\n- Download engine improvements.\n- Translation updated.\n\n## 1.5.3\n\n### Added\n\n- Vietnamese language.\n- An option to \"Select Queue\" dialog to start the queue immediately.\n- An option to allow user set custom User-agent in the settings.\n- An option to not \"Use Category\" by default.\n- An option to disable SSL Certificate Verification.\n- An option to show/hide icon labels in the main toolbar (you can hover over them to see their labels).\n- An option to not use System Tray.\n\n### Fixed\n\n- Sometimes Thread count not applied correctly.\n- The download completion dialog appeared even when its option is disabled.\n- Some servers return 256 Bytes instead of full size\n\n### Improved\n\n- Translations updated\n- Minor UI improvements\n- Use system language as default language\n- Proxy Settings page improved\n\n## 1.5.2\n\n### Added\n\n- An ability to validate downloads with File Checksum\n- System Proxy support\n- Proxy Auto Configuration (pac) support\n\n### Changed\n\n- Maximum allowed thread count has been increased\n\n### Fixed\n\n- Fixed the incorrect System Tray name on Linux.\n\n### Improved\n\n- Translations Updated\n- Settings window size will be remembered now\n\n## 1.5.1\n\n### Added\n\n- Italian Language\n- German Language\n- Georgian Language\n- Indonesian Language\n- An option to change download speed unit\n- An ability to start new download using Rest-Api\n\n### Fixed\n\n- System tray in Linux now has correct icon and native option menu\n- App crashes when changing theme in Linux\n- Open file/folder action fails sometimes in Windows\n\n### Improved\n\n- Translations updated\n- Split category and location configuration options in multi download page\n- Home page minor UI improvements\n\n## 1.5.0\n\n### Added\n\n- In App Update feature.\n- An option to track deleted files on disk and remove them from the download list (either manually or automatically).\n- Delete option to the Main toolbar.\n\n### Changed\n\n- UI Scale maximum value increased to 3x.\n\n### Fixed\n\n- When you change \"Default download Folder\", the \"Download Location\" of categories also updated (if they are inside \"\n  Default Download Location\").\n- Issue on the \"Add Multiple Download\" page where the \"Save Mode\" set to \"All in Same Location\" was not functioning as\n  expected.\n- App does not start on boot when installation path contains space.\n\n### Improved\n\n- Redesigned \"About\" Page.\n- Translations updated.\n- \"Extra Config\" section on the \"Add Download\" page was not displaying correctly in some languages.\n\n## 1.4.4\n\n### Added\n\n- UI Scale option\n\n### Changed\n\n- The \"Selection\" cell in download list table can be hidden (optional)\n\n### Fixed\n\n- Improved \"Open Folder\" in Windows\n- Support third party file managers in Windows\n- \"Download Progress\" Window shows up even if the \"Auto Show Progress Window\" option is disabled\n- Improved \"Confirm Delete Download\" UX\n- Improved the readability of shortcut text in menus\n- Improved sort of download list by status\n- Resize handle moves in opposite direction for RTL languages\n- Improved \"Home\" page\n- Improved \"About\" page\n- Updated translations\n\n## 1.4.3\n\n### Added\n\n- \"Download Completion\" window\n- \"Exit Confirmation\" dialog when there is active download\n- An Option to automatically show \"Download Completion\" window (Optional)\n- An Option to automatically show \"Download Progress\" window when user presses on \"Resume\" (Optional)\n\n### Changed\n\n- Default thread count is now 8\n- Shape of filename of release binaries changed (added arch name after platform name)\n\n### Fixed\n\n- \"Delete entire list\" task does not remove all downloads\n- Filename not detected correctly from some download servers\n- Rename download changes state to paused if it was finished\n- Improved installation script for linux\n- Improved \"Settings\" page\n- Translations updated\n\n## 1.4.2\n\n### Added\n\n- Edit Download Page (Rename, Refresh links/credentials etc…)\n- Translators Credit Page\n- Traditional Chinese Language\n- Spanish Language\n- French Language\n- Turkish Language\n\n### Changed\n\n- Updated translations\n\n## 1.4.1\n\n### Added\n\n- Portuguese (Brazilian) Language\n\n### Changed\n\n- Updated some languages\n\n### Fixed\n\n- Language names not shown by their native names\n- Selected language not saved properly\n- Wrong text for \"Close\" button in \"Batch Download\" page\n\n## 1.4.0\n\n### Added\n\n- Localization Support\n- Persian Language\n- Arabic Language\n- Chinese (Simplified) Language\n- Ukrainian Language\n- Russian Language\n- Albanian Language\n- Bengali Language\n\n### Changed\n\n- Category Download Location is now optional\n\n### Fixed\n\n- A bug in Download Engine\n- \"Add Queue\" page will be shown properly when opened from \"Import List\" page\n\n## 1.3.0\n\n### Added\n\n- Proxy Support\n- Categories now can have URL patterns\n\n### Fixed\n\n- Application freezes a while when we drag(and drop) a large file on it\n- Improved category section in the home screen\n\n## 1.2.0\n\n- in this version we replaced Wix installer with Nsis for better customization and more control over the installation\n  process in Windows.\n- if you use Windows in order to install this version please first uninstall the previous msi version (your settings and\n  downloads will be safe)\n\n### Added\n\n- You can now create and customize categories\n- Pause/Resume in header actions\n\n### Changed\n\n- Change installer in Windows from Wix to Nsis\n- Improved import link page\n\n## 1.1.0\n\n### Added\n\n- Added Batch Download\n- Added an option to merge TitleBar with MenuBar (disabled by default)\n- Added two cli options --version, --exit\n\n### Fixed\n\n- Fixed Opening downloaded file creates a subprocess in Windows\n- Fixed that some non-standard links not imported correctly\n- Improved window custom decoration logic and title bar position\n- Improved settings page\n\n## 1.0.10\n\n### Fixed\n\n- Improve home page\n- Improve download page\n- Opening Directory Picker cause the app to crash in Linux\n- Folder opened two times when clicking on open folder in Linux\n\n## 1.0.9\n\n### Added\n\n- Sparse File Allocation\n\n### Fixed\n\n- Improve directory picker\n- Improve show help UX in settings\n- Improve Download Engine\n\n## 1.0.8\n\n### Added\n\n- Use server Last-Modified time option in settings\n- Show Open File button if new download already exists and completed\n\n### Fixed\n\n- Improved custom window decoration in linux\n- When we click on open folder in linux it opens the file instead!\n- Some URLEncoded filenames are not decoded properly\n\n## 1.0.7\n\n### Added\n\n- support follow system Dark/Light mode\n- auto paste link (if any) from clipboard when opening add url page\n\n### Fixed\n\n- App is now open in center of screen\n- Some settings doesn't persist after app restart\n- Download speed shows a high value incorrectly when we reopen the app window after a while\n- Some files not downloaded correctly now fixed\n\n## 1.0.6\n\n### Added\n\n- Add Community and Browser Integration links to the app menu\n\n### Changed\n\n- Change default download folder\n\n### Fixed\n\n- Exception will not throw anymore if System Tray is not supported by the OS\n\n## 1.0.5\n\n- Improve Download Engine\n\n## 1.0.4\n\n- Improved UI/UX in Download Page\n\n## 1.0.3\n\n- Download Info Page now show users that a download file supports resuming or not\n\n### Fixed\n\n- Download links that does not support resume now handled correctly\n- Some Web pages does not download correctly\n\n## 1.0.2\n\n- Error messages improvements\n\n### Fixed\n\n- handle some webservers does not respect requested range at first place\n\n## 1.0.1\n\n- UI improvements\n\n### Changed\n\n- repository url updated\n\n## 1.0.0\n\n- This is the first release of the app\n\n### Added\n\n- Multi Connection File Download\n- Speed limiter\n- Download Queues\n- Download Scheduler\n- DownloadManager Browser Integration Support\n- Dark/Light themes\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to AB Download Manager\n\n> ❌ **Important Notice:** The entire codebase is being completely rewritten. Pull requests are **not accepted** at this\n> time, as incoming changes may be lost or conflict heavily with ongoing refactoring. Please wait until the refactor is\n> complete before submitting any PRs.\n\nThank you for your interest in contributing to AB Download Manager! I appreciate any help you can offer.\n\n## What Contributions Are Accepted?\n\nI welcome the following types of contributions:\n\n- **Bug Reports**: If you find a bug, please report it by opening an issue with details about the problem.\n\n- **Feature Requests**: Have an idea for a new feature? Let me know by opening an issue or starting a discussion.\n\n- **Translations**: You can translate AB Download Manager into other languages on Crowdin. See the Translations section\n  below for more information.\n\n- **Pull Requests**: If you’d like to contribute code, feel free to submit a pull request. Just make sure to read the guidelines below before you start.\n\n## Translations\n\nIf you’d like to help translate AB Download Manager into another language, or improve existing translations, you can do\nso on Crowdin. Here’s how:\n\n- Visit the project in [Crowdin](https://crowdin.com/project/ab-download-manager)\n- Please DO NOT submit translations via pull requests.\n- If you want to add a new language, please see [here](https://github.com/amir1376/ab-download-manager/issues/144)\n\n## Pull Requests\n\nIf you're ready to contribute code, that's awesome! Before you start, here’s what you need to know about creating a pull request (PR):\n\n- **Discuss First**: Before you start working on a PR, please open an issue or discussion to explain what you want to do. This helps me understand your idea and make sure it's something that can be merged. It also saves time if changes are needed.\n\n- **Fork the Repo**: Fork this repository to your own GitHub account. This creates your own copy where you can make changes.\n\n- **Create a Branch**: In your forked repository, create a new branch for your changes. Give it a descriptive name, like feature/add-some-feature or fix/some-error.\n\n- **Make Your Changes**: Now you can start coding! Make sure your changes follow the project’s coding standards.\n\n- **Submit the PR**: Once you're happy with your changes, push your branch to your fork and submit a pull request. In the PR description, explain what changes you made and why.\n\n- **Review & Feedback**: I’ll review your PR as soon as I can. There might be some feedback or requests for changes, so be ready to make adjustments if needed.\n\n- **Merging**: If everything looks good, I’ll merge your PR into the master branch.\n"
  },
  {
    "path": "DONATE.md",
    "content": "# ❤️ Donate\n\nWant to support the project? You can make a donation using these crypto addresses:\n\n<a href=\"#ton\" alt=\"Toncoin\"><img src=\"https://img.shields.io/badge/Donate-Toncoin-0098EA?logo=ton\" /></a>\n<a href=\"#usdt\" alt=\"USDT\"><img src=\"https://img.shields.io/badge/Donate-USDT-26A17B?logo=tether\" /></a>\n\n## TON\n\nAddress (TON): `UQAAPTagY3Y9XWJc9IMYGFYdVHugoBV_Xa3OjdsBHax69eYg`\n\n## USDT\n\nAddress (TRC-20): `TK8hMh24yGZGUAYwuSf8rRXncm6s9LJmAx`\n\n\nIf you make a contribution, please text me in [Telegram](https://t.me/Amir_Ai), so I can thank you personally. Thank you for your support!"
  },
  {
    "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  <a href=\"https://abdownloadmanager.com\" target=\"_blank\">\n    <img width=\"180\" src=\"assets/logo/app_logo_with_background.svg\" alt=\"AB Download Manager Logo\">\n  </a>\n</div>\n<h1 align=\"center\">AB Download Manager</h1>\n<p align=\"center\">\n    <a href=\"https://github.com/amir1376/ab-download-manager/releases/latest\"><img alt=\"GitHub Release\" src=\"https://img.shields.io/github/v/release/amir1376/ab-download-manager?color=greenlight&label=latest%20release\"></a>\n    <a href=\"https://abdownloadmanager.com\"><img alt=\"AB Download Manager Website\" src=\"https://img.shields.io/badge/project-website-purple?&labelColor=gray\"></a>\n    <a href=\"https://t.me/abdownloadmanager_discussion\"><img alt=\"Telegram Group\" src=\"https://img.shields.io/badge/Telegram-Group-blue?logo=telegram&labelColor=gray\"></a>\n    <a href=\"https://t.me/abdownloadmanager\"><img alt=\"Telegram Channel\" src=\"https://img.shields.io/badge/Telegram-Channel-blue?logo=telegram&labelColor=gray\"></a>\n    <a href=\"https://crowdin.com/project/ab-download-manager\"><img alt=\"Crowdin\" src=\"https://badges.crowdin.net/ab-download-manager/localized.svg\"></a>\n</p>\n\n<a href=\"https://abdownloadmanager.com\" target=\"_blank\">\n    <img alt=\"AB Download Manager Banner\" src=\"assets/banners/app_banner.png\"/>\n</a>\n\n\n## Description\n\n[AB Download Manager](https://abdownloadmanager.com) is a desktop app that helps you manage and organize your downloads more efficiently than ever before.\n\n## Features\n\n- ⚡️ Faster Download Speed\n- ⏰ Queues and Schedulers\n- 🌐 Browser Extensions\n- 💻 Multiplatform (Android / Windows / Linux / Mac)\n- 🌙 Multiple Themes (Dark/Light/Black and more) with modern UI\n- ❤️ Free and Open Source\n\nPlease visit [Project Website](https://abdownloadmanager.com) for more info.\n\n## Installation\n\n### Download and Install the App\n\n<a href=\"https://abdownloadmanager.com\"><img src=\"https://img.shields.io/badge/Official%20Website-897BFF?logo=abdownloadmanager&logoColor=fff&style=flat-square\" alt=\"Official Website\" height=\"32\" /></a>\n<a href=\"https://github.com/amir1376/ab-download-manager/releases/latest\"><img src=\"https://img.shields.io/badge/GitHub%20Releases-2a2f36?logo=github&logoColor=fff&style=flat-square\" alt=\"GitHub Releases\" height=\"32\" /></a>\n\n#### Installation script (Linux)\n\n```bash\nbash <(curl -fsSL https://raw.githubusercontent.com/amir1376/ab-download-manager/master/scripts/install.sh)\n```\n\n#### Winget or Scoop (for Windows)\n\n**winget**:\n\n```bash\nwinget install amir1376.ABDownloadManager\n```\n\n**scoop**:\n\n```bash\nscoop install extras/abdownloadmanager\n```\n\n#### Homebrew (for macOS & Linux)\n\n```bash\nbrew tap amir1376/tap && brew install --cask ab-download-manager\n```\n\n> ⚠️ **Warning:** This software is NOT on Google Play or other app stores unless listed here. Any version **claiming to be or related to this project** should be considered SCAM and UNSAFE.\n\nFor alternative installation methods, uninstallation instructions, and more details, please refer to the [wiki](https://github.com/amir1376/ab-download-manager/wiki/) page.\n\n### Browser Extensions\n\nYou can download the browser extension to integrate the app with your browser.\n\n<p align=\"left\">\n<a href=\"https://addons.mozilla.org/firefox/addon/ab-download-manager/\">\n    <picture>\n        <img alt=\"Chrome Extension\" src=\"./assets/banners/firefox-extension.png\" height=\"48\">\n    </picture>\n</a>\n<a href=\"https://chromewebstore.google.com/detail/bbobopahenonfdgjgaleledndnnfhooj\">\n    <picture>\n        <source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/banners/chrome-extension_dark.png\" height=\"48\">\n        <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/banners/chrome-extension_light.png\" height=\"48\">\n        <img alt=\"Chrome Extension\" src=\"./assets/banners/chrome-extension_light.png\" height=\"48\">\n    </picture>\n</a>\n</p>\n\n## Screenshots\n\n<div align=\"center\">\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/screenshots/app-home_dark.png\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/screenshots/app-home_light.png\">\n  <img alt=\"App Home Section\" src=\"./assets/screenshots/app-home_dark.png\">\n</picture>\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/screenshots/app-download_dark.png\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/screenshots/app-download_light.png\">\n  <img alt=\"App Download Section\" src=\"./assets/screenshots/app-download_dark.png\">\n</picture>\n</div>\n\n## Project Status & Feedback\n\nPlease keep in mind that this project is in the beginning of its journey.\n**Lots of features** are on the way!\n\n**But**, in the meantime you may face **Bugs or Problems**. If you do, please report them to me via the [Community chat](#community) or through `GitHub Issues`, and I'll do my best to fix them ASAP.\n\n## Community\n\nYou can join our [Telegram Group](https://t.me/abdownloadmanager_discussion) to:\n\n- Report problems\n- Suggest features\n- Get help with the app\n\n## Repositories And Source Code\n\nThere are multiple repositories related to the **AB Download Manager** project:\n\n| Repository                                                                                 | Description                                                                   |\n|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|\n| [Main Application](https://github.com/amir1376/ab-download-manager) (You are here)         | Contains the  **Application** that runs on your  **device**                   |\n| [Browser Integration](https://github.com/amir1376/ab-download-manager-browser-integration) | Contains the **Browser Extension** to be installed on your  **browser**       |\n| [Website](https://github.com/amir1376/ab-download-manager-website)                         | Contains the **AB Download Manager** [website](https://abdownloadmanager.com) |\n\nI've spent a lot of time to create this project.\n\nIf you like my work, please consider giving it a ⭐ — thanks! ❤️\n\n## Bug Report\n\nIf you notice any bugs in the source code, please report them via the `GitHub Issues` section.\n\n## Build From Source\n\nTo compile and test the desktop app on your local machine,\nfollow these steps:\n\n1. Clone the project.\n2. Download and extract the [JBR](https://github.com/JetBrains/JetBrainsRuntime/releases), and make it available by either:\n    \n    - Adding it to your `PATH`, or\n    - Setting the `JAVA_HOME` environment variable to its installation path.\n  \n3. Navigate to the project directory, open your terminal and execute the following command:\n\n    ```bash\n    ./gradlew createReleaseFolderForCi\n    ```\n\n4. The output will be available at:\n\n    ```\n    <project_dir>/build/ci-release\n    ```\n\n> **Note**. This project is compiled and published by GitHub actions [here](./.github/workflows/publish.yml), so if you\n> faced any problem you can check that too.\n\n## Translations\n\nIf you’d like to help translate AB Download Manager into another language, or improve existing translations, you can do\nso on Crowdin. Here’s how:\n\n- Visit the project in [Crowdin](https://crowdin.com/project/ab-download-manager)\n- Please DO NOT submit translations via pull requests.\n- If you want to add a new language, please see [this](https://github.com/amir1376/ab-download-manager/issues/144).\n\n## Contribution\n\nIf you want to contribute to this project, please read [Contributing Guide](CONTRIBUTING.md) first.\n\n## Support the Project\n\nIf you'd like to support the project, you can find details on how to donate in the [DONATE.md](DONATE.md) file.\n"
  },
  {
    "path": "REST-API.yml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Download Service API\n  version: 1.0.0\n  description: API for managing download tasks and queues.\nservers:\n  - url: http://localhost:15151\n    description: Default server running on port 15151\n\npaths:\n  /add:\n    post:\n      summary: Add a new download source\n      requestBody:\n        description: Data for adding a download source\n        content:\n          application/json:\n            schema:\n              type: array\n              items:\n                type: object\n                properties:\n                  link:\n                    type: string\n                    description: The link to the download source\n                  headers:\n                    type: object\n                    additionalProperties:\n                      type: string\n                    description: Optional headers for the request\n                  downloadPage:\n                    type: string\n                    description: Optional download page URL\n      responses:\n        \"200\":\n          description: Successfully added the download\n          content:\n            plain/text:\n              schema:\n                type: string\n                description: OK on success\n\n  /queues:\n    get:\n      summary: Get list of download queues\n      responses:\n        \"200\":\n          description: List of download queues\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    id:\n                      type: integer\n                      description: The unique ID of the queue\n                    name:\n                      type: string\n                      description: The name of the queue\n  /start-headless-download:\n    post:\n      summary: Add a new download task\n      requestBody:\n        description: Data for adding a download task\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                downloadSource:\n                  type: object\n                  properties:\n                    link:\n                      type: string\n                      description: The link to the download source\n                    headers:\n                      type: object\n                      additionalProperties:\n                        type: string\n                      description: Optional headers for the request\n                    downloadPage:\n                      type: string\n                      description: Optional download page URL\n                folder:\n                  type: string\n                  description: Optional folder to save the download (Unix style path)\n                name:\n                  type: string\n                  description: Optional name for the download task\n                queueId:\n                  type: integer\n                  description: Optional queue ID to associate the task with\n      responses:\n        \"200\":\n          description: Successfully added the download task\n          content:\n            plain/text:\n              schema:\n                type: string\n                description: OK on success\n"
  },
  {
    "path": "android/app/.gitignore",
    "content": "release\n"
  },
  {
    "path": "android/app/build.gradle.kts",
    "content": "import buildlogic.CiDirs\nimport buildlogic.CiUtils\nimport buildlogic.versioning.convertToVersionCode\nimport buildlogic.versioning.getAppName\nimport buildlogic.versioning.getAppVersion\nimport buildlogic.versioning.getAppVersionString\nimport buildlogic.versioning.getApplicationPackageName\nimport com.android.build.api.artifact.SingleArtifact\nimport ir.amirab.installer.InstallerTargetFormat\nimport ir.amirab.plugin.common_android.task.SignApkTask\nimport ir.amirab.plugin.common_android.task.androidEnableFileTypesGeneratorForManifest\nimport org.gradle.kotlin.dsl.registering\nimport org.gradle.kotlin.dsl.support.uppercaseFirstChar\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport java.util.Properties\n\nplugins {\n    id(Plugins.Android.application)\n    id(MyPlugins.kotlinAndroid)\n    id(MyPlugins.composeAndroid)\n    id(Plugins.ksp)\n    id(Plugins.Kotlin.serialization)\n    id(Plugins.aboutLibraries)\n    id(Plugins.aboutLibrariesAndroid)\n}\nandroid {\n    defaultConfig {\n        minSdk = 26\n        targetSdk = 36\n        applicationId = getApplicationPackageName()\n        versionCode = getAppVersion().convertToVersionCode()\n        versionName = getAppVersionString()\n    }\n    compileSdk = 36\n    namespace = \"com.abdownloadmanager.android\"\n    buildTypes {\n        debug {\n            applicationIdSuffix = \".debug\"\n            resValue(\"string\", \"app_short_name\", \"AB DM - Debug\")\n        }\n    }\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\nkotlin.compilerOptions {\n    jvmTarget = JvmTarget.JVM_21\n}\ndependencies {\n    implementation(libs.compose.runtime)\n    implementation(libs.compose.foundation)\n    implementation(libs.androidx.activity.compose)\n    implementation(libs.decompose.jbCompose)\n    implementation(libs.aboutLibraries.core)\n    implementation(project(\":shared:app\"))\n    ksp(libs.arrow.opticKsp)\n}\n\nandroidEnableFileTypesGeneratorForManifest(\n    targetActivityClass = \".pages.add.AddDownloadActivity\",\n    fileTypesFile = project.layout.projectDirectory.file(\"filetypes.txt\")\n)\n\n\n// ======= begin of GitHub action stuff\nval ciDir = CiUtils.getCiDir(project)\nandroidComponents.onVariants { variant ->\n    tasks.register(\n        \"createReleaseSignedBinary${variant.name.uppercaseFirstChar()}\",\n        SignApkTask::class\n    ) {\n        inputDir.set(variant.artifacts.get(SingleArtifact.APK))\n        outputDIr.set(project.layout.buildDirectory.dir(\"generatedSignedApks\"))\n        platformToolsVersion.set(\"36.1.0\")\n        keystoreUri.set(provider {\n            getFromEnvOrProperties(\"ABDM_KEYSTORE_FILE\")\n        })\n        keystorePassword.set(provider {\n            getFromEnvOrProperties(\"ABDM_KEYSTORE_FILE_PASSWORD\")\n        })\n        keyPassword.set(provider {\n            getFromEnvOrProperties(\"ABDM_KEYSTORE_KEY_PASSWORD\")\n        })\n        keyAlias.set(provider {\n            getFromEnvOrProperties(\"ABDM_KEYSTORE_KEY_ALIAS\")\n        })\n    }\n}\n\nval androidBinaries by tasks.registering {\n    val signedApks = tasks.named(\"createReleaseSignedBinaryRelease\")\n        .map { task ->\n            task.outputs.files.singleFile\n        }\n    inputs.dir(signedApks)\n    outputs.dir(ciDir.binariesDir)\n    doLast {\n        // at the moment we only have one apk\n        // if I decided to add multiple targets (arm64 x64 etc..)\n        // ... I need to extract arch and use forEach instead of first\n        val signedApk = signedApks.get().listFiles()\n            .first { it.name.endsWith(\".apk\") }\n        val outputFileName = CiUtils.getTargetFileName(\n            getAppName(),\n            getAppVersion(),\n            InstallerTargetFormat.Apk,\n            null,\n        )\n        CiUtils.copyAndHashToDestination(\n            src = signedApk,\n            destinationFolder = ciDir.binariesDir.get().asFile,\n            name = outputFileName,\n        )\n    }\n}\n\ntasks.register(CiUtils.getCreateBinaryFolderForCiTaskName()) {\n    dependsOn(androidBinaries)\n}\n\n\nprivate val localProperties by lazy {\n    val file = project.rootProject.projectDir.resolve(\"local.properties\")\n    file.inputStream().use {\n        Properties().apply { load(it) }\n    }\n}\n\nfun getFromEnvOrProperties(key: String): String? {\n    val string = (System.getenv(key)?.takeIf { it.isNotEmpty() }\n        ?: localProperties.getProperty(key))\n    return string\n}\n"
  },
  {
    "path": "android/app/filetypes.txt",
    "content": "# music / audio\nmp3\naac\nm4a\nwav\nflac\nogg\nwma\namr\nopus\nmid\n\n# video\nmp4\nmkv\navi\nmov\n3gp\nwebm\nwmv\nflv\nmpeg\nmpg\nm4v\nts\n\n# image\njpg\njpeg\npng\ngif\nbmp\nwebp\nsvg\ntiff\nico\nheic\nraw\n\n# documents / text formats\npdf\ntxt\ndoc\ndocx\nxls\nxlsx\nppt\npptx\nrtf\nodt\nods\nodp\ncsv\njson\nxml\nyaml\nyml\nini\ncfg\nmd\nlog\n\n# compressed\nzip\nrar\n7z\ntar\ngz\nbz2\nxz\niso\n\n# executables\nexe\nmsi\napk\njar\nbat\ncmd\nsh\ndeb\nrpm\n\n# fonts\nttf\notf\nwoff\nwoff2\n\n# ebooks\nepub\nmobi\nazw3\ndjvu\n\n# subtitles\nsrt\nass\nvtt\n\n# design / creative\npsd\nai\neps\n\n# miscellaneous\ndat\nbin\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest\n        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    <!--    Start on boot-->\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_SPECIAL_USE\"/>\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\"/>\n    <application\n            android:name=\".ABDMApp\"\n            android:theme=\"@style/Theme.ABDownloadManager\"\n            android:icon=\"@mipmap/ic_launcher\"\n            android:roundIcon=\"@mipmap/ic_launcher_round\"\n            android:usesCleartextTraffic=\"true\"\n            android:requestLegacyExternalStorage=\"true\"\n            android:label=\"@string/app_short_name\"\n    >\n        <activity\n                android:name=\".ui.MainActivity\"\n                android:exported=\"true\"\n                android:launchMode=\"singleInstance\"\n                android:windowSoftInputMode=\"adjustResize\"\n        >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <activity\n                android:name=\".pages.singledownload.SingleDownloadPageActivity\"\n                android:exported=\"true\"\n                android:theme=\"@style/Theme.ABDownloadManager.Transparent\"\n                android:windowSoftInputMode=\"adjustResize\"\n        />\n        <activity\n                android:name=\".pages.browser.BrowserActivity\"\n                android:label=\"@string/app_browser\"\n                android:exported=\"true\"\n                android:theme=\"@style/Theme.ABDownloadManager\"\n                android:windowSoftInputMode=\"adjustResize\"\n                android:configChanges=\"orientation\"\n        >\n            <intent-filter>\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                <data android:scheme=\"http\"/>\n                <data android:scheme=\"https\"/>\n            </intent-filter>\n        </activity>\n        <activity-alias\n                android:name=\"com.abdownloadmanager.browser.BrowserIconInLauncher\"\n                android:enabled=\"false\"\n                android:exported=\"true\"\n                android:targetActivity=\".pages.browser.BrowserActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity-alias>\n\n        <!--        Accept external download from browsers/send intents etc...-->\n        <activity\n                android:name=\".pages.add.AddDownloadActivity\"\n                android:exported=\"true\"\n                android:theme=\"@style/Theme.ABDownloadManager.Transparent\"\n        />\n\n        <activity\n                android:name=\".pages.add.single.AddSingleDownloadActivity\"\n                android:exported=\"false\"\n                android:theme=\"@style/Theme.ABDownloadManager.Transparent\"\n                android:windowSoftInputMode=\"adjustResize\"\n        />\n\n        <activity\n                android:name=\".pages.add.multiple.AddMultiDownloadActivity\"\n                android:exported=\"false\"\n                android:theme=\"@style/Theme.ABDownloadManager\"\n                android:windowSoftInputMode=\"adjustResize\"\n        />\n\n        <activity\n                android:name=\".pages.directorypicker.DirectoryPickerActivity\"\n                android:exported=\"false\"\n                android:theme=\"@style/Theme.ABDownloadManager.Transparent\"\n                android:windowSoftInputMode=\"adjustResize\"\n        />\n\n        <activity\n                android:name=\".pages.crashreport.CrashReportActivity\"\n                android:exported=\"true\"\n                android:theme=\"@style/Theme.ABDownloadManager.Transparent\"\n                android:windowSoftInputMode=\"adjustResize\"\n        />\n\n        <service\n                android:name=\".service.DownloadSystemService\"\n                android:exported=\"false\"\n                android:foregroundServiceType=\"specialUse\"\n        />\n\n        <receiver\n                android:name=\".receiver.StartOnBootBroadcastReceiver\"\n                android:enabled=\"false\"\n                android:exported=\"false\"\n        >\n            <intent-filter>\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\"/>\n            </intent-filter>\n        </receiver>\n\n        <provider\n                android:name=\"androidx.core.content.FileProvider\"\n                android:authorities=\"${applicationId}.provider\"\n                android:exported=\"false\"\n                android:grantUriPermissions=\"true\">\n            <meta-data\n                    android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                    android:resource=\"@xml/provider_paths\"/>\n        </provider>\n\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ABDMApp.kt",
    "content": "package com.abdownloadmanager.android\n\nimport android.app.Application\nimport com.abdownloadmanager.android.di.Di\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.AndroidGlobalExceptionHandler\nimport com.abdownloadmanager.android.util.AppInfo\nimport com.abdownloadmanager.android.util.ApplicationBackgroundTracker\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.appinfo.PreviousVersion\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass ABDMApp : Application(), KoinComponent {\n    val TAG_NAME = ABDMApp::class.simpleName!!\n    val appManager: ABDMAppManager by inject()\n    val appRepository: BaseAppRepository by inject()\n    val previousVersion: PreviousVersion by inject()\n    val scope: CoroutineScope by inject()\n    override fun onCreate() {\n        super.onCreate()\n        AppInfo.init(this)\n        Di.boot(this)\n        ApplicationBackgroundTracker.startTracking(this)\n        appRepository.boot()\n        previousVersion.boot()\n        Thread.setDefaultUncaughtExceptionHandler(\n            AndroidGlobalExceptionHandler(\n                this,\n                Thread.getDefaultUncaughtExceptionHandler(),\n            )\n        )\n        appManager.boot()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/action/Actions.kt",
    "content": "package com.abdownloadmanager.android.action\n\nimport com.abdownloadmanager.android.util.pagemanager.IBrowserPageManager\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\n\nfun createOpenBrowserAction(\n    browserPageManager: IBrowserPageManager,\n): AnAction {\n    return simpleAction(\n        Res.string.browser.asStringSource(),\n        MyIcons.earth,\n    ) {\n        browserPageManager.openBrowser(null)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/di/Di.kt",
    "content": "package com.abdownloadmanager.android.di\n\nimport AndroidDirectLinkUpdateApplier\nimport android.app.Application\nimport android.content.Context\nimport com.abdownloadmanager.github.GithubApi\nimport com.abdownloadmanager.UpdateDownloadLocationProvider\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.android.ABDMApp\nimport com.abdownloadmanager.android.pages.home.HomePageStateToPersist\nimport com.abdownloadmanager.android.pages.onboarding.permissions.ABDMPermissions\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.receiver.StartOnBootBroadcastReceiver\nimport com.abdownloadmanager.android.repository.AppRepository\nimport com.abdownloadmanager.android.storage.AndroidExtraDownloadItemSettings\nimport com.abdownloadmanager.android.storage.AndroidExtraQueueSettings\nimport com.abdownloadmanager.android.storage.AndroidOnBoardingStorage\nimport com.abdownloadmanager.android.storage.AppSettingsStorage\nimport com.abdownloadmanager.android.storage.BrowserBookmarksStorage\nimport com.abdownloadmanager.android.storage.HomePageStorage\nimport com.abdownloadmanager.android.storage.OnBoardingData\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.ABDMServiceNotificationManager\nimport com.abdownloadmanager.android.util.AndroidDefinedPaths\nimport com.abdownloadmanager.android.util.AndroidDownloadItemOpener\nimport com.abdownloadmanager.android.util.AppInfo\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport ir.amirab.downloader.queue.QueueManager\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.DownloadSettings\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.OkHttpHttpDownloaderClient\nimport ir.amirab.downloader.db.*\nimport ir.amirab.downloader.monitor.DownloadMonitor\nimport ir.amirab.downloader.utils.IDiskStat\nimport com.abdownloadmanager.resources.ABDMLanguageResources\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSDownloaderInUi\nimport com.abdownloadmanager.shared.downloaderinui.http.HttpDownloaderInUi\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.storage.PerHostSettingsDatastoreStorage\nimport com.abdownloadmanager.shared.storage.ProxyDatastoreStorage\nimport com.abdownloadmanager.shared.storage.impl.LastSavedLocationStorage\nimport com.abdownloadmanager.shared.ui.theme.ThemeSettingsStorage\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.updater.UpdateDownloaderViaDownloadSystem\nimport com.abdownloadmanager.shared.util.AndroidDiskStat\nimport com.abdownloadmanager.shared.util.AndroidSystemThemeDetector\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.DefinedPaths\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.UserAgentProviderFromSettings\nimport com.abdownloadmanager.shared.util.*\nimport com.abdownloadmanager.updateapplier.UpdateApplier\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.util.config.datastore.createMapConfigDatastore\nimport kotlinx.coroutines.*\nimport kotlinx.serialization.json.Json\nimport okhttp3.Dispatcher\nimport okhttp3.OkHttpClient\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.context.startKoin\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\nimport com.abdownloadmanager.updatechecker.GithubUpdateChecker\nimport com.abdownloadmanager.updatechecker.UpdateChecker\nimport ir.amirab.util.AppVersionTracker\nimport com.abdownloadmanager.shared.util.appinfo.PreviousVersion\nimport com.abdownloadmanager.shared.util.autoremove.RemovedDownloadsFromDiskTracker\nimport com.abdownloadmanager.shared.util.category.*\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.NoOpOnDownloadCompletionActionProvider\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionProvider\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionRunner\nimport com.abdownloadmanager.shared.util.onqueuecompletion.NoopOnQueueCompletionActionProvider\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueEventActionRunner\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueCompletionActionProvider\nimport com.abdownloadmanager.shared.util.perhostsettings.IPerHostSettingsStorage\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.ui.IMyIcons\nimport com.abdownloadmanager.shared.util.proxy.IProxyStorage\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport ir.amirab.downloader.DownloaderRegistry\nimport ir.amirab.downloader.connection.UserAgentProvider\nimport ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider\nimport ir.amirab.downloader.connection.proxy.NoopSystemProxySelectorProvider\nimport ir.amirab.downloader.connection.proxy.ProxyStrategyProvider\nimport ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloader\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloader\nimport ir.amirab.downloader.monitor.DownloadItemStateFactory\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.queue.ManualDownloadQueue\nimport ir.amirab.downloader.utils.EmptyFileCreator\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.compose.localizationmanager.LanguageSourceProvider\nimport ir.amirab.util.compose.localizationmanager.LanguageStorage\nimport ir.amirab.util.config.datastore.kotlinxSerializationDataStore\nimport ir.amirab.util.startup.AbstractStartupManager\nimport ir.amirab.util.startup.Startup\nimport kotlinx.serialization.modules.SerializersModule\nimport kotlinx.serialization.modules.polymorphic\nimport okhttp3.Protocol\nimport okhttp3.internal.tls.OkHostnameVerifier\n\nval downloaderModule = module {\n    single<IDownloadQueueDatabase> {\n        val definedPaths = get<DefinedPaths>()\n\n        DownloadQueueFileStorageDatabase(\n            queueFolder = get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.queuesDir\n            ),\n            fileSaver = get(),\n        )\n    }\n    single<IDownloadListDb> {\n        val definedPaths = get<DefinedPaths>()\n        DownloadListFileStorage(\n            downloadListFolder = get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.downloadListDir\n            ),\n            fileSaver = get(),\n        )\n    }\n    single {\n        TransactionalFileSaver(get())\n    }\n    single<IDownloadPartListDb> {\n        val definedPaths = get<DefinedPaths>()\n        PartListFileStorage(\n            get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.partsDir\n            ),\n            get()\n        )\n    }\n    single<IDiskStat> {\n        AndroidDiskStat()\n    }\n    single<ISystemThemeDetector> {\n        AndroidSystemThemeDetector(get())\n    }\n    single {\n        QueueManager(get(), get())\n    }\n    single {\n        DownloadFoldersRegistry()\n    }\n    single {\n        DownloadSettings(\n            8,\n        )\n    }\n    single {\n        ProxyManager(\n            get()\n        )\n    }.bind<ProxyStrategyProvider>()\n    single<SystemProxySelectorProvider> {\n        NoopSystemProxySelectorProvider()\n    }\n    single<AutoConfigurableProxyProvider> {\n        AutoConfigurableProxyProvider.NoOp()\n    }\n    single<UserAgentProvider> {\n        UserAgentProviderFromSettings(get())\n    }\n    single<HttpDownloaderClient> {\n        OkHttpHttpDownloaderClient(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }\n    single {\n        val downloadSettings: DownloadSettings = get()\n        EmptyFileCreator(\n            diskStat = get(),\n            useSparseFile = { downloadSettings.useSparseFileAllocation }\n        )\n    }\n    single {\n        HLSDownloader(inject())\n    }\n    single {\n        HLSDownloaderInUi(get(), get())\n    }\n    single {\n        HttpDownloader(inject())\n    }\n    single {\n        HttpDownloaderInUi(get(), get())\n    }\n    single {\n        DownloaderInUiRegistry().apply {\n            add(get<HttpDownloaderInUi>())\n            add(get<HLSDownloaderInUi>())\n        }\n    }.bind<DownloadItemStateFactory<IDownloadItem, DownloadJob>>()\n    single {\n        DownloaderRegistry().apply {\n            add(get<HttpDownloader>())\n            add(get<HLSDownloader>())\n        }\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        DownloadManager(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.downloadDataDir\n            )\n        )\n    }.bind(DownloadManagerMinimalControl::class)\n    single {\n        ManualDownloadQueue(get(), get())\n    }\n    single<IDownloadMonitor> {\n        DownloadMonitor(\n            downloadManager = get(),\n            manualDownloadQueue = get(),\n            downloadItemStateFactory = inject(),\n        )\n    }\n}\nval downloadSystemModule = module {\n    single {\n        val definedPaths = get<DefinedPaths>()\n        get<DownloadFoldersRegistry>().registerAndGet(definedPaths.categoriesDir)\n        CategoryFileStorage(\n            file = definedPaths.categoriesFile.toFile(),\n            fileSaver = get()\n        )\n    }.bind<CategoryStorage>()\n    single {\n        FileIconProviderUsingCategoryIcons(\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }.bind<FileIconProvider>()\n    single {\n        DefaultCategories(\n            icons = get(),\n            getDefaultDownloadFolder = {\n                get<BaseAppSettingsStorage>().defaultDownloadFolder.value\n            }\n        )\n    }\n    single {\n        DownloadManagerCategoryItemProvider(get())\n    }.bind<ICategoryItemProvider>()\n    single {\n        CategoryManager(\n            categoryStorage = get(),\n            scope = get(),\n            defaultCategoriesFactory = get(),\n            categoryItemProvider = get(),\n        )\n    }\n\n    single {\n        DownloadSystem(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        val extraDownloadSettingsStorageFolder = get<DownloadFoldersRegistry>().registerAndGet(\n            definedPaths.extraDownloadSettings\n        )\n        ExtraDownloadSettingsStorage(\n            extraDownloadSettingsStorageFolder,\n            get(),\n            AndroidExtraDownloadItemSettings\n        )\n    }.bind<IExtraDownloadSettingsStorage<*>>()\n    single {\n        val definedPaths = get<DefinedPaths>()\n        val extraQueueSettingsStorageFolder = get<DownloadFoldersRegistry>().registerAndGet(\n            definedPaths.extraQueueSettings\n        )\n        ExtraQueueSettingsStorage(\n            extraQueueSettingsStorageFolder,\n            get(),\n            AndroidExtraQueueSettings\n        )\n    }.apply {\n        bind<IExtraQueueSettingsStorage<*>>()\n    }\n    single<OnDownloadCompletionActionProvider> {\n        NoOpOnDownloadCompletionActionProvider()\n    }\n    single<OnQueueCompletionActionProvider> {\n        NoopOnQueueCompletionActionProvider()\n    }\n    single {\n        OnDownloadCompletionActionRunner(\n            downloadManagerMinimalControl = get(),\n            scope = get(),\n            onDownloadCompletionActionProvider = get(),\n        )\n    }\n    single {\n        OnQueueEventActionRunner(\n            queueManager = get(),\n            scope = get(),\n            onQueueCompletionActionProvider = get(),\n        )\n    }\n    single {\n        PermissionManager(\n            ABDMPermissions.importantPermissions,\n            get(),\n        )\n    }\n}\nval coroutineModule = module {\n    single {\n        CoroutineScope(SupervisorJob())\n    }\n}\nval jsonModule = module {\n    single {\n        val downloaderRegistry: DownloaderRegistry by inject()\n        Json {\n            this.encodeDefaults = true\n            this.prettyPrint = true\n            this.ignoreUnknownKeys = true\n            this.serializersModule = SerializersModule {\n                polymorphic(IDownloadItem::class) {\n                    downloaderRegistry.getAll().forEach {\n                        subclass(it.downloadItemClass, it.downloadItemSerializer)\n                    }\n                    defaultDeserializer {\n                        HttpDownloadItem.serializer()\n                    }\n                }\n                polymorphic(IDownloadCredentials::class) {\n                    downloaderRegistry.getAll().forEach {\n                        subclass(it.downloadCredentialsClass, it.downloadCredentialsSerializer)\n                    }\n                    defaultDeserializer {\n                        HttpDownloadCredentials.serializer()\n                    }\n                }\n            }\n        }\n    }\n}\nval updaterModule = module {\n    single {\n        val definedPaths = get<DefinedPaths>()\n        UpdateDownloadLocationProvider {\n            definedPaths.updateDownloadLocation.toFile()\n        }\n    }\n    single<UpdateApplier> {\n        val definedPaths = get<DefinedPaths>()\n        definedPaths.updateDownloadLocation\n        AndroidDirectLinkUpdateApplier(\n            updateDownloader = UpdateDownloaderViaDownloadSystem(\n                get(),\n                get(),\n            ),\n        )\n    }\n    single<UpdateChecker> {\n        GithubUpdateChecker(\n            AppVersion.get(),\n            githubApi = GithubApi(\n                owner = SharedConstants.projectGithubOwner,\n                repo = SharedConstants.projectGithubRepo,\n                client = OkHttpClient\n                    .Builder()\n                    .build()\n            )\n        )\n    }\n    single {\n        UpdateManager(\n            updateChecker = get(),\n            updateApplier = get(),\n            appVersionTracker = get(),\n        )\n    }\n}\nval startUpModule = module {\n    single {\n        Startup.getStartUpManager(get(), StartOnBootBroadcastReceiver::class.java)\n    }.apply {\n        bind<AbstractStartupManager>()\n    }\n}\n\nfun getAppModule(context: ABDMApp) = module {\n    includes(downloaderModule)\n    includes(downloadSystemModule)\n    includes(coroutineModule)\n    includes(jsonModule)\n    includes(updaterModule)\n    includes(startUpModule)\n//    single {\n//        NetworkChecker(get())\n//    }\n    single {\n        AppInfo.definedPaths\n    }.apply {\n        bind<DefinedPaths>()\n        bind<AndroidDefinedPaths>()\n    }\n    single {\n        AppRepository(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }.apply {\n        bind<BaseAppRepository>()\n        bind<SizeAndSpeedUnitProvider>()\n    }\n    single {\n        ThemeManager(get(), get(), get())\n    }\n//    single {\n//        FontManager(get())\n//    }\n    single {\n        LanguageManager(\n            get(),\n            LanguageSourceProvider(\n                ABDMLanguageResources.defaultLanguageResource,\n                ABDMLanguageResources.languages,\n            )\n        )\n    }\n    single {\n        MyIcons\n    }.apply {\n        bind<IMyIcons>()\n        bind<IIconResolver>()\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        ProxyDatastoreStorage(\n            kotlinxSerializationDataStore(\n                definedPaths.proxySettingsFile.toFile(),\n                get(),\n                ProxyData::default,\n            )\n        )\n    }.bind<IProxyStorage>()\n    single {\n        val definedPaths = get<DefinedPaths>()\n        AppSettingsStorage(\n            createMapConfigDatastore(\n                definedPaths.appSettingsFile.toFile(),\n                get(),\n            )\n        )\n    }.apply {\n        bind<BaseAppSettingsStorage>()\n        bind<LanguageStorage>()\n        bind<ThemeSettingsStorage>()\n    }\n    single {\n        RemovedDownloadsFromDiskTracker(\n            get(), get(), get(),\n        )\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        PreviousVersion(\n            systemPath = definedPaths.systemDir.toFile(),\n            currentVersion = AppVersion.get(),\n        )\n    }\n    single {\n        AppVersionTracker(\n            previousVersion = {\n                // it MUST be booted first\n                get<PreviousVersion>().get()\n            },\n            currentVersion = AppVersion.get(),\n        )\n    }\n\n    single {\n        val appSettingsStorage: BaseAppSettingsStorage = get()\n        AppSSLFactoryProvider(\n            ignoreSSLCertificates = appSettingsStorage.ignoreSSLCertificates\n        )\n    }\n    single {\n        val appSettingsStorage: BaseAppSettingsStorage = get()\n        AppHostNameVerifier(\n            delegateHostnameVerifier = OkHostnameVerifier,\n            ignoreHostNameVerification = appSettingsStorage.ignoreSSLCertificates\n        )\n    }\n    single<OkHttpClient> {\n        val appSSLFactoryProvider: AppSSLFactoryProvider = get()\n        val appHostNameVerifier: AppHostNameVerifier = get()\n        OkHttpClient\n            .Builder()\n            .protocols(listOf(Protocol.HTTP_1_1))\n            .dispatcher(Dispatcher().apply {\n                //bypass limit on concurrent connections!\n                maxRequests = Int.MAX_VALUE\n                maxRequestsPerHost = Int.MAX_VALUE\n            })\n            .sslSocketFactory(\n                appSSLFactoryProvider.createSSLSocketFactory(),\n                appSSLFactoryProvider.trustManager,\n            )\n            .hostnameVerifier(appHostNameVerifier)\n            .build()\n    }\n    single<ILastSavedLocationsStorage> {\n        val definedPaths = get<AndroidDefinedPaths>()\n        LastSavedLocationStorage(\n            kotlinxSerializationDataStore<List<String>>(\n                definedPaths.lastSavedLocationFile.toFile(),\n                get(),\n                ::emptyList,\n            )\n        )\n    }\n    single<IPerHostSettingsStorage> {\n        val definedPaths = get<DefinedPaths>()\n        PerHostSettingsDatastoreStorage(\n            kotlinxSerializationDataStore<List<PerHostSettingsItem>>(\n                definedPaths.perHostSettingsFile.toFile(),\n                get(),\n                ::emptyList,\n            )\n        )\n    }\n    single {\n        PerHostSettingsManager(get())\n    }\n    single { context }.apply {\n        bind<ABDMApp>()\n        bind<Application>()\n        bind<Context>()\n    }\n    single {\n        ABDMAppManager(get(), get(), get(), get(), get(), get(), get())\n    }\n    single {\n        ABDMServiceNotificationManager(get(), get(), get(), get(), get())\n    }\n    single {\n        AndroidDownloadItemOpener(get())\n    }.apply {\n        bind<DownloadItemOpener>()\n    }\n    single { NotificationManager() }\n    single {\n        val paths = get<AndroidDefinedPaths>()\n        AndroidOnBoardingStorage(\n            kotlinxSerializationDataStore(\n                paths.onboardingFile.toFile(),\n                get(),\n                ::OnBoardingData,\n            )\n        )\n    }\n    single {\n        val paths = get<AndroidDefinedPaths>()\n        HomePageStorage(\n            kotlinxSerializationDataStore(\n                paths.homePageFile.toFile(),\n                get(),\n                ::HomePageStateToPersist,\n            )\n        )\n    }\n    single {\n        val paths = get<AndroidDefinedPaths>()\n        BrowserBookmarksStorage(\n            kotlinxSerializationDataStore(\n                paths.browserBookmarksFile.toFile(),\n                get(),\n                ::emptyList,\n            )\n        )\n    }\n}\n\n\nobject Di : KoinComponent {\n    fun boot(applicationContext: ABDMApp) {\n        startKoin {\n            modules(getAppModule(applicationContext))\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/about/AboutPage.kt",
    "content": "package com.abdownloadmanager.android.pages.about\n\nimport androidx.compose.runtime.Composable\n\n\nimport androidx.compose.foundation.*\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.*\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.page.PageFooter\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.page.createAlphaForHeader\nimport com.abdownloadmanager.android.ui.page.rememberHeaderAlpha\nimport com.abdownloadmanager.android.util.compose.useBack\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Tooltip\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.dpToPx\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun AboutPage(\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    val state = rememberScrollState()\n    var paddings by remember { mutableStateOf(PaddingValues.Zero) }\n    val headerAlpha =\n        createAlphaForHeader(state.value.toFloat(), paddings.calculateTopPadding().dpToPx(LocalDensity.current))\n    val shape = myShapes.defaultRounded\n    PageUi(\n        header = {\n            val onBack = useBack()\n            PageHeader(\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        icon = MyIcons.back,\n                        contentDescription = Res.string.back.asStringSource()\n                    ) {\n                        onBack?.onBackPressed()\n                    }\n                },\n                headerTitle = {\n                    PageTitle(myStringResource(Res.string.about))\n                },\n                modifier = Modifier\n                    .background(\n                        myColors.background.copy(\n                            alpha = headerAlpha * 0.75f\n                        )\n                    )\n                    .statusBarsPadding(),\n\n            )\n        },\n        footer = {\n            PageFooter {\n                Column(\n                    Modifier\n                        .fillMaxWidth()\n                        .navigationBarsPadding()\n                        .padding(horizontal = mySpacings.largeSpace)\n                        .padding(bottom = mySpacings.largeSpace)\n                        .border(1.dp, myColors.onBackground / 0.15f, shape)\n                        .clip(shape)\n                        .background(myColors.surface)\n                ) {\n                    Spacer(Modifier.height(mySpacings.largeSpace))\n                    DevelopedWithLove(\n                        Modifier\n                            .fillMaxWidth()\n                            .wrapContentWidth()\n                    )\n                    Spacer(Modifier.height(mySpacings.mediumSpace))\n                    SocialAndLinks(\n                        Modifier\n                            .fillMaxWidth()\n                            .wrapContentWidth(),\n                        horizontalPadding = 8.dp,\n                    )\n                    Spacer(Modifier.height(mySpacings.mediumSpace))\n                    Spacer(\n                        Modifier\n                            .fillMaxWidth()\n                            .background(myColors.onBackground / 0.05f)\n                            .height(1.dp)\n                    )\n                    MainWebsite(Modifier)\n                }\n            }\n        }\n    ) {\n        paddings = it.paddingValues\n        Column(\n            Modifier\n                .fillMaxSize()\n                .verticalScroll(state)\n                .padding(it.paddingValues),\n            verticalArrangement = Arrangement.SpaceBetween,\n        ) {\n            Column(\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = mySpacings.largeSpace),\n            ) {\n                AppIconAndVersion(\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 32.dp)\n                )\n            }\n            CreditsSection(\n                modifier = Modifier\n                    .fillMaxWidth(),\n                onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries,\n                onRequestShowTranslators = onRequestShowTranslators,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AppIconAndVersion(\n    modifier: Modifier,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier.padding(\n            horizontal = 24.dp,\n            vertical = 8.dp,\n        )\n    ) {\n        val shape = RoundedCornerShape(16.dp)\n        Image(\n            MyIcons.appIcon.rememberPainter(),\n            null,\n            Modifier\n                .shadow(12.dp, shape, spotColor = myColors.primary)\n                .clip(shape)\n                .border(\n                    1.dp,\n                    Brush.linearGradient(\n                        listOf(myColors.primary, myColors.secondary)\n                    ),\n                    shape\n                )\n                .background(myColors.surface)\n                .padding(16.dp)\n                .size(52.dp)\n        )\n        Spacer(Modifier.size(16.dp))\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                SharedConstants.appDisplayName,\n                fontSize = myTextSizes.lg,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(Modifier.height(2.dp))\n            WithContentAlpha(0.75f) {\n                Text(\n                    myStringResource(\n                        Res.string.version_n,\n                        Res.string.version_n_createArgs(\n                            value = AppVersion.get().toString(),\n                        )\n                    ),\n                    fontSize = myTextSizes.base,\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nfun MainWebsite(\n    modifier: Modifier\n) {\n    val uriHandler = LocalUriHandler.current\n    val websiteUrl = SharedConstants.projectWebsite\n    val websiteDisplayName = remember(websiteUrl) {\n        HttpUrlUtils.getHost(websiteUrl) ?: websiteUrl\n    }\n    Column(\n        modifier\n            .fillMaxWidth()\n            .clickable {\n                uriHandler.openUri(websiteUrl)\n            }\n            .padding(\n                mySpacings.largeSpace\n            ),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Text(\n            text = websiteDisplayName,\n            color = myColors.info,\n        )\n    }\n}\n\n@Composable\nfun DevelopedWithLove(modifier: Modifier) {\n    Column(\n        modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Text(\n            myStringResource(Res.string.developed_with_love_for_you),\n            Modifier\n                .fillMaxWidth()\n                .wrapContentWidth()\n        )\n        Spacer(Modifier.height(mySpacings.largeSpace))\n        DonateButton(Modifier)\n    }\n}\n\n@Composable\nprivate fun SocialAndLinks(\n    modifier: Modifier = Modifier,\n    horizontalPadding: Dp,\n) {\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n        modifier = modifier\n            .padding(\n                horizontal = horizontalPadding,\n            )\n    ) {\n        SocialSmallButton(\n            MyIcons.earth,\n            Res.string.visit_the_project_website.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectWebsite)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.openSource,\n            Res.string.view_the_source_code.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectSourceCode)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.speaker,\n            Res.string.channel.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.telegramChannelUrl)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.group,\n            Res.string.group.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.telegramGroupUrl)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.language,\n            Res.string.translators_contribute_title.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectTranslations)\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun CreditsSection(\n    modifier: Modifier = Modifier,\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    Column(\n        modifier\n            .padding(horizontal = 16.dp)\n            .padding(vertical = 8.dp),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        val itemModifier = Modifier.fillMaxWidth()\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.hearth,\n            title = Res.string.this_is_a_free_and_open_source_software.asStringSource(),\n            description = Res.string.view_the_source_code.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectSourceCode)\n            }\n        )\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.openSource,\n            title = Res.string.powered_by_open_source_software.asStringSource(),\n            description = Res.string.view_the_open_source_licenses.asStringSource(),\n            onClick = {\n                onRequestShowOpenSourceLibraries()\n            }\n        )\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.language,\n            title = Res.string.localized_by_translators.asStringSource(),\n            description = Res.string.meet_the_translators.asStringSource(),\n            onClick = {\n                onRequestShowTranslators()\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun SocialSmallButton(\n    icon: IconSource,\n    title: StringSource,\n    onClick: () -> Unit,\n) {\n    Tooltip(title) {\n        IconActionButton(\n            icon,\n            contentDescription = title,\n            onClick = onClick,\n        )\n    }\n}\n\n@Composable\nprivate fun AboutPageListItemButton(\n    modifier: Modifier,\n    icon: IconSource,\n    title: StringSource,\n    description: StringSource,\n    onClick: () -> Unit,\n) {\n    val shape = myShapes.defaultRounded\n    Row(\n        modifier\n            .border(1.dp, myColors.onBackground / 0.15f, shape)\n            .clip(shape)\n            .clickable(onClick = onClick)\n            .background(myColors.surface)\n            .padding(\n                horizontal = 8.dp,\n                vertical = 8.dp,\n            ),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        MyIcon(\n            icon = icon,\n            contentDescription = null,\n            modifier = Modifier.size(24.dp)\n        )\n        Spacer(Modifier.width(8.dp))\n        Column {\n            Text(\n                title.rememberString(),\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(Modifier.height(4.dp))\n            WithContentAlpha(0.75f) {\n                Text(description.rememberString())\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun DonateButton(\n    modifier: Modifier,\n) {\n    ActionButton(\n        modifier = modifier,\n        start = {\n            MyIcon(\n                MyIcons.hearth,\n                null,\n                modifier = Modifier.size(24.dp),\n                tint = myColors.error,\n            )\n            Spacer(Modifier.width(8.dp))\n        },\n        text = myStringResource(Res.string.donate),\n        onClick = {\n            URLOpener.openUrl(SharedConstants.donateLink)\n        }\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/AddDownloadActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.add\n\nimport android.content.Intent\nimport android.os.Bundle\nimport arrow.core.firstOrNone\nimport arrow.core.getOrElse\nimport com.abdownloadmanager.android.pages.add.multiple.AddMultiDownloadActivity\nimport com.abdownloadmanager.android.pages.add.single.AddSingleDownloadActivity\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.ui.MainActivity\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.DownloadCredentialFromStringExtractor\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.inject\n\nclass AddDownloadActivity : ABDMActivity() {\n    val json: Json by inject()\n    val permissionManager: PermissionManager by inject()\n    private fun createDownloaderInUiProps(\n        credentials: IDownloadCredentials\n    ): AddDownloadCredentialsInUiProps {\n        return AddDownloadCredentialsInUiProps(\n            credentials,\n            AddDownloadCredentialsInUiProps.Configs(),\n        )\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (!permissionManager.isReady()) {\n            // user not opened the app at least once. we must redirect it to the permission page first\n            val intent = Intent(this, MainActivity::class.java)\n            startActivity(intent)\n            finish()\n            return\n        }\n        val credentials = getDownloadCredentialsFromIntent(intent)\n        val intent = if (credentials.size > 1) {\n            AddMultiDownloadActivity.createIntent(\n                this,\n                AddDownloadConfig.MultipleAddConfig(\n                    newDownloads = credentials.map(::createDownloaderInUiProps),\n                    importOptions = ImportOptions(),\n                ),\n                json = json,\n            )\n        } else {\n            AddSingleDownloadActivity.createIntent(\n                this,\n                AddDownloadConfig.SingleAddConfig(\n                    newDownload = credentials\n                        .firstOrNone()\n                        .getOrElse { HttpDownloadCredentials(\"\") }\n                        .let(::createDownloaderInUiProps),\n                    importOptions = ImportOptions(),\n                ),\n                json = json,\n            )\n        }\n        startActivity(intent)\n        finish()\n    }\n\n    private fun getDownloadCredentialsFromIntent(intent: Intent): List<IDownloadCredentials> {\n        val links = when (intent.action) {\n            Intent.ACTION_SEND -> {\n                intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()\n            }\n\n            else -> {\n                // action view etc...\n                intent.data?.toString().orEmpty()\n            }\n        }\n        return DownloadCredentialFromStringExtractor\n            .extract(links)\n            .distinctBy { it.link }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/multiple/AddMultiDownloadActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.add.multiple\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport com.abdownloadmanager.android.pages.category.CategorySheet\nimport com.abdownloadmanager.android.pages.newqueue.NewQueueSheet\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.android.util.activity.HandleActivityEffects\nimport com.abdownloadmanager.android.util.activity.getSerializedExtra\nimport com.abdownloadmanager.android.util.activity.putSerializedExtra\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.delay\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.inject\n\nclass AddMultiDownloadActivity : ABDMActivity() {\n    private val json: Json by inject()\n    private val downloadSystem: DownloadSystem by inject()\n    private val appManager: ABDMAppManager by inject()\n    private val downloaderInUiRegistry: DownloaderInUiRegistry by inject()\n    private val lastSavedLocationsStorage: ILastSavedLocationsStorage by inject()\n    private val queueManager: QueueManager by inject()\n    private val categoryManager: CategoryManager by inject()\n    private val iconProvider: FileIconProvider by inject()\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val myRetainedComponent = myRetainedComponent {\n            val config = getComponentConfig(intent)\n            val appManager = appManager\n            val closeAddDownloadDialog = {\n                this@myRetainedComponent.finishActivityAction()\n            }\n            AndroidAddMultiDownloadComponent(\n                ctx = it,\n                onRequestClose = closeAddDownloadDialog,\n                lastSavedLocationsStorage = lastSavedLocationsStorage,\n                id = config.id,\n                queueManager = queueManager,\n                categoryManager = categoryManager,\n                downloadSystem = downloadSystem,\n                onRequestAdd = { items, queueId, categorySelectionMode ->\n                    appManager.addDownloads(\n                        items = items,\n                        categorySelectionMode = categorySelectionMode,\n                        queueId = queueId,\n                    )\n                },\n                perHostSettingsManager = perHostSettingsManager,\n                fileIconProvider = iconProvider,\n                appRepository = appRepository,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n            ).apply { addItems(config.newDownloads) }\n        }\n        val addDownloadComponent = myRetainedComponent.component\n        setABDMContent {\n            myRetainedComponent.HandleActivityEffects()\n            AddMultiItemPage(addDownloadComponent)\n            CategorySheet(\n                categoryComponent = addDownloadComponent.categorySlot.rememberChild(),\n                onDismiss = addDownloadComponent::closeCategoryDialog\n            )\n            NewQueueSheet(\n                onQueueCreate = addDownloadComponent::createQueueWithName,\n                isOpened = addDownloadComponent.showAddQueue.collectAsState().value,\n                onCloseRequest = { addDownloadComponent.setShowAddQueue(false) },\n            )\n        }\n    }\n\n    private fun getComponentConfig(intent: Intent): AddDownloadConfig.MultipleAddConfig {\n        runCatching {\n            with(json) {\n                intent.getSerializedExtra<AddDownloadConfig.MultipleAddConfig>(COMPONENT_CONFIG_KEY)\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()?.let {\n            return it\n        }\n        return AddDownloadConfig.MultipleAddConfig()\n    }\n\n    companion object {\n        const val COMPONENT_CONFIG_KEY = \"ComponentConfig\"\n        fun createIntent(\n            context: Context,\n            multipleAddConfig: AddDownloadConfig.MultipleAddConfig,\n            json: Json,\n        ): Intent {\n            val intent = Intent(\n                context,\n                AddMultiDownloadActivity::class.java,\n            )\n            with(json) {\n                intent.putSerializedExtra(COMPONENT_CONFIG_KEY, multipleAddConfig)\n            }\n            return intent\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/multiple/AddMultiDownloadList.kt",
    "content": "package com.abdownloadmanager.android.pages.add.multiple\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.combinedClickable\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.heightIn\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.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.alpha\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.NewMultiDownloadState\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\nfun AddMultiDownloadList(\n    modifier: Modifier,\n    component: AndroidAddMultiDownloadComponent,\n    itePaddingValues: PaddingValues,\n) {\n    val dividerColor = myColors.onBackground / 0.5f\n    val listState by component.filteredList.collectAsState()\n    LazyColumn(modifier) {\n        itemsIndexed(\n            items = listState,\n        ) { index, item ->\n            val isSelected = remember(item, component.selectionList) {\n                component.isSelected(item.id)\n            }\n            val isFirstItem = index == 0\n            RenderAddDownloadItem(\n                state = item,\n                iconProvider = component.fileIconProvider,\n                onSelectionChange = { selected ->\n                    component.setSelect(item.id, selected)\n                },\n                onLongPress = {\n                    component.openConfigurableList(\n                        item.id\n                    )\n                },\n                isSelected = isSelected,\n                itemPadding = itePaddingValues,\n                modifier = Modifier.ifThen(!isFirstItem) {\n                    drawBehind {\n                        drawLine(\n                            brush = Brush.horizontalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    dividerColor,\n                                    Color.Transparent,\n                                )\n                            ),\n                            start = Offset.Zero,\n                            end = Offset(size.width, 0f)\n                        )\n                    }\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderAddDownloadItem(\n    state: NewMultiDownloadState,\n    iconProvider: FileIconProvider,\n    isSelected: Boolean,\n    onLongPress: () -> Unit,\n    onSelectionChange: (Boolean) -> Unit,\n    itemPadding: PaddingValues,\n    modifier: Modifier,\n) {\n    val name = state.name\n    val icon = iconProvider.rememberIcon(name)\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .heightIn(mySpacings.thumbSize)\n            .ifThen(isSelected) {\n                background(\n                    myColors.selectionGradient(1f, 0.5f)\n                )\n            }\n            .combinedClickable(\n                onClick = {\n                    onSelectionChange(!isSelected)\n                },\n                onLongClick = {\n                    onLongPress()\n                }\n            )\n            .padding(itemPadding)\n\n    ) {\n        Column {\n            Text(\n                text = state.link,\n                color = LocalContentColor.current / 0.75f,\n                maxLines = 1,\n                overflow = TextOverflow.MiddleEllipsis,\n            )\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            Text(\n                text = name.takeIf { it.isNotEmpty() } ?: \"...\",\n                maxLines = 1,\n                modifier = Modifier.basicMarquee(),\n            )\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            val sizeTitle = myStringResource(Res.string.size)\n            val sizeValue = state.sizeString.rememberString()\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CheckBox(\n                    value = isSelected,\n                    onValueChange = {\n                        onSelectionChange(it)\n                    },\n                    size = 24.dp,\n                )\n                Spacer(Modifier.width(mySpacings.largeSpace))\n                MyIcon(\n                    icon = icon,\n                    contentDescription = null,\n                    modifier = Modifier\n                        .size(24.dp)\n                        .alpha(0.75f)\n                )\n                Spacer(Modifier.width(mySpacings.largeSpace))\n                Text(\n                    text = \"$sizeTitle: $sizeValue\",\n                    maxLines = 1,\n                )\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/multiple/AddMultiItemPage.kt",
    "content": "package com.abdownloadmanager.android.pages.add.multiple\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.add.shared.CategoryAddButton\nimport com.abdownloadmanager.android.pages.add.shared.CategorySelect\nimport com.abdownloadmanager.android.pages.add.shared.ExtraConfig\nimport com.abdownloadmanager.android.pages.add.shared.LocationTextField\nimport com.abdownloadmanager.android.pages.add.shared.ShowAddToQueueDialog\nimport com.abdownloadmanager.android.ui.RenderControlSelections\nimport com.abdownloadmanager.android.ui.SelectionControlButton\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitleWithDescription\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun AddMultiItemPage(\n    addMultiDownloadComponent: AndroidAddMultiDownloadComponent,\n) {\n    val hasSelection = addMultiDownloadComponent.selectionList.isNotEmpty()\n    BackHandler(hasSelection) {\n        addMultiDownloadComponent.selectAll(false)\n    }\n    val pageHorizontalPadding = 16.dp\n    PageUi(\n        modifier = Modifier\n            .background(myColors.background)\n            .statusBarsPadding(),\n        header = {\n            val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher\n            PageHeader(\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        MyIcons.back,\n                        contentDescription = Res.string.back.asStringSource()\n                    ) {\n                        backDispatcher?.onBackPressed()\n                    }\n                },\n                headerTitle = {\n                    PageTitleWithDescription(\n                        title = myStringResource(\n                            Res.string.add_download\n                        ),\n                        description = myStringResource(\n                            Res.string.add_multi_download_page_header\n                        )\n                    )\n                }\n            )\n        },\n        footer = {\n            Footer(\n                Modifier,\n                addMultiDownloadComponent,\n            )\n        },\n    ) {\n        Column(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.background)\n                .padding(it.paddingValues)\n        ) {\n            AddMultiDownloadList(\n                Modifier.weight(1f),\n                addMultiDownloadComponent,\n                itePaddingValues = PaddingValues(\n                    horizontal = pageHorizontalPadding,\n                    vertical = 16.dp,\n                )\n            )\n        }\n    }\n    val currentDownloadConfigurableList by addMultiDownloadComponent.currentDownloadConfigurableList.collectAsState()\n    currentDownloadConfigurableList?.let {\n        ExtraConfig(\n            onDismiss = {\n                addMultiDownloadComponent.openConfigurableList(null)\n            },\n            configurables = it,\n            isOpened = true,\n        )\n    }\n    ShowAddToQueueDialog(\n        queueList = addMultiDownloadComponent.queueList.collectAsState().value,\n        onQueueSelected = { queue, startQueue ->\n            addMultiDownloadComponent.requestAddDownloads(\n                queue, startQueue\n            )\n        },\n        onClose = {\n            addMultiDownloadComponent.closeAddToQueue()\n        },\n        isOpened = addMultiDownloadComponent.showAddToQueue,\n        newQueueAction = addMultiDownloadComponent.newQueueAction,\n    )\n}\n\n\n@Composable\nfun Footer(\n    modifier: Modifier = Modifier,\n    component: AndroidAddMultiDownloadComponent,\n) {\n    Column(\n        modifier\n            .fillMaxWidth()\n            .background(myColors.surface)\n            .navigationBarsPadding()\n            .imePadding()\n    ) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onSurface / 0.15f)\n        )\n        Column(\n            Modifier\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 16.dp),\n        ) {\n            val total = component.totalList.size\n            val showMoreOptions by component.showMoreOptions.collectAsState()\n            RenderControlSelections(\n                onRequestSelectAll = { component.selectAll(true) },\n                onRequestSelectInside = { component.toggleSelectInside() },\n                onRequestInvertSelection = { component.inverseSelection() },\n                total = total,\n                selectionCount = component.selectionList.size,\n            ) {\n                SelectionControlButton(\n                    icon = if (showMoreOptions) {\n                        MyIcons.down\n                    } else {\n                        MyIcons.up\n                    },\n                    contentDescription = Res.string.more_options.asStringSource(),\n                    onClick = {\n                        component.setShowMoreOptions(!showMoreOptions)\n                    }\n                )\n            }\n            AnimatedVisibility(\n                showMoreOptions\n            ) {\n                Column {\n                    Spacer(Modifier.height(8.dp))\n                    SaveSettings(\n                        modifier = Modifier\n                            .fillMaxWidth(),\n                        component = component,\n                    )\n                    val text = component.filterText.collectAsState().value\n                    Spacer(Modifier.height(8.dp))\n                    MyTextFieldWithIcons(\n                        modifier = Modifier\n                            .fillMaxWidth(),\n                        text = text,\n                        onTextChange = component::setFilterText,\n                        placeholder = myStringResource(Res.string.search),\n                        end = {\n                            MyTextFieldIcon(\n                                MyIcons.clear,\n                                enabled = text.isNotEmpty(),\n                            ) {\n                                component.setFilterText(\"\")\n                            }\n                        }\n                    )\n                    Spacer(Modifier.height(8.dp))\n                }\n            }\n            Spacer(Modifier.height(8.dp))\n            Row(\n                Modifier\n            ) {\n                val buttonModifier = Modifier.weight(1f)\n                PrimaryMainActionButton(\n                    text = myStringResource(Res.string.add),\n                    onClick = {\n                        component.openAddToQueueDialog()\n                    },\n                    enabled = component.canClickAdd,\n                    modifier = buttonModifier,\n                )\n//                ActionButton(\n//                    text = myStringResource(Res.string.cancel),\n//                    onClick = {\n//                        component.requestClose()\n//                    },\n//                    modifier = buttonModifier,\n//                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SaveSettings(\n    modifier: Modifier,\n    component: AndroidAddMultiDownloadComponent,\n) {\n    val selectedCategory by component.selectedCategory.collectAsState()\n\n    val folder by component.folder.collectAsState()\n\n    Column(modifier) {\n        Text(\"${myStringResource(Res.string.save_to)}:\")\n        Spacer(Modifier.height(8.dp))\n        Column(Modifier.fillMaxWidth()) {\n            CategorySaveOption(selectedCategory, component)\n            Spacer(Modifier.height(8.dp))\n            LocationSaveOption(component, folder)\n            Spacer(Modifier)\n        }\n    }\n}\n\n@Composable\nprivate fun LocationSaveOption(\n    component: AndroidAddMultiDownloadComponent,\n    folder: String\n) {\n    val allItemsInSameLocation by component.allInSameLocation.collectAsState()\n    SaveOption(\n        title = myStringResource(Res.string.all_items_in_one_Location),\n        selectedHelp = myStringResource(Res.string.all_items_in_one_Location_description),\n        unselectedHelp = myStringResource(Res.string.unselected_all_items_in_specific_location_description),\n        selected = allItemsInSameLocation,\n        onSelectedChange = {\n            component.setAllItemsInSameLocation(it)\n        },\n        selectedContent = {\n            LocationTextField(\n                text = folder,\n                setText = {\n                    component.setFolder(it)\n                },\n                modifier = Modifier.fillMaxWidth(),\n                lastUsedLocations = component.lastUsedLocations.collectAsState().value,\n                onRequestRemoveSaveLocation = component::removeFromLastDownloadLocation\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun CategorySaveOption(\n    selectedCategory: Category?,\n    component: AndroidAddMultiDownloadComponent\n) {\n\n    SaveOption(\n        title = myStringResource(Res.string.all_items_in_one_category),\n        selectedHelp = myStringResource(Res.string.all_items_in_one_category_description),\n        unselectedHelp = myStringResource(Res.string.each_item_on_its_own_category_description),\n        selected = selectedCategory != null,\n        onSelectedChange = {\n            if (it) {\n                component.setSelectedCategory(component.categories.value.firstOrNull())\n            } else {\n                component.setSelectedCategory(null)\n            }\n            component.setAlsoAutoCategorize(!it)\n        },\n        selectedContent = {\n            Row(\n                Modifier\n                    .height(IntrinsicSize.Max)\n                    .fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CategorySelect(\n                    categories = component.categories.collectAsState().value,\n                    modifier = Modifier.weight(1f),\n                    selectedCategory = component.selectedCategory.collectAsState().value,\n                    onCategorySelected = {\n                        component.setSelectedCategory(it)\n                    }\n                )\n                Spacer(Modifier.width(8.dp))\n                CategoryAddButton(\n                    Modifier.fillMaxHeight(),\n                    enabled = true,\n                    onClick = {\n                        component.onRequestAddCategory()\n                    },\n                )\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun SaveOption(\n    title: String,\n    selectedHelp: String,\n    unselectedHelp: String,\n    selected: Boolean,\n    onSelectedChange: (Boolean) -> Unit,\n    selectedContent: @Composable () -> Unit\n) {\n    ExpandableItem(\n        modifier = Modifier\n            .fillMaxWidth(),\n        isExpanded = selected,\n        header = {\n            Row(\n                modifier = Modifier\n                    .heightIn(mySpacings.thumbSize)\n                    .clickable { onSelectedChange(!selected) },\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                CheckBox(\n                    value = selected,\n                    onValueChange = onSelectedChange,\n                    size = 24.dp\n                )\n                Text(title)\n                Help(if (selected) selectedHelp else unselectedHelp)\n            }\n        },\n        body = {\n            Column {\n                Spacer(Modifier.height(8.dp))\n                selectedContent()\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/multiple/AndroidAddMultiDownloadComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.add.multiple\n\nimport com.abdownloadmanager.shared.action.createNewQueueAction\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NewQueuePageManager\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.BaseAddMultiDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.OnRequestAdd\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.activate\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.dismiss\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.builtins.serializer\n\nclass AndroidAddMultiDownloadComponent(\n    ctx: ComponentContext,\n    id: String,\n    onRequestClose: () -> Unit,\n    onRequestAdd: OnRequestAdd,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n    perHostSettingsManager: PerHostSettingsManager, downloadSystem: DownloadSystem,\n    fileIconProvider: FileIconProvider,\n    appRepository: BaseAppRepository,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n) : BaseAddMultiDownloadComponent(\n    ctx = ctx,\n    id = id,\n    lastSavedLocationsStorage = lastSavedLocationsStorage,\n    onRequestAdd = onRequestAdd,\n    onRequestClose = onRequestClose,\n    perHostSettingsManager = perHostSettingsManager,\n    downloadSystem = downloadSystem,\n    appRepository = appRepository,\n    fileIconProvider = fileIconProvider,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n), NewQueuePageManager, CategoryDialogManager {\n    val categoryComponentNavigation = SlotNavigation<Long>()\n    val categorySlot = childSlot(\n        source = categoryComponentNavigation,\n        childFactory = { config, ctx ->\n            CategoryComponent(\n                ctx = ctx,\n                id = config,\n                close = ::closeCategoryDialog,\n                submit = { submittedCategory ->\n                    if (submittedCategory.id < 0) {\n                        categoryManager.addCustomCategory(submittedCategory)\n                    } else {\n                        categoryManager.updateCategory(\n                            submittedCategory.id\n                        ) {\n                            submittedCategory.copy(\n                                items = it.items\n                            )\n                        }\n                    }\n                    closeCategoryDialog()\n                },\n            )\n        },\n        serializer = Long.serializer(),\n    ).subscribeAsStateFlow()\n    val newQueueAction = createNewQueueAction(\n        scope,\n        this,\n    )\n\n    override fun openCategoryDialog(categoryId: Long) {\n        scope.launch {\n            categoryComponentNavigation.activate(categoryId)\n        }\n    }\n\n    override fun closeCategoryDialog() {\n        scope.launch {\n            categoryComponentNavigation.dismiss()\n        }\n    }\n\n    override fun getCategoryPageManager(): CategoryDialogManager {\n        return this\n    }\n\n    private val _showMoreInputs = MutableStateFlow(false)\n    val showMoreOptions = _showMoreInputs.asStateFlow()\n    fun setShowMoreOptions(value: Boolean) {\n        _showMoreInputs.value = value\n    }\n\n    private val _showAddQueue = MutableStateFlow(false)\n    val showAddQueue = _showAddQueue.asStateFlow()\n    fun setShowAddQueue(value: Boolean) {\n        _showAddQueue.value = value\n    }\n\n    fun createQueueWithName(name: String) {\n        scope.launch { queueManager.addQueue(name) }\n        setShowAddQueue(false)\n    }\n\n    override fun closeNewQueueDialog() {\n        setShowAddQueue(false)\n    }\n\n    override fun openNewQueueDialog() {\n        setShowAddQueue(true)\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/shared/CategorySelect.kt",
    "content": "package com.abdownloadmanager.android.pages.add.shared\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.*\nimport com.abdownloadmanager.android.ui.configurable.RenderSpinnerInSheet\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.rememberIconPainter\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun CategorySelect(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    categories: List<Category>,\n    selectedCategory: Category?,\n    onCategorySelected: (Category) -> Unit,\n) {\n    var isSelectionOpen by remember {\n        mutableStateOf(false)\n    }\n    val closeDialog = {\n        isSelectionOpen = false\n    }\n    RenderSelectedCategory(\n        modifier = modifier,\n        item = selectedCategory,\n        enabled = enabled,\n        onClick = {\n            isSelectionOpen = true\n        },\n        renderItem = {\n            RenderCategory(\n                category = it,\n                modifier = Modifier,\n            )\n        }\n    )\n    selectedCategory?.let {\n        RenderSpinnerInSheet(\n            title = Res.string.categories.asStringSource(),\n            isOpened = isSelectionOpen,\n            onDismiss = closeDialog,\n            possibleValues = categories,\n            render = {\n                RenderCategory(\n                    category = it,\n                    modifier = Modifier,\n                )\n            },\n            value = selectedCategory,\n            onSelect = {\n                onCategorySelected(it)\n            },\n//        renderEmpty = {\n//            Column(\n//                modifier = Modifier.fillMaxSize().wrapContentSize(),\n//                horizontalAlignment = Alignment.CenterHorizontally,\n//            ) {\n//                MyIcon(MyIcons.info, null, Modifier.size(64.dp))\n//                Spacer(Modifier.height(16.dp))\n//                Text(\n//                    myStringResource(Res.string.no_categories_found),\n//                    fontWeight = FontWeight.Bold,\n//                    fontSize = myTextSizes.lg,\n//                )\n//            }\n//        }\n        )\n    }\n}\n\n@Composable\nprivate fun RenderCategory(\n    modifier: Modifier,\n    category: Category,\n) {\n    Row(\n        modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val icon = category.rememberIconPainter()\n        val iconModifier = Modifier.size(16.dp)\n        if (icon != null) {\n            MyIcon(\n                icon,\n                null,\n                iconModifier,\n            )\n        } else {\n            Spacer(iconModifier)\n        }\n        Spacer(Modifier.width(8.dp))\n        Text(\n            category.name,\n            softWrap = false,\n            maxLines = 1,\n            modifier = Modifier.weight(1f)\n        )\n    }\n}\n\n@Composable\nfun CategoryAddButton(\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    IconActionButton(\n        modifier = modifier,\n        icon = MyIcons.add,\n        contentDescription = Res.string.add_category.asStringSource(),\n        enabled = enabled,\n        onClick = onClick,\n    )\n}\n\n@Composable\nprivate fun <T> RenderSelectedCategory(\n    item: T?,\n    enabled: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier,\n    renderItem: @Composable (T) -> Unit,\n) {\n    val borderColor = myColors.onBackground / 0.1f\n//    val background = myColors.surface / 50\n    val shape = myShapes.defaultRounded\n    Row(\n        modifier\n            .height(IntrinsicSize.Max)\n            .heightIn(mySpacings.thumbSize)\n            .clip(shape)\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .border(1.dp, borderColor, shape)\n\n//            .background(background)\n            .clickable(\n                enabled = enabled\n            ) { onClick() }\n            .padding(horizontal = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val contentModifier = Modifier\n            .padding(vertical = 8.dp)\n            .weight(1f)\n        if (item != null) {\n            Box(contentModifier) {\n                renderItem(item)\n            }\n        } else {\n            Text(\n                myStringResource(Res.string.no_category_selected),\n                contentModifier\n            )\n        }\n        Spacer(\n            Modifier\n                .padding(horizontal = 8.dp)\n                .fillMaxHeight()\n                .padding(vertical = 1.dp)\n                .width(1.dp)\n                .background(borderColor)\n        )\n        MyIcon(\n            MyIcons.down,\n            null,\n            Modifier\n                .align(Alignment.CenterVertically)\n                .size(16.dp),\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/shared/ExtraConfig.kt",
    "content": "package com.abdownloadmanager.android.pages.add.shared\n\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\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.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport io.github.oikvpqya.compose.fastscroller.rememberScrollbarAdapter\n\n@Composable\nfun ExtraConfig(\n    isOpened: Boolean,\n    onDismiss: () -> Unit,\n    configurables: List<Configurable<*>>,\n) {\n    val dialogState = rememberResponsiveDialogState(false)\n    LaunchedEffect(isOpened) {\n        if (isOpened) {\n            dialogState.show()\n        } else {\n            dialogState.hide()\n        }\n    }\n    dialogState.OnFullyDismissed {\n        onDismiss()\n    }\n    ResponsiveDialog(\n        state = dialogState,\n        onDismiss = {\n            dialogState.hide()\n        }\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\"Extra Config\")\n                    },\n                    headerActions = {}\n                )\n            }\n        ) {\n            Box {\n                val scrollState = rememberScrollState()\n                Column(\n                    Modifier.verticalScroll(scrollState)\n                ) {\n                    for ((index, cfg) in configurables.withIndex()) {\n                        RenderConfigurable(\n                            cfg,\n                            ConfigurableUiProps(\n                                itemPaddingValues = PaddingValues(vertical = 8.dp, horizontal = 32.dp)\n                            )\n                        )\n                        if (index != configurables.lastIndex) {\n                            Divider()\n                        }\n                    }\n                }\n                MultiplatformVerticalScrollbar(\n                    rememberScrollbarAdapter(scrollState),\n                    Modifier\n                        .matchParentSize()\n                        .wrapContentWidth()\n                        .align(Alignment.CenterEnd)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier\n            .fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/shared/LocationTextField.kt",
    "content": "package com.abdownloadmanager.android.pages.add.shared\n\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MyDropDown\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.directorypicker.rememberAndroidDirectoryPickerLauncher\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.asStringSource\nimport java.io.File\n\n@Composable\nfun LocationTextField(\n    modifier: Modifier,\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    lastUsedLocations: List<String> = emptyList(),\n    onRequestRemoveSaveLocation: (String) -> Unit,\n) {\n    var showLastUsedLocations by remember { mutableStateOf(false) }\n\n    val downloadLauncherFolderPickerLauncher = rememberAndroidDirectoryPickerLauncher(\n        title = Res.string.download_location.asStringSource(),\n        initialDirectory = remember(text) {\n            runCatching {\n                File(text).canonicalPath\n            }.getOrNull()\n        },\n    ) { directory ->\n        directory?.let(setText)\n    }\n\n    var widthForDropDown by remember {\n        mutableStateOf(0.dp)\n    }\n    val density = LocalDensity.current\n    Box(modifier) {\n        MyTextFieldWithIcons(\n            text,\n            setText,\n            myStringResource(Res.string.location),\n            modifier = Modifier\n                .fillMaxWidth()\n                .onGloballyPositioned {\n                    widthForDropDown = with(density) {\n                        it.size.width.toDp()\n                    }\n                },\n            errorText = errorText,\n            end = {\n                Row {\n                    MyTextFieldIcon(MyIcons.folder) {\n                        downloadLauncherFolderPickerLauncher.launch()\n                    }\n                    MyTextFieldIcon(MyIcons.down) {\n                        showLastUsedLocations = !showLastUsedLocations\n                    }\n                }\n            }\n        )\n        if (showLastUsedLocations) {\n            ShowSuggestions(\n                width = { widthForDropDown },\n                suggestions = lastUsedLocations,\n                onSuggestionSelected = {\n                    setText(it)\n                    showLastUsedLocations = false\n                },\n                onDismiss = {\n                    showLastUsedLocations = false\n                },\n                onRequestRemove = onRequestRemoveSaveLocation\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ShowSuggestions(\n    width: () -> Dp,\n    suggestions: List<String>,\n    onRequestRemove: (String) -> Unit,\n    onSuggestionSelected: (String) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    MyDropDown(onDismiss) {\n        Column(\n            Modifier\n                .width(width())\n                .clip(myShapes.defaultRounded)\n                .background(myColors.surface)\n                .verticalScroll(rememberScrollState())\n        ) {\n            for (l in suggestions) {\n                Row(\n                    Modifier.height(IntrinsicSize.Max)\n                ) {\n                    Text(\n                        text = l,\n                        modifier = Modifier\n                            .weight(1f)\n                            .clickable {\n                                onSuggestionSelected(l)\n                            }\n                            .padding(vertical = 4.dp, horizontal = 4.dp),\n                        fontSize = myTextSizes.sm\n                    )\n                    MyIcon(\n                        MyIcons.clear,\n                        null,\n                        Modifier\n                            .fillMaxHeight()\n                            .clickable {\n                                onRequestRemove(l)\n                            }\n                            .wrapContentHeight()\n                            .padding(horizontal = 2.dp)\n                            .size(12.dp)\n                            .alpha(0.25f)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/shared/SelectQueue.kt",
    "content": "package com.abdownloadmanager.android.pages.add.shared\n\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\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.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport io.github.oikvpqya.compose.fastscroller.rememberScrollbarAdapter\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun ShowAddToQueueDialog(\n    queueList: List<DownloadQueue>,\n    isOpened: Boolean,\n    onQueueSelected: (Long?, Boolean) -> Unit,\n    newQueueAction: AnAction,\n    onClose: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(isOpened) {\n        if (isOpened) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    state.OnFullyDismissed {\n        onClose()\n    }\n    val (startQueue, setStartQueue) = remember {\n        mutableStateOf(false)\n    }\n    ResponsiveDialog(\n        onDismiss = state::hide,\n        state = state,\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\n                            myStringResource(Res.string.select_queue)\n                        )\n                    }\n                )\n            }\n        ) {\n            WithContentColor(myColors.onBackground) {\n                Column(\n                    Modifier.fillMaxWidth()\n                ) {\n                    Column(\n                        Modifier\n                            .padding(horizontal = 8.dp)\n                            .padding(bottom = 8.dp)\n                    ) {\n                        val addToQueueModifier = Modifier.fillMaxWidth()\n                        Spacer(Modifier.height(8.dp))\n                            val scrollState = rememberScrollState()\n                        VerticalScrollableContent(\n                            scrollState,\n                            Modifier\n                                .border(1.dp, myColors.onBackground / 5, myShapes.defaultRounded)\n                                .padding(1.dp),\n                        ) {\n                            Column(\n                                modifier = Modifier\n                                    .verticalScroll(scrollState)\n                            ) {\n                                for (q in queueList) {\n                                    key(q.id) {\n                                        val queueModel by q.queueModel.collectAsState()\n                                        QueueItemToSelect(\n                                            modifier = addToQueueModifier,\n                                            name = queueModel.name,\n                                            onSelect = {\n                                                onQueueSelected(queueModel.id, startQueue)\n                                            }\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                        Spacer(Modifier.height(8.dp))\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier\n                                .clickable {\n                                    setStartQueue(!startQueue)\n                                }\n                                .padding(vertical = 4.dp)\n                                .padding(start = 2.dp)\n                        ) {\n                            CheckBox(\n                                size = 24.dp,\n                                value = startQueue,\n                                onValueChange = setStartQueue\n                            )\n                            Spacer(Modifier.width(mySpacings.mediumSpace))\n                            Text(myStringResource(Res.string.start_queue))\n                        }\n                        Row(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.SpaceBetween\n                        ) {\n                            IconActionButton(\n                                MyIcons.add,\n                                contentDescription = Res.string.add_new_queue.asStringSource(),\n                                onClick = newQueueAction\n                            )\n                            Spacer(Modifier.width(mySpacings.mediumSpace))\n                            ActionButton(\n                                text = myStringResource(Res.string.without_queue),\n                                modifier = Modifier.weight(1f),\n                                onClick = {\n                                    onQueueSelected(null, startQueue)\n                                }\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun QueueItemToSelect(\n    modifier: Modifier,\n    name: String,\n    onSelect: () -> Unit,\n) {\n    Row(\n        modifier\n            .clickable(onClick = onSelect)\n            .heightIn(mySpacings.thumbSize)\n            .padding(vertical = 4.dp)\n            .padding(horizontal = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            name,\n            fontSize = myTextSizes.base,\n        )\n    }\n}\n\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier\n            .fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/single/AddSingleDownloadActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.add.single\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport com.abdownloadmanager.android.pages.browser.BrowserActivity\nimport com.abdownloadmanager.android.pages.category.CategorySheet\nimport com.abdownloadmanager.android.pages.newqueue.NewQueueSheet\nimport com.abdownloadmanager.android.pages.singledownload.SingleDownloadPageActivity\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.AndroidDownloadItemOpener\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.android.util.activity.HandleActivityEffects\nimport com.abdownloadmanager.android.util.activity.getSerializedExtra\nimport com.abdownloadmanager.android.util.activity.putSerializedExtra\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.inject\n\nclass AddSingleDownloadActivity : ABDMActivity() {\n    private val json: Json by inject()\n    private val downloadSystem: DownloadSystem by inject()\n    private val appManager: ABDMAppManager by inject()\n    private val downloadItemOpener: AndroidDownloadItemOpener by inject()\n    private val downloaderInUiRegistry: DownloaderInUiRegistry by inject()\n    private val lastSavedLocationsStorage: ILastSavedLocationsStorage by inject()\n    private val queueManager: QueueManager by inject()\n    private val categoryManager: CategoryManager by inject()\n    private val iconProvider: FileIconProvider by inject()\n    private val appContext: Context by inject()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val myRetainedComponent = myRetainedComponent {\n            // TODO consider use a factory to create AndroidAddSingleDownloadComponent\n            // we may create memory leaks if we accidentally pass Activity::this into the component lambdas\n            val config = getComponentConfig(intent)\n            val appManager = appManager\n            val appContext = this@AddSingleDownloadActivity.appContext\n            val scope = applicationScope\n            val downloadItemOpener = downloadItemOpener\n            val appSettingsStorage = appSettingsStorage\n            val downloadSystem = downloadSystem\n            val closeAddDownloadDialog = {\n                this@myRetainedComponent.finishActivityAction()\n            }\n            AndroidAddSingleDownloadComponent(\n                ctx = it,\n                onRequestClose = closeAddDownloadDialog,\n                onRequestDownload = { item, categoryId ->\n                    scope.launch {\n                        val id = appManager.startNewDownload(item, categoryId).await()\n                        if (appSettingsStorage.showDownloadProgressDialog.value) {\n                            runCatching {\n                                appContext.startActivity(\n                                    SingleDownloadPageActivity.createIntent(\n                                        appContext,\n                                        id,\n                                        true,\n                                    ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                                )\n                            }.onFailure {\n                                it.printStackTrace()\n                            }\n                        }\n                    }\n                },\n                onRequestAddToQueue = { item, queue, category ->\n                    appManager.addDownload(item, queue, category)\n                },\n                openExistingDownload = {\n                    scope.launch {\n                        downloadItemOpener.openDownloadItem(it)\n                    }\n                },\n                updateExistingDownloadCredentials = { id, newCredentials, downloadJobExtraConfig ->\n                    scope.launch {\n                        downloadSystem.downloadManager.updateDownloadItem(\n                            id = id,\n                            downloadJobExtraConfig = downloadJobExtraConfig,\n                            updater = {\n                                it.withCredentials(newCredentials)\n                            }\n                        )\n//                        openDownloadDialog(id)\n                    }\n                },\n                downloadItemOpener = downloadItemOpener,\n                lastSavedLocationsStorage = lastSavedLocationsStorage,\n                importOptions = config.importOptions,\n                id = config.id,\n                downloaderInUi = downloaderInUiRegistry.getDownloaderOf(config.newDownload.credentials)!!,\n                initialCredentials = config.newDownload,\n                queueManager = queueManager,\n                categoryManager = categoryManager,\n                downloadSystem = downloadSystem,\n                appSettings = appSettingsStorage,\n                iconProvider = iconProvider,\n                appScope = applicationScope,\n                appRepository = appRepository,\n                perHostSettingsManager = perHostSettingsManager,\n            )\n        }\n        val addDownloadComponent = myRetainedComponent.component\n        setABDMContent {\n            myRetainedComponent.HandleActivityEffects()\n            HandleEffects(addDownloadComponent) {\n                if (it is AndroidAddSingleDownloadComponent.Effects.OpenInBrowser) {\n                    startActivity(\n                        BrowserActivity.createIntent(this, it.link)\n                    )\n                    finish()\n                }\n            }\n            val dialogState = rememberResponsiveDialogState(false)\n            dialogState.OnFullyDismissed {\n                addDownloadComponent.onRequestClose()\n            }\n            LaunchedEffect(Unit) {\n                // animate open after activity becomes fully open\n                // is there a better way?\n                delay(10)\n                dialogState.show()\n            }\n            val onDismiss = { dialogState.hide() }\n            ResponsiveDialog(\n                dialogState,\n                onDismiss\n            ) {\n                AddSingleDownloadPage(addDownloadComponent, onDismiss)\n            }\n            CategorySheet(\n                categoryComponent = addDownloadComponent.categorySlot.rememberChild(),\n                onDismiss = addDownloadComponent::closeCategoryDialog\n            )\n            NewQueueSheet(\n                onQueueCreate = addDownloadComponent::createQueueWithName,\n                isOpened = addDownloadComponent.showAddQueue.collectAsState().value,\n                onCloseRequest = { addDownloadComponent.setShowAddQueue(false) },\n            )\n        }\n    }\n\n    private fun getComponentConfig(intent: Intent): AddDownloadConfig.SingleAddConfig {\n        runCatching {\n            with(json) {\n                intent.getSerializedExtra<AddDownloadConfig.SingleAddConfig>(COMPONENT_CONFIG_KEY)\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()?.let {\n            return it\n        }\n        val link = intent.data?.toString().orEmpty()\n        return AddDownloadConfig.SingleAddConfig(\n            newDownload = AddDownloadCredentialsInUiProps(\n                credentials = HttpDownloadCredentials(\n                    link = link,\n                )\n            )\n        )\n    }\n\n    companion object {\n        const val COMPONENT_CONFIG_KEY = \"ComponentConfig\"\n        const val LINK_KEY = \"link\"\n        fun createIntent(\n            context: Context,\n            singleAddConfig: AddDownloadConfig.SingleAddConfig,\n            json: Json,\n        ): Intent {\n            val intent = Intent(\n                context,\n                AddSingleDownloadActivity::class.java,\n            )\n            with(json) {\n                intent.putSerializedExtra(COMPONENT_CONFIG_KEY, singleAddConfig)\n            }\n            return intent\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/single/AddSingleDownloadPage.kt",
    "content": "package com.abdownloadmanager.android.pages.add.single\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport arrow.core.Some\nimport com.abdownloadmanager.android.pages.add.shared.CategoryAddButton\nimport com.abdownloadmanager.android.pages.add.shared.CategorySelect\nimport com.abdownloadmanager.android.pages.add.shared.ExtraConfig\nimport com.abdownloadmanager.android.pages.add.shared.LocationTextField\nimport com.abdownloadmanager.android.pages.add.shared.ShowAddToQueueDialog\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetTitleWithDescription\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.resources.myStringResource\n\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.*\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.downloaderinui.add.CanAddResult\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun ResponsiveDialogScope.AddSingleDownloadPage(\n    component: AndroidAddSingleDownloadComponent,\n    onDismiss: () -> Unit,\n) {\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(\n                        myStringResource(Res.string.new_download)\n                    )\n                },\n                headerActions = {\n                    TransparentIconActionButton(\n                        MyIcons.close,\n                        contentDescription = Res.string.close.asStringSource(),\n                        onClick = onDismiss,\n                    )\n                }\n            )\n        }\n    ) {\n        val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n        Column(\n            Modifier\n                .padding(horizontal = mySpacings.mediumSpace)\n        ) {\n            Column(\n                Modifier\n                    .weight(1f, false)\n                    .verticalScroll(rememberScrollState())\n            ) {\n                val credentials by component.credentials.collectAsState()\n                fun setLink(link: String) {\n                    component.setCredentials(\n                        credentials.copy(link = Some(link))\n                    )\n                }\n\n                val showMoreInputs by component.showMoreInputs.collectAsState()\n\n                HandleEffects(component) {\n                    when (it) {\n                        is BaseAddSingleDownloadComponent.Effects.Common -> {\n                            when (it) {\n                                is BaseAddSingleDownloadComponent.Effects.Common.SuggestUrl -> {\n                                    setLink(it.link)\n                                }\n                            }\n                        }\n\n                        is BaseAddSingleDownloadComponent.Effects.Platform -> {\n                            //\n                        }\n                    }\n                }\n\n                val canAddResult by component.canAddResult.collectAsState()\n                Column {\n                    UrlTextField(\n                        text = credentials.link,\n                        setText = {\n                            setLink(it)\n                        },\n                        modifier = Modifier\n                    )\n                    AnimatedVisibility(showMoreInputs) {\n                        Column {\n                            Space()\n                            val useCategory by component.useCategory.collectAsState()\n                            Column {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    modifier = Modifier\n                                        .clickable {\n                                            component.setUseCategory(!useCategory)\n                                        }\n                                        .padding(vertical = 4.dp)\n                                ) {\n                                    CheckBox(\n                                        size = 16.dp,\n                                        value = useCategory,\n                                        onValueChange = { component.setUseCategory(it) }\n                                    )\n                                    Spacer(Modifier.width(8.dp))\n                                    Text(myStringResource(Res.string.use_category))\n                                }\n                                Space()\n                                Row {\n                                    CategorySelect(\n                                        modifier = Modifier.weight(1f),\n                                        enabled = useCategory,\n                                        categories = component.categories.collectAsState().value,\n                                        selectedCategory = component.selectedCategory.collectAsState().value,\n                                        onCategorySelected = {\n                                            component.setSelectedCategory(it)\n                                        },\n                                    )\n                                    Spacer(Modifier.width(8.dp))\n                                    CategoryAddButton(\n                                        enabled = useCategory,\n                                        modifier = Modifier,\n                                        onClick = {\n                                            component.addNewCategory()\n                                        },\n                                    )\n                                }\n                            }\n                            Spacer(Modifier.size(8.dp))\n                            LocationTextField(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = component.folder.collectAsState().value,\n                                setText = {\n                                    component.setFolder(it)\n                                },\n                                errorText = when (canAddResult) {\n                                    CanAddResult.CantWriteInThisFolder -> myStringResource(Res.string.cant_write_to_this_folder)\n                                    else -> null\n                                },\n                                lastUsedLocations = component.lastUsedLocations.collectAsState().value,\n                                onRequestRemoveSaveLocation = component::removeFromLastDownloadLocation,\n                            )\n                        }\n                    }\n                    val name by component.name.collectAsState()\n                    Spacer(Modifier.size(8.dp))\n                    NameTextField(\n                        text = name,\n                        setText = {\n                            component.setName(it)\n                        },\n                        errorText = when (canAddResult) {\n                            is CanAddResult.DownloadAlreadyExists -> {\n                                if (onDuplicateStrategy == null) {\n                                    myStringResource(Res.string.download_already_exists)\n                                } else {\n                                    null\n                                }\n                            }\n\n                            CanAddResult.InvalidFileName -> myStringResource(Res.string.invalid_file_name)\n                            else -> null\n                        }.takeIf { name.isNotEmpty() }\n                    )\n                }\n            }\n            Column {\n                Space()\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    RenderFileTypeAndSize(component)\n                    RenderResumeSupport(component, Modifier.weight(1f))\n                    ConfigActionsButtons(component)\n                }\n                Space()\n                MainActionButtons(component)\n                ShowSolutionsOnDuplicateDownload(component)\n                ShowAddToQueueDialog(\n                    isOpened = component.shouldShowAddToQueue,\n                    queueList = component.queues.collectAsState().value,\n                    onClose = { component.shouldShowAddToQueue = false },\n                    onQueueSelected = { queue, startQueue ->\n                        component.onRequestAddToQueue(queue, startQueue)\n                    },\n                    newQueueAction = component.newQueuesAction\n                )\n                ExtraConfig(\n                    isOpened = component.showMoreSettings,\n                    onDismiss = { component.showMoreSettings = false },\n                    configurables = component.configurables,\n                )\n            }\n        }\n    }\n\n}\n\n@Composable\nprivate fun Space() {\n    Spacer(Modifier.size(mySpacings.mediumSpace))\n}\n\n@Composable\nprivate fun ShowSolutionsOnDuplicateDownload(\n    component: AndroidAddSingleDownloadComponent,\n) {\n    val state = rememberResponsiveDialogState(false)\n    val isOpen = component.showSolutionsOnDuplicateDownloadUi\n    val onRequestClose = {\n        component.showSolutionsOnDuplicateDownloadUi = false\n    }\n    state.OnFullyDismissed(onRequestClose)\n    LaunchedEffect(isOpen) {\n        if (isOpen) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n    ResponsiveDialog(\n        onDismiss = state::hide,\n        state = state,\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitleWithDescription(\n                            myStringResource(Res.string.select_a_solution),\n                            myStringResource(Res.string.select_download_strategy_description),\n                        )\n                    }\n                )\n            },\n            content = {\n                Column(\n                    Modifier\n                        .padding(horizontal = 8.dp)\n                        .padding(bottom = 8.dp)\n                ) {\n                    Spacer(Modifier.height(4.dp))\n                    Divider()\n                    Spacer(Modifier.height(4.dp))\n                    Column {\n                        OnDuplicateStrategySolutionItem(\n                            isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered,\n                            title = myStringResource(Res.string.download_strategy_add_a_numbered_file),\n                            description = myStringResource(Res.string.download_strategy_add_a_numbered_file_description),\n                        ) {\n                            component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered)\n                            onRequestClose()\n                        }\n                        OnDuplicateStrategySolutionItem(\n                            isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload,\n                            title = myStringResource(Res.string.download_strategy_override_existing_file),\n                            description = myStringResource(Res.string.download_strategy_override_existing_file_description),\n                        ) {\n                            component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload)\n                            onRequestClose()\n                        }\n                        OnDuplicateStrategySolutionItem(\n                            isSelected = null,\n                            title = myStringResource(Res.string.download_strategy_update_download_link),\n                            description = myStringResource(Res.string.download_strategy_update_download_link_description),\n                        ) {\n                            component.updateDownloadCredentialsOfOriginalDownload()\n                            onRequestClose()\n                        }\n                        OnDuplicateStrategySolutionItem(\n                            isSelected = null,\n                            title = myStringResource(Res.string.download_strategy_show_downloaded_file),\n                            description = myStringResource(Res.string.download_strategy_show_downloaded_file_description),\n                        ) {\n                            component.openDownloadFileForCurrentLink()\n                            onRequestClose()\n                        }\n                    }\n                }\n            }\n        )\n\n    }\n}\n\n@Composable\nprivate fun OnDuplicateStrategySolutionItem(\n    title: String,\n    description: String,\n    isSelected: Boolean?,\n    onClick: () -> Unit,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(8.dp)\n    ) {\n        isSelected?.let {\n            CheckBox(isSelected, { onClick() }, size = 12.dp)\n        }\n        Spacer(Modifier.width(8.dp))\n        Column {\n            Text(\n                title,\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold\n            )\n            Spacer(Modifier.height(4.dp))\n            WithContentAlpha(0.7f) {\n                Text(\n                    text = description,\n                    fontSize = myTextSizes.sm,\n                    modifier = Modifier\n                )\n            }\n        }\n\n    }\n}\n\n\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier\n            .fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n\n\n@Composable\nfun RenderResumeSupport(\n    component: AndroidAddSingleDownloadComponent,\n    modifier: Modifier,\n) {\n    val fileInfo by component.linkResponseInfo.collectAsState()\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .height(16.dp)\n            .padding(horizontal = 8.dp)\n    ) {\n        val lineModifier = Modifier\n            .weight(1f)\n            .height(1.dp)\n            .background(myColors.onBackground / 10)\n        Box(lineModifier)\n        val canAddToDownloads by component.canAddToDownloads.collectAsState()\n        AnimatedVisibility(\n            visible = canAddToDownloads && fileInfo != null,\n        ) {\n            fileInfo?.let { fileInfo ->\n                if (fileInfo.resumeSupport) {\n                    val iconModifier = Modifier\n                        .padding(horizontal = 8.dp)\n                        .size(16.dp)\n                    if (fileInfo.resumeSupport) {\n                        MyIcon(\n                            icon = MyIcons.check,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.success\n                        )\n                    } else {\n                        MyIcon(\n                            icon = MyIcons.clear,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.error,\n                        )\n                    }\n                }\n            }\n        }\n        Box(lineModifier)\n\n\n    }\n}\n\n@Composable\nprivate fun MainConfigActionButton(\n    text: String,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    ActionButton(text, modifier, enabled, onClick)\n}\n\n\n@Composable\nfun ConfigActionsButtons(component: AndroidAddSingleDownloadComponent) {\n    val responseInfo by component.linkResponseInfo.collectAsState()\n    Row {\n        IconActionButton(MyIcons.refresh, Res.string.refresh.asStringSource()) {\n            component.refresh()\n        }\n        Spacer(Modifier.width(6.dp))\n        val showMoreInputs by component.showMoreInputs.collectAsState()\n        IconActionButton(\n            if (showMoreInputs) {\n                MyIcons.up\n            } else {\n                MyIcons.down\n            },\n            Res.string.more_options.asStringSource(),\n        ) {\n            component.setShowMoreInputs(!showMoreInputs)\n        }\n        Spacer(Modifier.width(6.dp))\n        IconActionButton(\n            MyIcons.settings,\n            Res.string.settings.asStringSource(),\n            indicateActive = component.showMoreSettings,\n            requiresAttention = responseInfo?.requireBasicAuth ?: false\n        ) {\n            component.showMoreSettings = true\n        }\n    }\n}\n\n@Composable\nprivate fun MainActionButtons(component: AndroidAddSingleDownloadComponent) {\n\n    val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n    val canAddResult by component.canAddResult.collectAsState()\n    if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {\n        Row {\n            val buttonModifier = Modifier.weight(1f)\n            MainConfigActionButton(\n                text = myStringResource(Res.string.show_solutions),\n                modifier = buttonModifier,\n                onClick = { component.showSolutionsOnDuplicateDownloadUi = true },\n            )\n            if (component.shouldShowOpenFile.collectAsState().value) {\n                Spacer(Modifier.width(8.dp))\n                MainConfigActionButton(\n                    text = myStringResource(Res.string.open_file),\n                    modifier = buttonModifier,\n                    onClick = { component.openExistingFile() },\n                )\n            }\n        }\n    } else {\n        val canAddToDownloads by component.canAddToDownloads.collectAsState()\n        Column {\n            if (onDuplicateStrategy != null) {\n                MainConfigActionButton(\n                    text = myStringResource(Res.string.change_solution),\n                    modifier = Modifier.fillMaxWidth(),\n                    onClick = { component.showSolutionsOnDuplicateDownloadUi = true },\n                )\n                Space()\n            }\n            val isWebPage by component.isWebPage.collectAsState()\n            if (isWebPage) {\n                Row {\n                    MainConfigActionButton(\n                        text = myStringResource(Res.string.open_in_browser),\n                        modifier = Modifier.fillMaxWidth(),\n                        enabled = canAddToDownloads,\n                        onClick = {\n                            component.onRequestOpenLinkInBrowser()\n                        },\n                    )\n                }\n            } else {\n                Row {\n                    val buttonModifier = Modifier.weight(1f)\n                    MainConfigActionButton(\n                        text = myStringResource(Res.string.add),\n                        modifier = buttonModifier,\n                        enabled = canAddToDownloads,\n                        onClick = {\n                            component.shouldShowAddToQueue = true\n                        },\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    PrimaryMainActionButton(\n                        text = myStringResource(Res.string.download),\n                        modifier = buttonModifier,\n                        enabled = canAddToDownloads,\n                        onClick = {\n                            component.onRequestDownload()\n                        },\n                    )\n                }\n\n            }\n\n        }\n    }\n}\n\n@Composable\nfun RenderFileTypeAndSize(\n    component: AndroidAddSingleDownloadComponent,\n) {\n    val isLinkLoading by component.isLinkLoading.collectAsState()\n    val fileInfo by component.linkResponseInfo.collectAsState()\n    val fileIconProvider = component.iconProvider\n    val iconModifier = Modifier.size(mySpacings.iconSize)\n    Box(\n        contentAlignment = Alignment.Center,\n    ) {\n        AnimatedContent(\n            targetState = isLinkLoading,\n            transitionSpec = {\n                fadeIn() togetherWith fadeOut()\n            }\n        ) { loading ->\n            if (loading) {\n                LoadingIndicator(iconModifier)\n            } else {\n//                val extension = getExtension(fileInfo?.fileName ?: usersSetFileName) ?: \"unknown\"\n                val downloadItem by component.downloadItem.collectAsState()\n                val icon = fileIconProvider.rememberIcon(downloadItem.name)\n                AnimatedContent(\n                    fileInfo,\n                ) { fileInfo ->\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(1f) {\n                            if (fileInfo != null) {\n                                if (fileInfo.requiresAuth) {\n                                    MyIcon(\n                                        MyIcons.lock,\n                                        null,\n                                        iconModifier,\n                                        tint = myColors.error\n                                    )\n                                }\n                                MyIcon(\n                                    icon,\n                                    null,\n                                    iconModifier\n                                )\n                                val size = component.getLengthString()\n                                Spacer(Modifier.width(8.dp))\n                                Text(\n                                    size.rememberString(),\n                                    fontSize = myTextSizes.sm,\n                                )\n                            } else {\n                                MyIcon(\n                                    icon = MyIcons.question,\n                                    contentDescription = null,\n                                    modifier = iconModifier,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    modifier: Modifier = Modifier,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        end = {\n            MyTextFieldIcon(MyIcons.paste) {\n                setText(\n                    ClipboardUtil.read()\n                        .orEmpty()\n                )\n            }\n        },\n        errorText = errorText\n    )\n}\n\n@Composable\nprivate fun NameTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.name),\n        modifier = Modifier.fillMaxWidth(),\n        errorText = errorText,\n    )\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/add/single/AndroidAddSingleDownloadComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.add.single\n\nimport com.abdownloadmanager.shared.action.createNewQueueAction\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUi\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NewQueuePageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.single.OnRequestAddSingleItem\nimport com.abdownloadmanager.shared.pages.adddownload.single.OnRequestDownloadSingleItem\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.activate\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.dismiss\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.builtins.serializer\n\nclass AndroidAddSingleDownloadComponent(\n    ctx: ComponentContext,\n    onRequestClose: () -> Unit,\n    onRequestDownload: OnRequestDownloadSingleItem,\n    onRequestAddToQueue: OnRequestAddSingleItem,\n    openExistingDownload: (Long) -> Unit,\n    updateExistingDownloadCredentials: (Long, IDownloadCredentials, DownloadJobExtraConfig?) -> Unit,\n    downloadItemOpener: DownloadItemOpener,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n    downloadSystem: DownloadSystem,\n    appSettings: BaseAppSettingsStorage,\n    iconProvider: FileIconProvider,\n    appScope: CoroutineScope,\n    appRepository: BaseAppRepository,\n    perHostSettingsManager: PerHostSettingsManager,\n    importOptions: ImportOptions,\n    id: String,\n    downloaderInUi: DownloaderInUi<IDownloadCredentials, *, *, *, *, *, *, *, *, *>,\n    initialCredentials: AddDownloadCredentialsInUiProps,\n) : BaseAddSingleDownloadComponent(\n    ctx = ctx,\n    onRequestClose = onRequestClose,\n    onRequestDownload = onRequestDownload,\n    onRequestAddToQueue = onRequestAddToQueue,\n    openExistingDownload = openExistingDownload,\n    updateExistingDownloadCredentials = updateExistingDownloadCredentials,\n    downloadItemOpener = downloadItemOpener,\n    lastSavedLocationsStorage = lastSavedLocationsStorage,\n    importOptions = importOptions,\n    id = id,\n    downloaderInUi = downloaderInUi,\n    initialCredentials = initialCredentials,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n    downloadSystem = downloadSystem,\n    appSettings = appSettings,\n    iconProvider = iconProvider,\n    appScope = appScope,\n    appRepository = appRepository,\n    perHostSettingsManager = perHostSettingsManager,\n), CategoryDialogManager, NewQueuePageManager {\n    val categoryComponentNavigation = SlotNavigation<Long>()\n    val categorySlot = childSlot(\n        source = categoryComponentNavigation,\n        childFactory = { config, ctx ->\n            CategoryComponent(\n                ctx = ctx,\n                id = config,\n                close = ::closeCategoryDialog,\n                submit = { submittedCategory ->\n                    if (submittedCategory.id < 0) {\n                        categoryManager.addCustomCategory(submittedCategory)\n                    } else {\n                        categoryManager.updateCategory(\n                            submittedCategory.id\n                        ) {\n                            submittedCategory.copy(\n                                items = it.items\n                            )\n                        }\n                    }\n                    closeCategoryDialog()\n                },\n            )\n        },\n        serializer = Long.serializer(),\n    ).subscribeAsStateFlow()\n    val newQueuesAction = createNewQueueAction(\n        appScope,\n        this,\n    )\n\n    override fun openCategoryDialog(categoryId: Long) {\n        scope.launch {\n            categoryComponentNavigation.activate(categoryId)\n        }\n    }\n\n    override fun closeCategoryDialog() {\n        scope.launch {\n            categoryComponentNavigation.dismiss()\n        }\n    }\n\n    override fun getCategoryPageManager(): CategoryDialogManager {\n        return this\n    }\n\n    private val _showMoreInputs = MutableStateFlow(false)\n    val showMoreInputs = _showMoreInputs.asStateFlow()\n    fun setShowMoreInputs(value: Boolean) {\n        _showMoreInputs.value = value\n    }\n\n    private val _showAddQueue = MutableStateFlow(false)\n    val showAddQueue = _showAddQueue.asStateFlow()\n    fun setShowAddQueue(value: Boolean) {\n        _showAddQueue.value = value\n    }\n\n    val isWebPage = downloadChecker\n        .responseInfo\n        .mapStateFlow { it?.isWebPage ?: false }\n\n    fun createQueueWithName(name: String) {\n        scope.launch { queueManager.addQueue(name) }\n        setShowAddQueue(false)\n    }\n\n    override fun closeNewQueueDialog() {\n        setShowAddQueue(false)\n    }\n\n    override fun openNewQueueDialog() {\n        setShowAddQueue(true)\n    }\n\n    fun onRequestOpenLinkInBrowser() {\n        sendEffect(\n            Effects.OpenInBrowser(\n                downloadChecker.credentials.value.link\n            )\n        )\n    }\n\n    sealed interface Effects : BaseAddSingleDownloadComponent.Effects.Platform {\n        data class OpenInBrowser(val link: String) : Effects\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/batchdownload/AndroidBatchDownloadComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.batchdownload\n\nimport com.abdownloadmanager.shared.pages.batchdownload.BaseBatchDownloadComponent\nimport com.arkivanov.decompose.ComponentContext\n\nclass AndroidBatchDownloadComponent(\n    ctx: ComponentContext,\n    onClose: () -> Unit,\n    importLinks: (List<String>) -> Unit,\n) : BaseBatchDownloadComponent(\n    ctx = ctx,\n    onClose = onClose,\n    importLinks = importLinks\n) {\n    sealed interface Effects : BaseBatchDownloadComponent.Effects.PlatformEffects {\n        // nothing for now\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/batchdownload/BatchDownloadPage.kt",
    "content": "package com.abdownloadmanager.android.pages.batchdownload\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.*\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.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.batchdownload.BatchDownloadValidationResult\nimport com.abdownloadmanager.shared.pages.batchdownload.WildcardLength\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun BatchDownloadSheet(\n    component: AndroidBatchDownloadComponent?,\n    onDismiss: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    state.OnFullyDismissed(onDismiss)\n    LaunchedEffect(component) {\n        if (component == null) {\n            state.hide()\n        } else {\n            state.show()\n        }\n    }\n    ResponsiveDialog(state, state::hide) {\n        component?.let {\n            BatchDownloadPage(component, state::hide)\n        }\n    }\n}\n\n@Composable\nprivate fun ResponsiveDialogScope.BatchDownloadPage(\n    component: AndroidBatchDownloadComponent,\n    onDismiss: () -> Unit,\n) {\n    val link by component.link.collectAsState()\n    val setLink = component::setLink\n    val start by component.start.collectAsState()\n    val setStart = component::setStart\n    val end by component.end.collectAsState()\n    val setEnd = component::setEnd\n    val scrollState = rememberScrollState()\n    val validationResult by component.validationResult.collectAsState()\n    val linkFocusRequester = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        linkFocusRequester.requestFocus()\n    }\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(\n                        myStringResource(Res.string.batch_download)\n                    )\n                }\n            )\n        }\n    ) {\n        Column(\n            Modifier\n                .padding(16.dp)\n        ) {\n            VerticalScrollableContent(\n                scrollState,\n                modifier = Modifier.weight(1f, false),\n            ) {\n                Column(\n                    modifier = Modifier\n                        .verticalScroll(scrollState)\n                ) {\n                    LabeledContent(\n                        label = {\n                            Text(myStringResource(Res.string.batch_download_link_help))\n                        },\n                        content = {\n                            MyTextFieldWithIcons(\n                                text = link,\n                                onTextChange = setLink,\n                                placeholder = \"https://example.com/photo-*.png\",\n                                modifier = Modifier\n                                    .focusRequester(linkFocusRequester)\n                                    .fillMaxWidth(),\n                                start = {\n                                    MyTextFieldIcon(MyIcons.link)\n                                },\n                                end = {\n                                    MyTextFieldIcon(MyIcons.paste, onClick = {\n                                        val v = ClipboardUtil.read()\n                                        if (v != null) {\n                                            setLink(v)\n                                        }\n                                    })\n                                },\n                                errorText = when (val v = validationResult) {\n                                    BatchDownloadValidationResult.URLInvalid -> {\n                                        myStringResource(Res.string.invalid_url)\n                                    }\n\n                                    is BatchDownloadValidationResult.MaxRangeExceed -> myStringResource(\n                                        Res.string.list_is_too_large_maximum_n_items_allowed,\n                                        Res.string.list_is_too_large_maximum_n_items_allowed_createArgs(\n                                            count = v.allowed.toString()\n                                        )\n                                    )\n\n                                    BatchDownloadValidationResult.Others -> null\n                                    BatchDownloadValidationResult.Ok -> null\n                                }\n                            )\n                        }\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    LabeledContent(\n                        label = {\n                            Text(myStringResource(Res.string.enter_range))\n                        },\n                        content = {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically\n                            ) {\n                                MyTextFieldWithIcons(\n                                    text = start,\n                                    onTextChange = setStart,\n                                    placeholder = \"\",\n                                    modifier = Modifier.width(90.dp),\n                                    start = {\n                                        Text(\n                                            \"${myStringResource(Res.string.range_from)}:\",\n                                            Modifier.padding(horizontal = 8.dp)\n                                        )\n                                    }\n                                )\n                                Spacer(Modifier.width(8.dp))\n                                Text(\"...\")\n                                Spacer(Modifier.width(8.dp))\n\n                                MyTextFieldWithIcons(\n                                    text = end,\n                                    onTextChange = setEnd,\n                                    placeholder = \"\",\n                                    modifier = Modifier.width(90.dp),\n                                    start = {\n                                        Text(\n                                            \"${myStringResource(Res.string.range_to)}:\",\n                                            Modifier.padding(horizontal = 8.dp)\n                                        )\n                                    }\n                                )\n                            }\n                        }\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    LabeledContent(\n                        label = {\n                            Text(myStringResource(Res.string.batch_download_wildcard_length))\n                        },\n                        content = {\n                            WildcardLengthUi(\n                                component.wildcardLength.collectAsState().value,\n                                component::setWildCardLength\n                            )\n                        }\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        val lineModifier = Modifier\n                            .height(1.dp)\n                            .padding(horizontal = 5.dp)\n                            .background(LocalContentColor.current.copy(0.05f))\n\n                        Spacer(\n                            Modifier\n                                .padding(vertical = 4.dp)\n                                .fillMaxWidth()\n                                .then(lineModifier)\n                        )\n                    }\n                    Spacer(Modifier.height(8.dp))\n                    LabeledContent(\n                        label = {\n                            Text(myStringResource(Res.string.first_link))\n                        },\n                        content = {\n                            LinkPreview(component.startLinkResult.collectAsState().value)\n                        }\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    LabeledContent(\n                        label = {\n                            Text(myStringResource(Res.string.last_link))\n                        },\n                        content = {\n                            LinkPreview(component.endLinkResult.collectAsState().value)\n                        }\n                    )\n                }\n            }\n            Spacer(Modifier.height(8.dp))\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n            ) {\n                val buttonModifier = Modifier.weight(1f)\n                ActionButton(\n                    myStringResource(Res.string.close),\n                    onClick = onDismiss,\n                    modifier = buttonModifier,\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(\n                    text = myStringResource(Res.string.ok),\n                    enabled = component.canConfirm.collectAsState().value,\n                    onClick = component::confirm,\n                    modifier = buttonModifier,\n                )\n            }\n        }\n    }\n\n}\n\n@Composable\nfun LinkPreview(link: String) {\n    Text(\n        link,\n        Modifier\n            .fillMaxWidth()\n            .clip(myShapes.defaultRounded)\n            .border(1.dp, myColors.onSurface / 0.1f, myShapes.defaultRounded)\n//            .background(myColors.surface)\n            .padding(vertical = 4.dp, horizontal = 6.dp)\n    )\n}\n\nenum class WildcardSelect(\n    val text: StringSource,\n) {\n    Auto(Res.string.auto.asStringSource()),\n    Unspecified(Res.string.unspecified.asStringSource()),\n    Custom(Res.string.custom.asStringSource());\n\n    companion object {\n        fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect {\n            return when (wildcardLength) {\n                WildcardLength.Auto -> Auto\n                is WildcardLength.Custom -> Custom\n                WildcardLength.Unspecified -> Unspecified\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun WildcardLengthUi(\n    wildcardLength: WildcardLength,\n    onChangeWildcardLength: (WildcardLength) -> Unit,\n) {\n    var customLength by remember {\n        mutableIntStateOf(2)\n    }\n    FlowRow(\n        itemVerticalAlignment = Alignment.CenterVertically\n    ) {\n        Multiselect(\n            selections = WildcardSelect.entries,\n            selectedItem = WildcardSelect.fromWildcardLength(wildcardLength),\n            onSelectionChange = {\n                onChangeWildcardLength(\n                    when (it) {\n                        WildcardSelect.Auto -> WildcardLength.Auto\n                        WildcardSelect.Unspecified -> WildcardLength.Unspecified\n                        WildcardSelect.Custom -> WildcardLength.Custom(customLength)\n                    }\n                )\n            },\n            render = {\n                Text(it.text.rememberString())\n            }\n        )\n        AnimatedVisibility(wildcardLength is WildcardLength.Custom) {\n            Row {\n                Spacer(Modifier.width(8.dp))\n                IntTextField(\n                    value = customLength,\n                    onValueChange = {\n                        customLength = it\n                        onChangeWildcardLength(\n                            WildcardLength.Custom(it)\n                        )\n                    },\n                    range = 1..10,\n                    keyboardOptions = KeyboardOptions.Default,\n                    modifier = Modifier.width(96.dp)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LabeledContent(\n    label: @Composable () -> Unit,\n    content: @Composable () -> Unit,\n) {\n    Column {\n        label()\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/BrowserActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Bundle\nimport com.abdownloadmanager.android.util.AndroidIntentUtils\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.arkivanov.decompose.defaultComponentContext\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport androidx.core.net.toUri\nimport com.abdownloadmanager.android.storage.BrowserBookmarksStorage\n\nclass BrowserActivity : ABDMActivity() {\n    private val browserBookmarksStorage: BrowserBookmarksStorage by inject()\n    private val json: Json by inject()\n    val component by lazy {\n        BrowserComponent(\n            componentContext = defaultComponentContext(),\n            context = applicationContext,\n            json = json,\n            browserBookmarksStorage = browserBookmarksStorage,\n        )\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setABDMContent {\n            HandleEffects(component) {\n                when (it) {\n                    is BrowserComponent.Effects.StartActivity -> {\n                        startActivity(it.intent)\n                    }\n\n                    is BrowserComponent.Effects.ShareText -> {\n                        AndroidIntentUtils.shareText(this, it.text)\n                    }\n                }\n            }\n            BrowserPage(component)\n        }\n    }\n\n    override fun handleIntent(intent: Intent) {\n        if (intent.action == Intent.ACTION_VIEW) {\n            val url = intent.data?.toString()\n            if (url != null && HttpUrlUtils.isValidUrl(url)) {\n                component.newTab(url)\n            }\n        }\n    }\n\n    companion object {\n        fun createIntent(\n            context: Context,\n            url: String? = null,\n        ): Intent {\n            return Intent(context, BrowserActivity::class.java).apply {\n                action = Intent.ACTION_VIEW\n                data = url?.toUri()\n            }\n        }\n\n        object Launcher : KoinComponent {\n            private val context: Context by inject()\n            private val browserLauncherActivityAliasName by lazy {\n                \"com.abdownloadmanager.browser.BrowserIconInLauncher\"\n            }\n\n            fun setEnabled(\n                isEnabled: Boolean,\n            ) {\n                val newState = if (isEnabled) {\n                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED\n                } else {\n                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED\n                }\n                context.packageManager.setComponentEnabledSetting(\n                    ComponentName(context, browserLauncherActivityAliasName),\n                    newState,\n                    PackageManager.DONT_KILL_APP\n                )\n            }\n\n            fun isEnabled(): Boolean {\n                return context.packageManager.getComponentEnabledSetting(\n                    ComponentName(context, browserLauncherActivityAliasName),\n                ) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/BrowserComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.compose.runtime.Stable\nimport com.abdownloadmanager.android.pages.add.multiple.AddMultiDownloadActivity\nimport com.abdownloadmanager.android.pages.add.single.AddSingleDownloadActivity\nimport com.abdownloadmanager.android.pages.browser.bookmark.EditBookmarkState\nimport com.abdownloadmanager.android.storage.BrowserBookmark\nimport com.abdownloadmanager.android.storage.BrowserBookmarksStorage\nimport com.abdownloadmanager.android.ui.widget.WebContent\nimport com.abdownloadmanager.android.ui.widget.WebViewState\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.json.Json\nimport java.util.UUID\nimport kotlin.text.orEmpty\n\nclass BrowserComponent(\n    componentContext: ComponentContext,\n    private val context: Context,\n    private val json: Json,\n    private val browserBookmarksStorage: BrowserBookmarksStorage,\n) : BaseComponent(\n    componentContext,\n), ContainsEffects<BrowserComponent.Effects> by supportEffects() {\n    val downloadInterceptor = DownloadInterceptor(\n        scope, {\n            val intent = when (it.size) {\n                0 -> null\n                1 -> AddSingleDownloadActivity.createIntent(\n                    context,\n                    AddDownloadConfig.SingleAddConfig(it.first()),\n                    json,\n                )\n\n                else -> AddMultiDownloadActivity.createIntent(\n                    context,\n                    AddDownloadConfig.MultipleAddConfig(\n                        it\n                    ),\n                    json\n                )\n            }\n            intent?.let { intent ->\n                sendEffect(Effects.StartActivity(intent))\n            }\n        }\n    )\n    private val currentSearchEngine = MutableStateFlow(\n        SearchEngines.DuckDuckGo\n    )\n    val tabs = MutableStateFlow(\n        ABDMTabs.createDefault()\n    )\n    val bookmarks = browserBookmarksStorage.bookmarksFlow\n    private val _mainMenu: MutableStateFlow<MenuItem.SubMenu?> = MutableStateFlow(null)\n    val mainMenu = _mainMenu.asStateFlow()\n    fun openMainMenu() {\n        val tab = tabs.value.activeTab\n        val url = tab?.tabState?.lastLoadedUrl\n        val title = tab?.tabState?.pageTitle\n        _mainMenu.value = MenuItem.SubMenu(\n            title = title?.asStringSource() ?: Res.string.menu.asStringSource(),\n            items = buildMenu {\n                +createNewTabAction()\n                separator()\n                +createShowBookmarksAction()\n                if (url != null) {\n                    if (isBookmarked(url)) {\n                        +createRemoveFromBookmarkAction(url)\n                    } else {\n                        +createAddToBookmarkAction(url, title)\n                    }\n                }\n                tab?.let {\n                    separator()\n                    +createCloseTabAction(it)\n                }\n            }\n        )\n    }\n\n    fun closeMainMenu() {\n        _mainMenu.value = null\n    }\n\n    fun newTab(\n        url: String? = ABDMBrowserTab.blankPage,\n        switch: Boolean = true,\n        id: String = UUID.randomUUID().toString(),\n        openedBy: ABDMBrowserTabId? = null,\n    ): ABDMBrowserTab {\n        val browserTab = ABDMBrowserTab(\n            tabId = id,\n            tabState = WebViewState(WebContent.fromNullableUrl(url)),\n        )\n        tabs.update { currentTabState ->\n            val newTabPosition = openedBy?.let {\n                // index of openedBy + 1 or null if not found\n                currentTabState.tabs\n                    .indexOfFirst { it.tabId == openedBy }\n                    .takeIf { it >= 0 }\n                    ?.plus(1)\n            } ?: currentTabState.tabs.size\n            val newItems = buildList {\n                addAll(currentTabState.tabs)\n                add(newTabPosition, browserTab)\n            }\n            val newIndex = if (switch) {\n                newTabPosition\n            } else {\n                currentTabState.activeTabIndex\n            }\n            currentTabState.copy(\n                tabs = newItems,\n                activeTabIndex = newIndex\n            )\n        }\n        return browserTab\n    }\n\n    fun closeTab(tabId: ABDMBrowserTabId) {\n        tabs.update {\n            val newItems = it.tabs.filterNot { it.tabId == tabId }\n            it.copy(\n                tabs = newItems,\n                activeTabIndex = runCatching {\n                    it.activeTabIndex.coerceIn(newItems.indices)\n                }.getOrElse { -1 },\n            )\n        }\n    }\n\n    fun addToBookmarks(\n        bookmark: BrowserBookmark,\n        replaceWith: BrowserBookmark?,\n    ) {\n        browserBookmarksStorage.bookmarksFlow.update { currentBookmarks ->\n            if (replaceWith != null) {\n                currentBookmarks.map { item ->\n                    item.ifThen(item == replaceWith) {\n                        bookmark\n                    }\n                }\n            } else {\n                currentBookmarks.plus(bookmark)\n            }\n        }\n    }\n\n    private val _showBookmarkList: MutableStateFlow<Boolean> = MutableStateFlow(false)\n    val showBookmarkList = _showBookmarkList.asStateFlow()\n    fun setShowBookmarkList(show: Boolean) {\n        _showBookmarkList.value = show\n    }\n\n    private val _editBookmarkState = MutableStateFlow<EditBookmarkState?>(null)\n    val editBookmarkState = _editBookmarkState.asStateFlow()\n    fun promptAddBookmark(\n        bookmark: BrowserBookmark,\n    ) {\n        _editBookmarkState.value = EditBookmarkState(\n            initialValue = bookmark,\n            editMode = false,\n        )\n    }\n\n    fun promptEditBookmark(\n        bookmark: BrowserBookmark\n    ) {\n        _editBookmarkState.value = EditBookmarkState(\n            initialValue = bookmark,\n            editMode = true,\n        )\n    }\n\n    fun dismissEditBookmark() {\n        _editBookmarkState.value = null\n    }\n\n    fun removeBookmark(url: String) {\n        browserBookmarksStorage.bookmarksFlow.update {\n            it.filterNot { bookmark -> bookmark.url == url }\n        }\n    }\n\n    fun clearBookmarks() {\n        browserBookmarksStorage.bookmarksFlow.value = emptyList()\n    }\n\n    fun isBookmarked(url: String): Boolean {\n        return browserBookmarksStorage.bookmarksFlow.value.find {\n            it.url == url\n        } != null\n    }\n\n    fun switchTab(tabId: ABDMBrowserTabId) {\n        tabs.update {\n            val tabIndex = it.tabs.indexOfFirst { it.tabId == tabId }\n            val newIndex = if (tabIndex < 0) {\n                it.activeTabIndex\n            } else {\n                tabIndex\n            }\n            it.copy(\n                activeTabIndex = newIndex,\n            )\n        }\n    }\n\n    private val websiteAndTLD by lazy {\n        \"\"\"^[\\w.-]+\\.[a-zA-Z]{2,}(:\\d{1,5})?$\"\"\".toRegex()\n    }\n\n    fun createNewUrlFor(urlOrSearch: String): String {\n        val value = urlOrSearch.trim()\n        if (value.contains(' ')) {\n            return createSearchEngineUrl(value)\n        }\n        if (HttpUrlUtils.isValidUrl(value)) {\n            return value\n        }\n        if (websiteAndTLD.matches(value)) {\n            val withHttpScheme = \"https://$value\"\n            if (HttpUrlUtils.isValidUrl(withHttpScheme)) {\n                return withHttpScheme\n            }\n        }\n        return createSearchEngineUrl(value)\n    }\n\n    private fun createSearchEngineUrl(searchText: String): String {\n        return currentSearchEngine.value.createSearchUrl(searchText)\n    }\n\n    val contextMenu: MutableStateFlow<MenuItem.SubMenu?> = MutableStateFlow(null)\n\n    fun closeContextMenu() {\n        contextMenu.value = null\n    }\n\n    fun onLinkSelected(\n        link: String,\n        tab: ABDMBrowserTab,\n    ) {\n        contextMenu.value = MenuItem.SubMenu(\n            title = link.asStringSource(),\n            items = buildMenu {\n                +simpleAction(\n                    Res.string.browser_open_in_new_tab.asStringSource(),\n                    MyIcons.file,\n                ) {\n                    newTab(\n                        url = link,\n                        switch = true,\n                        openedBy = tab.tabId,\n                    )\n                }\n                +simpleAction(\n                    Res.string.browser_open_in_new_background_tab.asStringSource(),\n                    MyIcons.file,\n                ) {\n                    newTab(\n                        url = link,\n                        switch = false,\n                        openedBy = tab.tabId,\n                    )\n                }\n                +simpleAction(\n                    Res.string.share.asStringSource(),\n                    MyIcons.share,\n                ) {\n                    sendEffect(Effects.ShareText(link))\n                }\n                +simpleAction(\n                    Res.string.copy.asStringSource(),\n                    MyIcons.copy,\n                ) {\n                    ClipboardUtil.copy(link)\n                }\n                +simpleAction(\n                    Res.string.download.asStringSource(),\n                    MyIcons.download,\n                ) {\n                    downloadInterceptor.onDownloadStart(\n                        url = link,\n                        userAgent = null,\n                        page = null,\n                        tab = tab,\n                    )\n                }\n                if (isBookmarked(link)) {\n                    +createRemoveFromBookmarkAction(link)\n                } else {\n                    +createAddToBookmarkAction(link, null)\n                }\n            }\n        )\n    }\n\n    fun createNewTabAction(): AnAction {\n        return simpleAction(\n            title = Res.string.browser_new_tab.asStringSource(),\n            icon = MyIcons.file,\n        ) {\n            newTab(\n                url = null,\n                switch = true,\n            )\n        }\n    }\n\n    fun createAddToBookmarkAction(\n        url: String,\n        title: String?,\n    ): AnAction {\n        return simpleAction(\n            Res.string.browser_add_to_bookmarks.asStringSource(),\n            MyIcons.add,\n        ) {\n            promptAddBookmark(\n                BrowserBookmark(\n                    url = url,\n                    title = title.orEmpty(),\n                )\n            )\n        }\n    }\n\n    fun createRemoveFromBookmarkAction(\n        url: String,\n    ): AnAction {\n        return simpleAction(\n            Res.string.browser_remove_from_bookmarks.asStringSource(),\n            MyIcons.remove,\n        ) {\n            removeBookmark(url)\n        }\n    }\n\n    fun createCloseTabAction(tab: ABDMBrowserTab): AnAction {\n        return simpleAction(\n            title = Res.string.browser_close_tab.asStringSource(),\n            icon = MyIcons.close,\n        ) {\n            closeTab(tab.tabId)\n        }\n    }\n\n    fun createShowBookmarksAction(): AnAction {\n        return simpleAction(\n            title = Res.string.browser_bookmarks.asStringSource(),\n            icon = MyIcons.hearth,\n        ) {\n            setShowBookmarkList(true)\n        }\n    }\n\n    sealed interface Effects {\n        data class StartActivity(\n            val intent: Intent\n        ) : Effects\n\n        data class ShareText(\n            val text: String,\n        ) : Effects\n    }\n}\n\ntypealias ABDMBrowserTabId = String\n\n@Stable\ndata class ABDMBrowserTab(\n    val tabId: ABDMBrowserTabId,\n    val tabState: WebViewState,\n) {\n    companion object {\n        fun createDefaultTab(\n            page: String = blankPage\n        ) = ABDMBrowserTab(\n            tabId = UUID.randomUUID().toString(),\n            tabState = WebViewState(WebContent.Url(page)),\n        )\n\n        val blankPage = \"about:blank\"\n    }\n}\n\n@Stable\ndata class ABDMTabs(\n    val tabs: List<ABDMBrowserTab>,\n    val activeTabIndex: Int,\n) {\n    val tabsSize = tabs.size\n    val activeTab get() = if (activeTabIndex == -1) null else tabs[activeTabIndex]\n\n    companion object {\n        fun createDefault(): ABDMTabs = ABDMTabs(\n            listOf(),\n            -1,\n        )\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/BrowserUi.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.Image\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.collectIsFocusedAsState\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.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.sizeIn\nimport androidx.compose.foundation.layout.statusBarsPadding\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.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.semantics.Role\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.unit.dp\nimport com.abdownloadmanager.android.pages.browser.bookmark.BookmarkList\nimport com.abdownloadmanager.android.pages.browser.bookmark.EditBookmarkSheet\nimport com.abdownloadmanager.android.storage.BrowserBookmark\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSheet\nimport com.abdownloadmanager.android.ui.page.PageFooter\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.widget.LoadingState\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\nfun BrowserPage(\n    browserComponent: BrowserComponent,\n) {\n    val scope = rememberCoroutineScope()\n    val viewRegistry = remember {\n        WebViewRegistry(scope, browserComponent)\n    }\n    DisposableEffect(viewRegistry) {\n        onDispose {\n            viewRegistry.disposeAll()\n        }\n    }\n    val tabs by browserComponent.tabs.collectAsState()\n    val tab = tabs.activeTab\n    val tabWebViewHolder = remember(tab?.tabId) {\n        tab?.let {\n            viewRegistry.getWebViewHolder(it)\n        }\n    }\n    BackHandler(tabs.tabsSize > 1) {\n        tab?.let {\n            browserComponent.closeTab(tab.tabId)\n        }\n    }\n    BackHandler(tabWebViewHolder?.navigator?.canGoBack ?: false) {\n        tabWebViewHolder?.webView?.goBack()\n    }\n    LaunchedEffect(tabs) {\n        viewRegistry.onTabsUpdated(tabs)\n    }\n    PageUi(\n        header = {\n            PageHeader(\n                leadingIcon = {\n                    MyIcon(\n                        MyIcons.earth,\n                        null,\n                        Modifier.size(mySpacings.iconSize)\n                    )\n                },\n                headerTitle = {\n                    PageTitle(\n                        myStringResource(Res.string.browser)\n                    )\n                },\n                modifier = Modifier\n                    .statusBarsPadding()\n                    .padding(horizontal = mySpacings.largeSpace),\n            )\n        },\n        footer = {\n            PageFooter {\n                Column(\n                    Modifier\n                        .background(myColors.surface)\n                        .navigationBarsPadding()\n                        .imePadding()\n                ) {\n                    Spacer(\n                        Modifier\n                            .height(1.dp)\n                            .fillMaxWidth()\n                            .background(myColors.onSurface / 0.1f)\n                    )\n                    tab?.tabState?.loadingState?.let {\n                        if (it is LoadingState.Loading) {\n                            Box(\n                                Modifier.fillMaxWidth(),\n                                contentAlignment = Alignment.CenterStart\n                            ) {\n                                Box(\n                                    Modifier\n                                        .height(1.dp)\n                                        .fillMaxWidth(it.progress)\n                                        .background(myColors.info),\n                                )\n                            }\n                        }\n                    }\n                    WithContentColor(myColors.onSurface) {\n                        AddressBar(\n                            browserComponent = browserComponent,\n                            currentWebViewHolder = tabWebViewHolder,\n                            tabs = tabs,\n                            modifier = Modifier,\n                        )\n                    }\n                }\n            }\n        }\n    ) {\n        if (tabWebViewHolder != null) {\n            ABDMWebView(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(myColors.background)\n                    .padding(it.paddingValues),\n                webViewHolder = tabWebViewHolder,\n            )\n        } else {\n            EmptyPage(\n                Modifier\n                    .fillMaxSize()\n                    .background(myColors.background)\n                    .padding(it.paddingValues),\n                onRequestOpenUrlFromClipboard = {\n                    ClipboardUtil.read()?.let {\n                        browserComponent.newTab(\n                            browserComponent.createNewUrlFor(it)\n                        )\n                    }\n                },\n                onRequestOpenBookmarks = {\n                    browserComponent.setShowBookmarkList(true)\n                }\n            )\n        }\n    }\n    RenderMenuInSheet(\n        browserComponent.contextMenu.collectAsState().value,\n        browserComponent::closeContextMenu\n    )\n    BookmarkList(\n        visible = browserComponent.showBookmarkList.collectAsState().value,\n        onDismissRequest = {\n            browserComponent.setShowBookmarkList(false)\n        },\n        onRemoveBookmarkRequest = {\n            browserComponent.removeBookmark(it.url)\n        },\n        onBookmarkClick = {\n            browserComponent.setShowBookmarkList(false)\n            val newLink = browserComponent.createNewUrlFor(it.url)\n            tabWebViewHolder\n                ?.navigator\n                ?.loadUrl(newLink)\n                ?: browserComponent.newTab(newLink)\n        },\n        bookmarks = browserComponent.bookmarks.collectAsState().value,\n        onRequestEditBookmark = browserComponent::promptEditBookmark,\n        onRequestNewBookmark = {\n            browserComponent.promptAddBookmark((BrowserBookmark(\"\", \"\")))\n        },\n    )\n    val editBookmarkState by browserComponent.editBookmarkState.collectAsState()\n    editBookmarkState?.let { s ->\n        EditBookmarkSheet(\n            state = s,\n            onSave = {\n                browserComponent.addToBookmarks(\n                    it,\n                    if (s.editMode) {\n                        s.initialValue\n                    } else {\n                        null\n                    },\n                )\n                browserComponent.dismissEditBookmark()\n            },\n            onCancel = {\n                browserComponent.dismissEditBookmark()\n            }\n        )\n    }\n    RenderMenuInSheet(\n        browserComponent.mainMenu.collectAsState().value,\n        browserComponent::closeMainMenu,\n    )\n}\n\n@Composable\nfun EmptyPage(\n    modifier: Modifier,\n    onRequestOpenUrlFromClipboard: () -> Unit,\n    onRequestOpenBookmarks: () -> Unit,\n) {\n    Box(modifier) {\n        Column(\n            modifier = Modifier.align(Alignment.Center),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Text(\n                myStringResource(Res.string.browser_no_tab_open),\n                maxLines = 1,\n            )\n            Spacer(Modifier.height(mySpacings.largeSpace))\n            ActionButton(\n                text = myStringResource(Res.string.browser_paste_and_go),\n                onClick = onRequestOpenUrlFromClipboard,\n                start = {\n                    MyIcon(\n                        MyIcons.paste,\n                        null,\n                        Modifier.size(mySpacings.iconSize)\n                    )\n                    Spacer(Modifier.width(mySpacings.mediumSpace))\n                }\n            )\n            Spacer(Modifier.height(mySpacings.largeSpace))\n            ActionButton(\n                text = myStringResource(Res.string.browser_bookmarks),\n                onClick = onRequestOpenBookmarks,\n                start = {\n                    MyIcon(\n                        MyIcons.hearth,\n                        null,\n                        Modifier.size(mySpacings.iconSize)\n                    )\n                    Spacer(Modifier.width(mySpacings.mediumSpace))\n                }\n            )\n        }\n    }\n}\n\n@Composable\nfun AddressBar(\n    browserComponent: BrowserComponent,\n    currentWebViewHolder: WebViewHolder?,\n    tabs: ABDMTabs,\n    modifier: Modifier,\n) {\n    val webViewState = currentWebViewHolder?.tab?.tabState\n    val navigator = currentWebViewHolder?.navigator\n    val canGoBack = navigator?.canGoBack ?: false\n    val canGoForward = navigator?.canGoForward ?: false\n    val currentURL = webViewState?.lastLoadedUrl\n    val currentTitle = webViewState?.pageTitle\n    var isTabListVisible by remember { mutableStateOf(false) }\n\n    Column(\n        modifier = modifier\n            .padding(horizontal = mySpacings.mediumSpace)\n            .padding(vertical = mySpacings.mediumSpace)\n    ) {\n        AddressField(\n            currentPageURL = currentURL,\n            currentPageTitle = currentTitle,\n            currentPageIcon = remember(webViewState?.pageIcon) {\n                webViewState?.pageIcon?.asImageBitmap()\n            },\n            onNewPageRequested = {\n                it?.let { text ->\n                    val newLink = browserComponent.createNewUrlFor(text)\n                    navigator\n                        ?.loadUrl(newLink)\n                        ?: browserComponent.newTab(newLink)\n                }\n            }\n        )\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        Row {\n            TransparentIconActionButton(\n                enabled = canGoBack,\n                icon = MyIcons.back,\n                contentDescription = Res.string.back.asStringSource()\n            ) {\n                navigator?.navigateBack()\n            }\n            TransparentIconActionButton(\n                enabled = canGoForward,\n                icon = MyIcons.next,\n                contentDescription = Res.string.next.asStringSource()\n            ) {\n                navigator?.navigateForward()\n            }\n            Spacer(Modifier.width(16.dp))\n            webViewState?.let {\n                TransparentIconActionButton(\n                    icon = if (webViewState.isLoading) {\n                        MyIcons.close\n                    } else {\n                        MyIcons.refresh\n                    },\n                    contentDescription = Res.string.next.asStringSource()\n                ) {\n                    if (webViewState.isLoading) {\n                        navigator?.stopLoading()\n                    } else {\n                        navigator?.reload()\n                    }\n                }\n            }\n            Spacer(Modifier.weight(1f))\n            val shape = myShapes.defaultRounded\n            Box(\n                Modifier\n                    .sizeIn(mySpacings.thumbSize, mySpacings.thumbSize)\n                    .clip(shape)\n                    .border(\n                        1.dp, myColors.onBackground / 0.1f, shape\n                    )\n                    .clickable(\n                        role = Role.Button,\n                        onClick = {\n                            isTabListVisible = !isTabListVisible\n                        },\n                    )\n                    .padding(vertical = 8.dp, horizontal = 16.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                Text(\n                    text = \"${tabs.tabsSize}\",\n                    maxLines = 1,\n                    fontWeight = FontWeight.Bold,\n                )\n            }\n            TransparentIconActionButton(\n                MyIcons.menu,\n                contentDescription = Res.string.menu.asStringSource()\n            ) {\n                browserComponent.openMainMenu()\n            }\n        }\n    }\n    TabList(\n        visible = isTabListVisible,\n        onDismissRequest = {\n            isTabListVisible = false\n        },\n        onCloseTabRequest = {\n            browserComponent.closeTab(it.tabId)\n        },\n        onTabClick = {\n            isTabListVisible = false\n            browserComponent.switchTab(it.tabId)\n        },\n        onRequestNewTab = { requestedUrl ->\n            isTabListVisible = false\n            browserComponent.newTab(\n                requestedUrl?.let {\n                    browserComponent.createNewUrlFor(it)\n                }\n            )\n        },\n        tabs = tabs,\n        currentTabId = currentWebViewHolder?.tab?.tabId,\n    )\n}\n\n@Composable\nprivate fun TabList(\n    visible: Boolean,\n    onDismissRequest: () -> Unit,\n    tabs: ABDMTabs,\n    onRequestNewTab: (String?) -> Unit,\n    onTabClick: (ABDMBrowserTab) -> Unit,\n    onCloseTabRequest: (ABDMBrowserTab) -> Unit,\n    currentTabId: String?,\n) {\n    val responsiveState = rememberResponsiveDialogState(visible)\n    LaunchedEffect(visible) {\n        if (visible) {\n            responsiveState.show()\n        } else {\n            responsiveState.hide()\n        }\n    }\n    ResponsiveDialog(\n        state = responsiveState,\n        onDismiss = onDismissRequest\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\n                            myStringResource(Res.string.browser_tabs),\n                        )\n                    },\n                    headerActions = {\n                        TransparentIconActionButton(\n                            MyIcons.paste,\n                            Res.string.paste.asStringSource(),\n                        ) {\n                            onRequestNewTab(\n                                ClipboardUtil.read()\n                            )\n                        }\n                        TransparentIconActionButton(\n                            MyIcons.add,\n                            Res.string.add.asStringSource(),\n                        ) {\n                            onRequestNewTab(null)\n                        }\n                        TransparentIconActionButton(\n                            MyIcons.close,\n                            Res.string.close.asStringSource(),\n                        ) {\n                            onDismissRequest()\n                        }\n                    }\n                )\n            }\n        ) {\n            LazyColumn {\n                items(tabs.tabs) { tabItem ->\n                    val isSelected = tabItem.tabId == currentTabId\n                    Row(\n                        modifier = Modifier\n                            .heightIn(mySpacings.thumbSize)\n                            .ifThen(isSelected) {\n                                background(myColors.onBackground / 0.1f)\n                            }\n                            .clickable {\n                                onTabClick(tabItem)\n                            }\n                            .padding(vertical = 8.dp, horizontal = 16.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        val websiteIconBitmap = remember(tabItem.tabState.pageIcon) {\n                            tabItem.tabState.pageIcon?.asImageBitmap()\n                        }\n                        val modifier = Modifier.size(24.dp)\n                        if (websiteIconBitmap != null) {\n                            Image(\n                                bitmap = websiteIconBitmap,\n                                contentDescription = null,\n                                modifier = modifier,\n                            )\n                        } else {\n                            MyIcon(\n                                MyIcons.earth,\n                                contentDescription = null,\n                                modifier = modifier,\n                            )\n                        }\n                        Spacer(Modifier.width(16.dp))\n                        Text(\n                            text = tabItem.tabState.let {\n                                it.pageTitle ?: it.lastLoadedUrl\n                            }.orEmpty(),\n                            modifier = Modifier\n                                .weight(1f),\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                        WithContentAlpha(0.5f) {\n                            TransparentIconActionButton(\n                                MyIcons.close,\n                                Res.string.close.asStringSource(),\n                            ) {\n                                onCloseTabRequest(tabItem)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nfun AddressField(\n    currentPageIcon: ImageBitmap?,\n    currentPageURL: String?,\n    currentPageTitle: String?,\n    onNewPageRequested: (String?) -> Unit,\n) {\n    val title = currentPageTitle ?: currentPageURL ?: \"Blank\"\n    val url = currentPageURL ?: \"\"\n    val isSecure = remember(url) {\n        url.startsWith(\"https://\")\n    }\n    var isEditing by remember {\n        mutableStateOf(false)\n    }\n    BackHandler(enabled = isEditing) {\n        isEditing = false\n    }\n    val textFieldInteractionSource = remember { MutableInteractionSource() }\n    val isFocused by textFieldInteractionSource.collectIsFocusedAsState()\n    LaunchedEffect(isFocused) {\n        isEditing = isFocused\n    }\n    if (isEditing) {\n        val fr = remember { FocusRequester() }\n        LaunchedEffect(Unit) {\n            fr.requestFocus()\n        }\n        var editingText by remember {\n            mutableStateOf(url)\n        }\n        MyTextField(\n            text = editingText,\n            onTextChange = {\n                editingText = it\n            },\n            interactionSource = textFieldInteractionSource,\n            placeholder = \"URL\",\n            modifier = Modifier\n                .fillMaxWidth()\n                .focusRequester(fr),\n            keyboardOptions = KeyboardOptions(\n                keyboardType = KeyboardType.Uri,\n                imeAction = ImeAction.Go,\n            ),\n            keyboardActions = KeyboardActions(\n                onGo = {\n                    isEditing = false\n                    onNewPageRequested(editingText)\n                },\n            ),\n            end = {\n                MyIcon(\n                    MyIcons.paste,\n                    contentDescription = myStringResource(Res.string.paste),\n                    modifier = Modifier\n                        .clickable {\n                            ClipboardUtil.read()?.let {\n                                editingText = it\n                            }\n                        }\n                        .fillMaxHeight()\n                        .padding(horizontal = 10.dp),\n                )\n                MyIcon(\n                    MyIcons.clear,\n                    contentDescription = null,\n                    modifier = Modifier\n                        .clickable {\n                            if (editingText.isNotEmpty()) {\n                                editingText = \"\"\n                            } else {\n                                isEditing = false\n                            }\n                        }\n                        .fillMaxHeight()\n                        .padding(horizontal = 10.dp),\n                )\n            },\n        )\n    } else {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .heightIn(mySpacings.thumbSize)\n                .clip(myShapes.defaultRounded)\n                .background(myColors.onSurface / 0.05f)\n                .clickable {\n                    isEditing = true\n                }\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            if (currentPageIcon != null) {\n                Image(\n                    bitmap = currentPageIcon,\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                )\n                Spacer(Modifier.width(16.dp))\n            }\n            Text(\n                title,\n                modifier = Modifier\n                    .weight(1f)\n                    .wrapContentHeight(),\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            if (isSecure) {\n                Spacer(Modifier.width(8.dp))\n                MyIcon(\n                    MyIcons.lock,\n                    \"HTTPS\",\n                    modifier = Modifier.size(24.dp),\n                    tint = myColors.success,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/DownloadInterceptor.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport android.webkit.CookieManager\nimport com.abdownloadmanager.android.ui.widget.WebViewState\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\ntypealias ABDMWebRequestId = String\n\ndata class ABDMWebRequest(\n    val url: String,\n    val headers: Map<String, String>,\n    val page: String?,\n) {\n    val id: ABDMWebRequestId = url\n}\n\ninterface RequestInterceptor {\n    fun interceptRequest(request: ABDMWebRequest)\n}\n\nclass DownloadInterceptor(\n    private val scope: CoroutineScope,\n    private val onNewDownload: (newDownloads: List<AddDownloadCredentialsInUiProps>) -> Unit,\n) : RequestInterceptor {\n    private val requests = mutableMapOf<String, ABDMWebRequest>()\n\n    fun onDownloadStart(\n        url: String?,\n        userAgent: String?,\n        page: String?,\n        tab: ABDMBrowserTab,\n    ) {\n        if (url == null) {\n            return\n        }\n        if (!HttpUrlUtils.isValidUrl(url)) {\n            return\n        }\n        val webRequest = getWebRequestOrDefault(\n            url = url,\n            userAgent = userAgent,\n            page = page,\n            webViewState = tab.tabState,\n        )\n        onNewDownload(\n            listOf(\n                AddDownloadCredentialsInUiProps(\n                    HttpDownloadCredentials(\n                        link = webRequest.url,\n                        headers = webRequest.headers,\n                        downloadPage = webRequest.page,\n                    ),\n                    AddDownloadCredentialsInUiProps.Configs()\n                )\n            )\n        )\n    }\n\n    override fun interceptRequest(\n        request: ABDMWebRequest,\n    ) {\n        addToHeaders(request)\n    }\n\n    private fun addToHeaders(request: ABDMWebRequest) {\n        requests[request.id] = request\n        scope.launch {\n            delay(REMOVE_REQUESTS_DELAY)\n            requests.remove(request.id)\n        }\n    }\n\n    private fun getWebRequestOrDefault(\n        url: String,\n        userAgent: String?,\n        page: String?,\n        webViewState: WebViewState,\n    ): ABDMWebRequest {\n        var request = requests[url]\n        if (request == null) {\n            request = ABDMWebRequest(\n                url = url,\n                headers = emptyMap(),\n                page = getPageUrl(webViewState) ?: page,\n            )\n        }\n        return request\n            .withUserAgent(userAgent)\n            .withCookieManagerCookies()\n    }\n\n    private fun ABDMWebRequest.withUserAgent(userAgent: String?): ABDMWebRequest {\n        val request = this\n        if (userAgent == null) {\n            return request\n        }\n        val userAgentKey = \"User-Agent\"\n        if (request.headers.containsKey(userAgentKey)) {\n            return request\n        }\n        return request.copy(\n            headers = request.headers.plus(\n                userAgentKey to userAgent\n            )\n        )\n    }\n\n    private fun ABDMWebRequest.withCookieManagerCookies(): ABDMWebRequest {\n        val request = this\n        val cookieFromCookieManager =\n            CookieManager.getInstance().getCookie(url)?.takeIf { it.isNotBlank() } ?: return request\n        val cookieKey = \"Cookie\"\n        val currentCookie = request.headers[cookieKey]?.takeIf { it.isNotBlank() }\n        return request.copy(\n            headers = request.headers.plus(\n                cookieKey to if (currentCookie != null) {\n                    \"$currentCookie; $cookieFromCookieManager\"\n                } else {\n                    cookieFromCookieManager\n                }\n            )\n        )\n    }\n\n    private fun getPageUrl(state: WebViewState): String? {\n        return state.lastLoadedUrl\n    }\n\n    companion object {\n        private const val REMOVE_REQUESTS_DELAY = 20_000L\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/SearchEngines.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport java.net.URLEncoder\nimport java.nio.charset.StandardCharsets\n\nsealed class SearchEngines(\n    val baseUrl: String,\n    val query: String,\n    val home: String = baseUrl,\n) {\n    fun createSearchUrl(textToSearch: String): String {\n        return buildSearchUrl(baseUrl, query, textToSearch)\n    }\n\n    data object DuckDuckGo : SearchEngines(\n        baseUrl = \"https://duckduckgo.com/\",\n        query = \"q\",\n    )\n\n    data object Google : SearchEngines(\n        baseUrl = \"https://www.google.com/search\",\n        query = \"q\",\n        home = \"https://www.google.com\",\n    )\n\n    data object Bing : SearchEngines(\n        baseUrl = \"https://www.bing.com/search\",\n        query = \"q\",\n    )\n\n    data object Brave : SearchEngines(\n        baseUrl = \"https://search.brave.com/search\",\n        query = \"q\",\n        home = \"https://search.brave.com\",\n    )\n\n    companion object {\n        private fun buildSearchUrl(\n            baseUrl: String,\n            queryParam: String,\n            query: String\n        ): String {\n            val encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8.toString())\n            return \"$baseUrl?$queryParam=$encodedQuery\"\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/WebView.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.key\nimport androidx.compose.ui.Modifier\nimport com.abdownloadmanager.android.ui.widget.WebView\n\n\n@Composable\nfun ABDMWebView(\n    modifier: Modifier = Modifier,\n    webViewHolder: WebViewHolder,\n) {\n    val tab = webViewHolder.tab\n    key(tab.tabId) {\n        val wState = tab.tabState\n        val navigator = webViewHolder.navigator\n        WebView(\n            state = wState,\n            modifier = modifier,\n            captureBackPresses = false,\n            navigator = navigator,\n            client = webViewHolder.client,\n            chromeClient = webViewHolder.chromeClient,\n            onDispose = {\n                webViewHolder.deactivate()\n            },\n            factory = {\n                webViewHolder.activate(it)\n            },\n        )\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/WebViewHolder.kt",
    "content": "package com.abdownloadmanager.android.pages.browser\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Message\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebResourceResponse\nimport android.webkit.WebSettings\nimport android.webkit.WebView\nimport com.abdownloadmanager.android.ui.widget.AccompanistWebChromeClient\nimport com.abdownloadmanager.android.ui.widget.AccompanistWebViewClient\nimport com.abdownloadmanager.android.ui.widget.WebContent\nimport com.abdownloadmanager.android.ui.widget.WebViewNavigator\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.util.UUID\n\nclass WebViewRegistry(\n    private val scope: CoroutineScope,\n    private val browserComponent: BrowserComponent,\n) : WebViewFactory {\n    val viewHolders = mutableMapOf<ABDMBrowserTabId, WebViewHolder>()\n    fun onTabsUpdated(\n        webViewStates: ABDMTabs,\n    ) {\n        val webViewStateIds = webViewStates.tabs.map { it.tabId }.toSet()\n        for (viewHolderKey in viewHolders.keys.toList()) {\n            if (viewHolderKey !in webViewStateIds) {\n                removeViewHolder(viewHolderKey)\n            }\n        }\n    }\n\n    fun getWebViewHolder(\n        tab: ABDMBrowserTab\n    ): WebViewHolder {\n        return viewHolders.getOrPut(tab.tabId, {\n            WebViewHolder(\n                tab = tab,\n                navigator = WebViewNavigator(scope),\n                webView = null,\n                client = ABDMWebViewClient(browserComponent.downloadInterceptor, scope),\n                chromeClient = ABDMChromeClient(browserComponent, ::getWebViewHolder),\n                webViewFactory = this,\n            )\n        })\n    }\n\n    fun removeViewHolder(id: String) {\n        viewHolders.remove(id)?.release()\n    }\n\n    fun disposeAll() {\n        viewHolders.forEach { (_, holder) ->\n            holder.release()\n        }\n        viewHolders.clear()\n    }\n\n    override fun createWebView(\n        context: Context,\n        tab: ABDMBrowserTab,\n    ): ABDMWebView {\n        return ABDMWebView(context).apply {\n            val webView = this\n            webView.settings.javaScriptEnabled = true\n            webView.settings.domStorageEnabled = true\n            webView.settings.setSupportZoom(true)\n            webView.settings.builtInZoomControls = false\n            webView.settings.setSupportMultipleWindows(true)\n            webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE\n            webView.isLongClickable = true\n            webView.setOnLongClickListener {\n                val hit = webView.hitTestResult\n\n                if (hit.type == WebView.HitTestResult.SRC_ANCHOR_TYPE ||\n                    hit.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE\n                ) {\n                    val url = hit.extra ?: return@setOnLongClickListener false\n\n                    browserComponent.onLinkSelected(\n                        url,\n                        tab,\n                    )\n                    true\n                } else {\n                    false\n                }\n            }\n            webView.setDownloadListener { url, userAgent, _, _, _ ->\n                scope.launch(Dispatchers.Main) {\n                    if (!webView.canGoBack() && webView.originalUrl == null) {\n                        browserComponent.closeTab(tab.tabId)\n                    }\n                    browserComponent.downloadInterceptor.onDownloadStart(\n                        url,\n                        userAgent,\n                        webView.originalUrl ?: webView.openedBy,\n                        tab,\n                    )\n                }\n            }\n            webView.tabId = tab.tabId\n        }\n    }\n\n}\n\ndata class WebViewHolder(\n    val tab: ABDMBrowserTab,\n    var webView: ABDMWebView? = null,\n    val navigator: WebViewNavigator,\n    val client: ABDMWebViewClient,\n    val chromeClient: ABDMChromeClient,\n    private val webViewFactory: WebViewFactory,\n) {\n\n    fun activate(context: Context): ABDMWebView {\n        return if (webView != null) {\n            (webView!!).also {\n                it.onResume()\n            }\n        } else {\n            webViewFactory.createWebView(context, tab).also { webView = it }\n        }\n    }\n\n    fun deactivate() {\n        webView?.onPause()\n        // prevent reloading after activated again\n        tab.tabState.content = WebContent.NavigatorOnly\n    }\n\n    fun release() {\n        webView?.onPause()\n        webView?.destroy()\n        webView = null\n    }\n}\n\ninterface WebViewFactory {\n    fun createWebView(\n        context: Context,\n        tab: ABDMBrowserTab,\n    ): ABDMWebView\n}\n\nclass ABDMWebViewClient(\n    private val requestInterceptor: DownloadInterceptor,\n    private val scope: CoroutineScope,\n) : AccompanistWebViewClient() {\n    override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {\n        if (request != null) {\n            scope.launch(Dispatchers.Main) {\n                requestInterceptor.interceptRequest(\n                    ABDMWebRequest(\n                        url = request.url.toString(),\n                        headers = request.requestHeaders,\n                        page = view?.originalUrl ?: view?.url\n                    )\n                )\n            }\n        }\n        return super.shouldInterceptRequest(view, request)\n    }\n\n    override fun shouldOverrideUrlLoading(\n        view: WebView,\n        request: WebResourceRequest\n    ): Boolean {\n\n        val url = request.url.toString()\n\n        // Let WebView load normal web pages\n        if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {\n            return false\n        }\n\n        // Handle intent:// URIs\n        if (url.startsWith(\"intent://\")) {\n            try {\n                val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)\n                val pm = view.context.packageManager\n\n                if (intent.resolveActivity(pm) != null) {\n                    view.context.startActivity(intent)\n                } else {\n                    intent.getStringExtra(\"browser_fallback_url\")?.let {\n                        view.loadUrl(it)\n                    }\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            return true\n        }\n\n        // Handle ALL other schemes (deep links)\n        try {\n            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n            val pm = view.context.packageManager\n\n            if (intent.resolveActivity(pm) != null) {\n                view.context.startActivity(intent)\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n\n        return true\n    }\n}\n\nclass ABDMChromeClient(\n    private val browserComponent: BrowserComponent,\n    private val createWebViewHolder: (tab: ABDMBrowserTab) -> WebViewHolder,\n) : AccompanistWebChromeClient() {\n    override fun onCreateWindow(\n        view: WebView?,\n        isDialog: Boolean,\n        isUserGesture: Boolean,\n        resultMsg: Message?\n    ): Boolean {\n        if (view == null) return false\n        val transport = (resultMsg?.obj as? WebView.WebViewTransport) ?: return false\n        val newTab = browserComponent.newTab(\n            id = UUID.randomUUID().toString(),\n            switch = true,\n            url = null,\n            openedBy = (view as? ABDMWebView)?.tabId\n        )\n        val newWebView = createWebViewHolder(newTab).activate(view.context)\n        newWebView.openedBy = view.originalUrl ?: view.url\n        transport.webView = newWebView\n        resultMsg.sendToTarget()\n        return true\n    }\n}\n\nclass ABDMWebView(\n    context: Context,\n) : WebView(context) {\n    var openedBy: String? = null\n    var tabId: String? = null\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/bookmark/Bookmarks.kt",
    "content": "package com.abdownloadmanager.android.pages.browser.bookmark\n\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.heightIn\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.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.storage.BrowserBookmark\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun BookmarkList(\n    visible: Boolean,\n    onDismissRequest: () -> Unit,\n    bookmarks: List<BrowserBookmark>,\n    onRequestNewBookmark: () -> Unit,\n    onRequestEditBookmark: (BrowserBookmark) -> Unit,\n    onBookmarkClick: (BrowserBookmark) -> Unit,\n    onRemoveBookmarkRequest: (BrowserBookmark) -> Unit,\n) {\n    val responsiveState = rememberResponsiveDialogState(visible)\n    LaunchedEffect(visible) {\n        if (visible) {\n            responsiveState.show()\n        } else {\n            responsiveState.hide()\n        }\n    }\n    ResponsiveDialog(\n        state = responsiveState,\n        onDismiss = onDismissRequest,\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\n                            myStringResource(Res.string.browser_bookmarks),\n                        )\n                    },\n                    headerActions = {\n                        TransparentIconActionButton(\n                            MyIcons.add,\n                            Res.string.add.asStringSource(),\n                        ) {\n                            onRequestNewBookmark()\n                        }\n                        TransparentIconActionButton(\n                            MyIcons.close,\n                            Res.string.close.asStringSource(),\n                        ) {\n                            onDismissRequest()\n                        }\n                    }\n                )\n            }\n        ) {\n            LazyColumn {\n                items(bookmarks) { bookmark ->\n                    Row(\n                        modifier = Modifier\n                            .heightIn(mySpacings.thumbSize)\n                            .combinedClickable(\n                                onLongClick = { onRequestEditBookmark(bookmark) },\n                                onClick = { onBookmarkClick(bookmark) }\n                            )\n                            .padding(vertical = 8.dp, horizontal = 16.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        val modifier = Modifier.size(24.dp)\n                        MyIcon(\n                            MyIcons.earth,\n                            contentDescription = null,\n                            modifier = modifier,\n                        )\n                        Spacer(Modifier.width(16.dp))\n                        Column(\n                            modifier = Modifier\n                                .weight(1f),\n                        ) {\n                            Text(\n                                text = bookmark.title,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                            Text(\n                                text = bookmark.url,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                                fontSize = myTextSizes.sm,\n                                color = LocalContentColor.current / 0.75f\n                            )\n                        }\n\n                        WithContentAlpha(0.5f) {\n                            TransparentIconActionButton(\n                                MyIcons.remove,\n                                Res.string.remove.asStringSource(),\n                            ) {\n                                onRemoveBookmarkRequest(bookmark)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/browser/bookmark/EditBookmark.kt",
    "content": "package com.abdownloadmanager.android.pages.browser.bookmark\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport com.abdownloadmanager.android.storage.BrowserBookmark\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Immutable\ndata class EditBookmarkState(\n    val initialValue: BrowserBookmark,\n    val editMode: Boolean = false,\n)\n\n@Composable\nfun EditBookmarkSheet(\n    state: EditBookmarkState,\n    onSave: (BrowserBookmark) -> Unit,\n    onCancel: () -> Unit,\n) {\n    val editMode = state.editMode\n    val initialValue = state.initialValue\n    val sheetTitle = if (editMode) Res.string.browser_edit_bookmark else Res.string.browser_add_bookmark\n    SheetInput(\n        title = sheetTitle.asStringSource(),\n        validate = {\n            it.title.isNotEmpty() && it.url.isNotEmpty()\n        },\n        isOpened = true,\n        initialValue = { initialValue },\n        onDismiss = onCancel,\n        onConfirm = onSave,\n        inputContent = { inputParams ->\n            var title by remember(state) {\n                mutableStateOf(state.initialValue.title)\n            }\n            var url by remember(state) {\n                mutableStateOf(state.initialValue.url)\n            }\n            LaunchedEffect(url, title) {\n                inputParams.setEditingValue(\n                    BrowserBookmark(\n                        url = url, title = title,\n                    )\n                )\n            }\n            Column(\n                modifier = inputParams.modifier,\n            ) {\n                val (urlFR, titleFR) = remember { FocusRequester.createRefs() }\n                LaunchedEffect(Unit) {\n                    when {\n                        url.isBlank() -> {\n                            urlFR.requestFocus()\n                        }\n\n                        title.isBlank() -> {\n                            titleFR.requestFocus()\n                        }\n                    }\n                }\n                val textFieldModifier = Modifier\n                MyTextField(\n                    text = url,\n                    onTextChange = {\n                        url = it\n                    },\n                    modifier = textFieldModifier\n                        .focusRequester(urlFR),\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Uri,\n                        imeAction = ImeAction.Next\n                    ),\n                    keyboardActions = KeyboardActions.Default,\n                    placeholder = \"URL\",\n                )\n                Spacer(modifier = Modifier.height(mySpacings.mediumSpace))\n                MyTextField(\n                    text = title,\n                    onTextChange = {\n                        title = it\n                    },\n                    modifier = textFieldModifier\n                        .focusRequester(titleFR),\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Text,\n                        imeAction = ImeAction.Done\n                    ),\n                    keyboardActions = inputParams.keyboardActions,\n                    placeholder = myStringResource(Res.string.name),\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/category/CategorySheet.kt",
    "content": "package com.abdownloadmanager.android.pages.category\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.directorypicker.rememberAndroidDirectoryPickerLauncher\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.asStringSource\nimport java.io.File\n@Composable\nfun CategorySheet(\n    categoryComponent: CategoryComponent?,\n    onDismiss: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(\n        categoryComponent\n    ) {\n        if (categoryComponent != null) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    state.OnFullyDismissed(onDismiss)\n    ResponsiveDialog(state, onDismiss = state::hide) {\n        categoryComponent?.let {\n            CategorySheetUi(it)\n        }\n    }\n}\n\n@Composable\nprivate fun ResponsiveDialogScope.CategorySheetUi(categoryComponent: CategoryComponent) {\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(\n                        myStringResource(\n                            if (categoryComponent.isEditMode) {\n                                Res.string.edit_category\n                            } else {\n                                Res.string.add_category\n                            }\n                        )\n                    )\n                }\n            )\n        }\n    ) {\n        Column(\n            modifier = Modifier\n                .padding(horizontal = mySpacings.mediumSpace)\n                .padding(vertical = mySpacings.mediumSpace)\n        ) {\n            Column(\n                Modifier\n                    .weight(1f, false)\n                    .verticalScroll(rememberScrollState())\n            ) {\n                Row {\n                    CategoryIcon(\n                        iconSource = categoryComponent.icon.collectAsState().value,\n                        onChange = categoryComponent::setIcon\n                    )\n                    Spacer(Modifier.width(16.dp))\n                    CategoryName(\n                        modifier = Modifier.weight(1f),\n                        name = categoryComponent.name.collectAsState().value,\n                        onNameChanged = categoryComponent::setName\n                    )\n                }\n                Spacer(Modifier.height(12.dp))\n                CategoryAutoTypes(\n                    types = categoryComponent.types.collectAsState().value,\n                    onTypesChanged = categoryComponent::setTypes,\n                    enabled = categoryComponent.typesEnabled.collectAsState().value,\n                    setEnabled = categoryComponent::setTypesEnabled\n                )\n                Spacer(Modifier.height(12.dp))\n                CategoryAutoUrls(\n                    urlPatterns = categoryComponent.urlPatterns.collectAsState().value,\n                    onUrlPatternChanged = categoryComponent::setUrlPatterns,\n                    enabled = categoryComponent.urlPatternsEnabled.collectAsState().value,\n                    setEnabled = categoryComponent::setUrlPatternsEnabled\n                )\n                Spacer(Modifier.height(12.dp))\n                CategoryDefaultPath(\n                    path = categoryComponent.path.collectAsState().value,\n                    onPathChanged = categoryComponent::setPath,\n                    defaultDownloadLocation = categoryComponent.defaultDownloadLocation.collectAsState().value,\n                    checked = categoryComponent.usePath.collectAsState().value,\n                    setChecked = categoryComponent::setUsePath\n                )\n            }\n            Spacer(Modifier.height(12.dp))\n            Row(Modifier\n                .fillMaxWidth()\n                .wrapContentWidth(Alignment.End)) {\n                ActionButton(\n                    myStringResource(\n                        when (categoryComponent.isEditMode) {\n                            true -> Res.string.change\n                            false -> Res.string.add\n                        }\n                    ),\n                    enabled = categoryComponent.canSubmit.collectAsState().value,\n                    onClick = {\n                        categoryComponent.submit()\n                    },\n                    modifier = Modifier.weight(1f)\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(\n                    myStringResource(Res.string.cancel),\n                    onClick = {\n                        categoryComponent.close()\n                    },\n                    modifier = Modifier.weight(1f)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun CategoryDefaultPath(\n    defaultDownloadLocation: String,\n    path: String,\n    onPathChanged: (String) -> Unit,\n    checked: Boolean,\n    setChecked: (Boolean) -> Unit,\n) {\n    val initialDirectory = remember(path, defaultDownloadLocation) {\n        path\n            .takeIf { it.isNotBlank() }\n            ?.let {\n                runCatching {\n                    File(path).canonicalPath\n                }.getOrNull()\n            } ?: defaultDownloadLocation\n    }\n    val downloadFolderPickerLauncher = rememberAndroidDirectoryPickerLauncher(\n        title = Res.string.category_download_location.asStringSource(),\n        initialDirectory = initialDirectory,\n    ) { directory ->\n        directory?.let(onPathChanged)\n    }\n\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_download_location),\n        helpText = myStringResource(Res.string.category_download_location_description),\n        enabled = checked,\n        setEnabled = setChecked,\n    ) {\n        MyTextFieldWithIcons(\n            text = path,\n            onTextChange = onPathChanged,\n            modifier = Modifier.fillMaxWidth(),\n            enabled = checked,\n            placeholder = \"\",\n            errorText = null,\n            end = {\n                MyTextFieldIcon(\n                    MyIcons.folder,\n                    enabled = checked,\n                ) {\n                    downloadFolderPickerLauncher.launch()\n                }\n            }\n        )\n    }\n}\n\n@Composable\nfun CategoryAutoTypes(\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    types: String,\n    onTypesChanged: (String) -> Unit,\n) {\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_file_types),\n        helpText = myStringResource(Res.string.category_file_types_description),\n        enabled = enabled,\n        setEnabled = setEnabled,\n    ) {\n        MyTextFieldWithIcons(\n            text = types,\n            onTextChange = onTypesChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"ext1 ext2 ext3\",\n            enabled = enabled,\n            singleLine = false,\n        )\n    }\n}\n\n@Composable\nfun CategoryAutoUrls(\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    urlPatterns: String,\n    onUrlPatternChanged: (String) -> Unit,\n) {\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_url_patterns),\n        helpText = myStringResource(Res.string.category_url_patterns_description),\n        enabled = enabled,\n        setEnabled = setEnabled\n    ) {\n        MyTextFieldWithIcons(\n            text = urlPatterns,\n            onTextChange = onUrlPatternChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"dl.example.com/pics example.com/*/path\",\n            enabled = enabled,\n            singleLine = false,\n        )\n    }\n}\n\n@Composable\nfun CategoryName(\n    name: String,\n    onNameChanged: (String) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    WithLabel(\n        myStringResource(Res.string.category_name),\n        modifier,\n    ) {\n        MyTextFieldWithIcons(\n            text = name,\n            onTextChange = onNameChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"Something...\",\n        )\n    }\n}\n\n@Composable\nprivate fun WithLabel(\n    label: String,\n    modifier: Modifier = Modifier,\n    helpText: String? = null,\n    content: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(label)\n            helpText?.let {\n                Spacer(Modifier.width(8.dp))\n                Help(helpText)\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n\n@Composable\nprivate fun OptionalWithLabel(\n    label: String,\n    modifier: Modifier = Modifier,\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    helpText: String? = null,\n    content: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Row(\n                modifier = Modifier.clickable {\n                    setEnabled(!enabled)\n                },\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CheckBox(enabled, setEnabled, size = 16.dp)\n                Spacer(Modifier.width(8.dp))\n                Text(label)\n            }\n            helpText?.let {\n                Spacer(Modifier.width(8.dp))\n                Help(helpText)\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n\n@Composable\nprivate fun CategoryIcon(\n    iconSource: IconSource?,\n    onChange: (IconSource) -> Unit,\n) {\n    var showIconPicker by remember {\n        mutableStateOf(false)\n    }\n    WithLabel(\n        myStringResource(Res.string.icon)\n    ) {\n        RenderIcon(\n            icon = iconSource,\n            requiresAttention = iconSource == null,\n            onClick = {\n                showIconPicker = !showIconPicker\n            }\n        )\n        if (showIconPicker) {\n            IconPick(\n                selectedIcon = iconSource,\n                icons = listOf(\n                    MyIcons.pictureFile,\n                    MyIcons.musicFile,\n                    MyIcons.zipFile,\n                    MyIcons.videoFile,\n                    MyIcons.applicationFile,\n                    MyIcons.documentFile,\n                    MyIcons.otherFile,\n\n                    MyIcons.file,\n                    MyIcons.folder,\n\n                    MyIcons.browserIntegration,\n                    MyIcons.appearance,\n\n                    MyIcons.settings,\n                    MyIcons.search,\n                    MyIcons.info,\n                    MyIcons.check,\n                    MyIcons.link,\n                    MyIcons.download,\n                    MyIcons.speaker,\n                    MyIcons.group,\n                    MyIcons.activeCount,\n                    MyIcons.speed,\n                    MyIcons.resume,\n                    MyIcons.pause,\n                    MyIcons.stop,\n                    MyIcons.queue,\n                    MyIcons.remove,\n                    MyIcons.clear,\n                    MyIcons.add,\n                    MyIcons.paste,\n                    MyIcons.copy,\n                    MyIcons.refresh,\n                    MyIcons.share,\n                    MyIcons.lock,\n                    MyIcons.question,\n                    MyIcons.verticalDirection,\n                    MyIcons.downloadEngine,\n                    MyIcons.network,\n                    MyIcons.externalLink,\n                ),\n                onSelected = {\n                    onChange(it)\n                    showIconPicker = false\n                },\n                onCancel = {\n                    showIconPicker = false\n                }\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderIcon(\n    icon: IconSource?,\n    indicateActive: Boolean = false,\n    requiresAttention: Boolean = false,\n    onClick: () -> Unit,\n) {\n    val shape = RoundedCornerShape(10.dp)\n    Box(\n        Modifier\n            .border(\n                1.dp,\n                myColors.onBackground / 10,\n                shape\n            )\n            .sizeIn(mySpacings.thumbSize, mySpacings.thumbSize)\n            .ifThen(indicateActive || requiresAttention) {\n                border(\n                    1.dp,\n                    myColors.primary / if (indicateActive) 1f else alphaFlicker(),\n                    shape\n                )\n            }\n            .clip(shape)\n            .background(myColors.surface)\n            .clickable {\n                onClick()\n            }\n            .padding(6.dp),\n        contentAlignment = Alignment.Center,\n    ) {\n        val modifier = Modifier\n            .size(20.dp)\n        if (icon != null) {\n            MyIcon(\n                icon,\n                null,\n                modifier,\n            )\n        } else {\n            Spacer(modifier)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/category/NewCategory.kt",
    "content": ""
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/checksum/AndroidFileChecksumComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.checksum\n\nimport com.abdownloadmanager.shared.pages.checksum.BaseFileChecksumComponent\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.serialization.Serializable\nimport java.util.UUID\n\nclass AndroidFileChecksumComponent(\n    ctx: ComponentContext,\n    id: String,\n    itemIds: List<Long>,\n    closeComponent: () -> Unit,\n    downloadSystem: DownloadSystem,\n    val iconProvider: FileIconProvider,\n) : BaseFileChecksumComponent(\n    ctx = ctx,\n    id = id,\n    itemIds = itemIds,\n    closeComponent = closeComponent,\n    downloadSystem = downloadSystem,\n) {\n    @Serializable\n    data class Config(\n        val id: String = UUID.randomUUID().toString(),\n        override val itemIds: List<Long>,\n    ) : BaseFileChecksumComponent.Config\n\n    sealed interface Effects : BaseFileChecksumComponent.Effects.Platform {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/checksum/FileChecksumPage.kt",
    "content": "package com.abdownloadmanager.android.pages.checksum\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\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.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.checksum.ChecksumStatus\nimport com.abdownloadmanager.shared.pages.checksum.DownloadItemWithChecksum\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Help\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.FileChecksumAlgorithm\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberDotLoading\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.flow.MutableStateFlow\n\n@Composable\nfun FileChecksumPage(component: AndroidFileChecksumComponent) {\n    val pageTitle = myStringResource(Res.string.file_checksum_page)\n    val horizontalPadding = mySpacings.largeSpace\n    Column(\n        Modifier\n            .background(myColors.background)\n            .statusBarsPadding()\n    ) {\n        PageTitle(pageTitle)\n        ItemsToBeChecked(\n            Modifier\n                .fillMaxWidth()\n                .weight(1f)\n                .padding(horizontal = horizontalPadding),\n            component,\n        )\n        Actions(\n            Modifier,\n            component,\n        )\n    }\n}\n\n@Composable\nfun ItemsToBeChecked(\n    modifier: Modifier = Modifier,\n    component: AndroidFileChecksumComponent,\n) {\n    val dividerColor = myColors.onBackground / 0.5f\n    val collectAsState by component.state.collectAsState()\n    var currentEditingItem: DownloadItemWithChecksum? by remember { mutableStateOf(null) }\n    LazyColumn(\n        modifier = modifier\n    ) {\n        itemsIndexed(collectAsState.items) { index, item ->\n            val isFirstItem = index == 0\n            RenderDownloadItemWithChecksum(\n                item = item,\n                iconProvider = component.iconProvider,\n                onRequestUpdateChecksum = {\n                    currentEditingItem = item\n                },\n                modifier = Modifier.ifThen(!isFirstItem) {\n                    drawBehind {\n                        drawLine(\n                            brush = Brush.horizontalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    dividerColor,\n                                    Color.Transparent,\n                                )\n                            ),\n                            start = Offset.Zero,\n                            end = Offset(size.width, 0f)\n                        )\n                    }\n                }\n            )\n        }\n    }\n    currentEditingItem?.let { item ->\n        FileChecksumTableCellRenderers.ChecksumEditSheet(\n            item = item,\n            onCloseRequest = {\n                currentEditingItem = null\n            },\n            onRequestSaveNewChecksum = {\n                component.updateChecksum(item.downloadItem.id, it)\n                currentEditingItem = null\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier,\n    component: AndroidFileChecksumComponent,\n) {\n    val uiState by component.state.collectAsState()\n    Column(\n        modifier\n            .fillMaxWidth()\n            .background(myColors.surface)\n            .navigationBarsPadding()\n    ) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onSurface / 0.15f)\n        )\n        Column(\n            Modifier\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 16.dp),\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm)\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Help(\n                        myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm_help)\n                    )\n                }\n                Spacer(Modifier.size(8.dp))\n                RenderSpinner(\n                    modifier = Modifier,\n                    possibleValues = FileChecksumAlgorithm.all(),\n                    value = uiState.defaultAlgorithm,\n                    enabled = !uiState.isChecking,\n                    onSelect = {\n                        component.onAlgorithmChange(it)\n                    },\n                    render = {\n                        Text(it.algorithm)\n                    })\n            }\n            Spacer(Modifier.height(8.dp))\n            Row {\n                ActionButton(\n                    myStringResource(Res.string.start),\n                    onClick = component::onRequestStartCheck,\n                    enabled = !uiState.isChecking,\n                    modifier = Modifier.weight(1f)\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(\n                    myStringResource(Res.string.close),\n                    onClick = component::onRequestClose,\n                    modifier = Modifier.weight(1f)\n                )\n            }\n        }\n    }\n\n}\n\n\nprivate data object FileChecksumTableCellRenderers {\n    @Composable\n    fun RenderStatus(item: DownloadItemWithChecksum) {\n        when (val status = item.checksumStatus) {\n            is ChecksumStatus.Checking -> {\n                RenderCheckingStatus(status.percent)\n            }\n\n            ChecksumStatus.Error.DownloadNotFinished -> {\n                RenderErrorStatus(myStringResource(Res.string.download_not_finished))\n            }\n\n            is ChecksumStatus.Error.Exception -> {\n                RenderErrorStatus(status.t.localizedMessage ?: status.t::class.simpleName.orEmpty())\n            }\n\n            ChecksumStatus.Error.FileNotFound -> {\n                RenderErrorStatus(myStringResource(Res.string.file_not_found))\n            }\n\n            is ChecksumStatus.Finished -> {\n                RenderFinishedStatus(\n                    status = status,\n                )\n            }\n\n            ChecksumStatus.Waiting -> {\n                RenderWaitingStatus()\n            }\n        }\n    }\n\n    @Composable\n    fun RenderCalculatedChecksum(item: DownloadItemWithChecksum) {\n        val calculatedChecksum = item.calculatedChecksum\n        ColumnKeyValue(\n            modifier = Modifier,\n            keyContent = {\n                RenderKey {\n                    Text(myStringResource(Res.string.calculated_checksum))\n                }\n            },\n            valueContent = {\n                if (calculatedChecksum != null) {\n                    SimpleText(calculatedChecksum)\n                } else if (item.isProcessing) {\n                    //shimmer\n                    ShimmerEffect(\n                        centerColor = myColors.onBackground / 0.4f,\n                        surroundingColor = myColors.onBackground / 0.1f,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clip(myShapes.defaultRounded)\n                            .height(myTextSizes.base.value.dp)\n                    )\n                } else if (item.isError) {\n                    SimpleText(\"!\")\n                }\n            },\n            actions = {\n\n                TransparentIconActionButton(\n                    enabled = calculatedChecksum != null,\n                    icon = MyIcons.copy,\n                    contentDescription = Res.string.copy.asStringSource(),\n                    onClick = {\n                        item.calculatedChecksum\n                            ?.let { savedChecksum -> ClipboardUtil.copy(savedChecksum) }\n                    },\n                )\n            }\n        )\n    }\n\n    @Composable\n    fun RenderSavedChecksum(\n        item: DownloadItemWithChecksum,\n        onRequestUpdateChecksum: () -> Unit,\n    ) {\n        ColumnKeyValue(\n            modifier = Modifier,\n            keyContent = {\n                RenderKey {\n                    Text(myStringResource(Res.string.saved_checksum))\n                }\n            },\n            valueContent = {\n                Text(item.savedChecksum.orEmpty())\n            },\n            actions = {\n                TransparentIconActionButton(\n                    icon = MyIcons.edit,\n                    contentDescription = Res.string.edit.asStringSource(),\n                    onClick = onRequestUpdateChecksum,\n                )\n                TransparentIconActionButton(\n                    icon = MyIcons.copy,\n                    contentDescription = Res.string.copy.asStringSource(),\n                    enabled = item.savedChecksum != null,\n                    onClick = {\n                        item.savedChecksum\n                            ?.let { savedChecksum -> ClipboardUtil.copy(savedChecksum) }\n                    },\n                )\n            }\n        )\n    }\n\n    @Composable\n    fun ChecksumEditSheet(\n        item: DownloadItemWithChecksum,\n        onCloseRequest: () -> Unit,\n        onRequestSaveNewChecksum: (FileChecksum?) -> Unit,\n    ) {\n        val editChecksumFlow = remember(item) {\n            MutableStateFlow<FileChecksum?>(FileChecksum(item.algorithm, item.savedChecksum.orEmpty()))\n        }\n        val fileChecksumConfigurable = remember(item) {\n            FileChecksumConfigurable(\n                title = Res.string.download_item_settings_file_checksum.asStringSource(),\n                description = Res.string.download_item_settings_file_checksum_description.asStringSource(),\n                backedBy = editChecksumFlow,\n                describe = {\n                    \"\".asStringSource()\n                },\n            )\n        }\n        SheetInput(\n            configurable = fileChecksumConfigurable,\n            isOpened = true,\n            onDismiss = onCloseRequest,\n            onConfirm = onRequestSaveNewChecksum,\n        ) {\n            RenderConfigurable(\n                fileChecksumConfigurable,\n                ConfigurableUiProps(\n                    modifier = it.modifier,\n                ),\n            )\n        }\n    }\n\n    @Composable\n    private fun ShimmerEffect(\n        modifier: Modifier = Modifier,\n        centerColor: Color = Color.Gray,\n        surroundingColor: Color = Color.Gray,\n    ) {\n        val transition = rememberInfiniteTransition()\n        val translateAnim = transition.animateFloat(\n            initialValue = 0f,\n            targetValue = 1000f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(\n                    durationMillis = 3000,\n                    easing = LinearEasing\n                )\n            )\n        )\n\n        val brush = Brush.linearGradient(\n            colors = listOf(\n                surroundingColor,\n                centerColor,\n                surroundingColor,\n            ),\n            start = Offset(0f, 0f),\n            end = Offset(translateAnim.value, 0f)\n        )\n\n        Box(\n            modifier = modifier\n                .background(brush = brush)\n        )\n    }\n\n    @Composable\n    private fun RenderErrorStatus(message: String) {\n        IconWithText(\n            icon = MyIcons.info,\n            text = message,\n            color = myColors.error,\n        )\n    }\n\n    @Composable\n    private fun RenderFinishedStatus(\n        status: ChecksumStatus.Finished,\n    ) {\n        val text: StringSource\n        val color: Color\n        val icon: IconSource\n        when (status) {\n            ChecksumStatus.Finished.Done -> {\n                text = Res.string.done.asStringSource()\n                icon = MyIcons.check\n                color = myColors.info\n            }\n\n            ChecksumStatus.Finished.Matches -> {\n                text = Res.string.matches.asStringSource()\n                icon = MyIcons.check\n                color = myColors.success\n            }\n\n            ChecksumStatus.Finished.NotMatches -> {\n                text = Res.string.not_matches.asStringSource()\n                icon = MyIcons.info\n                color = myColors.warning\n            }\n        }\n        IconWithText(\n            icon = icon,\n            text = text.rememberString(),\n            color = color,\n        )\n    }\n\n    @Composable\n    private fun IconWithText(\n        icon: IconSource,\n        text: String,\n        color: Color,\n    ) {\n        WithContentColor(color) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                MyIcon(\n                    icon,\n                    modifier = Modifier.size(16.dp),\n                    contentDescription = null,\n                )\n                Spacer(Modifier.width(2.dp))\n                SimpleText(text)\n            }\n        }\n    }\n\n    @Composable\n    private fun RenderCheckingStatus(percent: Int) {\n        Column {\n            ProgressStatus(percent, myColors.primaryGradient)\n        }\n    }\n\n    @Composable\n    private fun RenderWaitingStatus() {\n        Row {\n            SimpleText(\"${myStringResource(Res.string.waiting)} ${rememberDotLoading()}\")\n        }\n    }\n\n    @Composable\n    private fun ProgressStatus(\n        percent: Int?,\n        background: Brush = myColors.primaryGradient,\n    ) {\n        Box(\n            Modifier\n                .fillMaxWidth()\n                .clip(CircleShape)\n                .background(myColors.surface)\n        ) {\n            if (percent != null) {\n                val w = (percent / 100f).coerceIn(0f..1f)\n                Spacer(\n                    Modifier\n                        .height(5.dp)\n                        .fillMaxWidth(\n                            animateFloatAsState(\n                                w, tween(100)\n                            ).value\n                        )\n                        .background(background)\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun SimpleText(string: String, modifier: Modifier = Modifier) {\n        Text(\n            string,\n            modifier = modifier,\n            maxLines = 1,\n            overflow = TextOverflow.MiddleEllipsis,\n        )\n    }\n\n    @Composable\n    fun ColumnKeyValue(\n        modifier: Modifier,\n        keyContent: @Composable () -> Unit,\n        valueContent: @Composable () -> Unit,\n        actions: @Composable () -> Unit,\n    ) {\n        Row(modifier) {\n            Column(Modifier.weight(1f)) {\n                RenderKey { keyContent() }\n                Space()\n                RenderValue { valueContent() }\n            }\n            Space()\n            actions()\n        }\n    }\n\n    @Composable\n    fun Space() {\n        Spacer(Modifier.size(mySpacings.mediumSpace))\n    }\n\n    @Composable\n    fun RenderKey(\n        content: @Composable () -> Unit\n    ) {\n        WithContentAlpha(0.5f) {\n            content()\n        }\n    }\n\n    @Composable\n    fun RenderValue(\n        content: @Composable () -> Unit\n    ) {\n        WithContentAlpha(1f) {\n            content()\n        }\n    }\n\n    @Composable\n    fun RowKeyValue(\n        key: String,\n        value: String\n    ) {\n        Row {\n            RenderKey {\n                Text(key)\n            }\n            Space()\n            RenderValue {\n                Text(value)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderDownloadItemWithChecksum(\n    item: DownloadItemWithChecksum,\n    iconProvider: FileIconProvider,\n    onRequestUpdateChecksum: () -> Unit,\n    modifier: Modifier,\n) {\n    Column(\n        modifier.padding(\n            mySpacings.largeSpace,\n        )\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            MyIcon(\n                iconProvider.rememberIcon(item.downloadItem.name),\n                modifier = Modifier.size(24.dp),\n                contentDescription = null,\n            )\n            Spacer(Modifier.width(mySpacings.mediumSpace))\n            Column {\n                Text(item.downloadItem.name)\n                Spacer(Modifier.height(mySpacings.mediumSpace))\n                FileChecksumTableCellRenderers.RowKeyValue(\n                    key = myStringResource(Res.string.checksum_algorithm),\n                    value = item.algorithm\n                )\n            }\n        }\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        FileChecksumTableCellRenderers.RenderSavedChecksum(\n            item = item,\n            onRequestUpdateChecksum = onRequestUpdateChecksum\n        )\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        FileChecksumTableCellRenderers.RenderCalculatedChecksum(item)\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        FileChecksumTableCellRenderers.RenderStatus(item)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/crashreport/CrashReportActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.crashreport\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\n\nclass CrashReportActivity : ABDMActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val throwableData = getExceptionData(intent)\n        setABDMContent {\n            ErrorWindow(throwableData) {\n                finish()\n            }\n        }\n    }\n\n    private fun getExceptionData(intent: Intent): ThrowableData {\n        return ThrowableData(\n            intent.getStringExtra(TITLE_KEY).orEmpty(),\n            intent.getStringExtra(STACKTRACE_KEY).orEmpty(),\n        )\n    }\n\n    companion object {\n        private const val TITLE_KEY = \"title\"\n        private const val STACKTRACE_KEY = \"stacktrace\"\n        fun createIntent(\n            context: Context,\n            throwable: Throwable\n        ): Intent {\n            val throwableData = ThrowableData.fromThrowable(throwable)\n            return Intent(\n                context,\n                CrashReportActivity::class.java\n            ).apply {\n                putExtra(TITLE_KEY, throwableData.title)\n                putExtra(STACKTRACE_KEY, throwableData.stacktrace)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/crashreport/ErrorUi.kt",
    "content": "package com.abdownloadmanager.android.pages.crashreport\n\nimport android.os.Build\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.android.util.AppInfo\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun ErrorWindow(\n    throwable: ThrowableData,\n    close: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(true)\n    state.OnFullyDismissed(close)\n    ResponsiveDialog(state, state::hide) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\"Application Crash\")\n                    }\n                )\n            }\n        ) {\n            ErrorUi(throwable, state::hide)\n        }\n    }\n}\n\n@Composable\nprivate fun ErrorUi(\n    e: ThrowableData,\n    close: () -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .padding(horizontal = mySpacings.largeSpace)\n            .padding(bottom = mySpacings.largeSpace),\n    ) {\n        Header(\n            modifier = Modifier\n                .fillMaxWidth(),\n            e\n        )\n        Spacer(Modifier.height(8.dp))\n        RenderException(\n            modifier = Modifier\n                .fillMaxWidth()\n                .heightIn(max = 380.dp)\n                .weight(1f, false),\n            e = e\n        )\n        Spacer(Modifier.height(8.dp))\n        Actions(\n            modifier = Modifier\n                .fillMaxWidth(),\n            close = close,\n            copyInformation = {\n                ClipboardUtil.copy(createInformation(e))\n            }\n        )\n        Spacer(Modifier.height(8.dp))\n    }\n}\nfun createInformation(\n    exceptionString: ThrowableData,\n): String {\n\n    val version = AppInfo.version\n    val platform = AppInfo.platform.name\n    return \"\"\"\n### Application Runtime Error\n###### App Info\n```\nappVersion = $version\nplatform = $platform\nBrand: ${Build.BRAND}\nManufacturer: ${Build.MANUFACTURER}\nModel: ${Build.MODEL}\nAndroid Version: ${Build.VERSION.RELEASE}\nSDK: ${Build.VERSION.SDK_INT}\n```\n###### Exception\n```\n$exceptionString\n```\n\"\"\".trimIndent()\n}\n\n@Composable\nprivate fun Header(modifier: Modifier = Modifier, e: ThrowableData) {\n    Text(\n        text = \"We got an error in the application (\\\"${e.title}\\\")\", modifier = modifier,\n        fontSize = myTextSizes.xl\n    )\n}\n\n@Composable\nprivate fun RenderException(modifier: Modifier, e: ThrowableData) {\n    val errorText = e.stacktrace\n    Box(\n        modifier = modifier\n            .background(myColors.background)\n            .clip(myShapes.defaultRounded)\n            .horizontalScroll(rememberScrollState())\n            .verticalScroll(rememberScrollState())\n            .padding(8.dp)\n    ) {\n        SelectionContainer {\n            Text(\n                text = errorText,\n                color = myColors.error,\n                fontSize = myTextSizes.base,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier = Modifier,\n    close: () -> Unit,\n    copyInformation: () -> Unit,\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.End,\n    ) {\n        ActionButton(\n            text = myStringResource(Res.string.close),\n            onClick = close,\n            modifier = Modifier.weight(1f)\n        )\n        Spacer(Modifier.width(mySpacings.mediumSpace))\n        ActionButton(\n            text = myStringResource(Res.string.copy),\n            onClick = copyInformation,\n            modifier = Modifier.weight(1f)\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/crashreport/ThrowableData.kt",
    "content": "package com.abdownloadmanager.android.pages.crashreport\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ThrowableData(\n    val title: String,\n    val stacktrace: String,\n) {\n    companion object {\n        fun fromThrowable(throwable: Throwable): ThrowableData {\n            val title = throwable.localizedMessage ?: throwable.javaClass.simpleName ?: \"Unknown error\"\n            val stacktrace = throwable.stackTraceToString().replace(\"\\t\", \"    \")\n            return ThrowableData(title, stacktrace)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/credits/thirdpartylibraries/ExternalLibsPage.kt",
    "content": "package com.abdownloadmanager.android.pages.credits.thirdpartylibraries\n\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\nimport androidx.compose.foundation.background\nimport com.abdownloadmanager.shared.util.ui.ProvideTextStyle\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalResources\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.page.FooterFade\nimport com.abdownloadmanager.android.ui.page.HeaderFade\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.resources.Res\nimport com.mikepenz.aboutlibraries.Libs\nimport com.mikepenz.aboutlibraries.entity.Library\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.PageTitleWithDescription\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.page.rememberHeaderAlpha\nimport com.abdownloadmanager.android.util.compose.useBack\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.dpToPx\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n\n@Composable\nfun ThirdPartyLibrariesPage() {\n    val pageTitle = myStringResource(Res.string.third_party_libraries)\n    val pageDescription = myStringResource(Res.string.powered_by_open_source_software)\n    val onBack = useBack()\n    var contentPadding by remember { mutableStateOf(PaddingValues.Zero) }\n    val topPadding = contentPadding.calculateTopPadding()\n    val bottomPadding = contentPadding.calculateBottomPadding()\n    val density = LocalDensity.current\n    val listState = rememberLazyListState()\n    val headerAlpha by rememberHeaderAlpha(listState, topPadding.dpToPx(density))\n    PageUi(\n        header = {\n            PageHeader(\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        MyIcons.back,\n                        Res.string.back.asStringSource()\n                    ) {\n                        onBack?.onBackPressed()\n                    }\n                },\n                headerTitle = {\n                    PageTitleWithDescription(pageTitle, pageDescription)\n                },\n                modifier = Modifier\n                    .background(\n                        myColors.background.copy(\n                            alpha = headerAlpha * 0.75f,\n                        )\n                    )\n                    .statusBarsPadding()\n            )\n        },\n        footer = {\n            Spacer(Modifier.navigationBarsPadding())\n        }\n    ) {\n        contentPadding = it.paddingValues\n        Box(\n            Modifier\n                .fillMaxSize()\n        ) {\n            OpenSourceLibraries(\n                libs = rememberLibs(),\n                modifier = Modifier,\n                state = listState,\n                contentPadding = it.paddingValues,\n            )\n            FooterFade(bottomPadding)\n        }\n    }\n}\n\n@Composable\nprivate fun OpenSourceLibraries(\n    libs: Libs,\n    modifier: Modifier,\n    state: LazyListState,\n    contentPadding: PaddingValues,\n) {\n    val dividerColor = myColors.onBackground / 0.5f\n    var currentDialog by remember {\n        mutableStateOf(null as Library?)\n    }\n    Column(modifier) {\n        LazyColumn(\n            state = state,\n            contentPadding = contentPadding,\n        ) {\n            itemsIndexed(libs.libraries) { index, item ->\n                val isFirstItem = index == 0\n                RenderLibraryItemInList(\n                    item,\n                    Modifier\n                        .ifThen(!isFirstItem) {\n                            drawBehind {\n                                drawLine(\n                                    brush = Brush.horizontalGradient(\n                                        listOf(\n                                            Color.Transparent,\n                                            dividerColor,\n                                            Color.Transparent,\n                                        )\n                                    ),\n                                    start = Offset.Zero,\n                                    end = Offset(size.width, 0f)\n                                )\n                            }\n                        }\n                        .fillMaxWidth()\n                        .clickable {\n                            currentDialog = item\n                        }\n                        .padding(mySpacings.largeSpace)\n                )\n            }\n        }\n    }\n    LibraryDialog(\n        library = currentDialog,\n        onCloseRequest = {\n            currentDialog = null\n        }\n    )\n\n}\n\n@Composable\nprivate fun RenderLibraryItemInList(\n    library: Library,\n    modifier: Modifier,\n) {\n    Column(modifier) {\n        Column {\n            WithContentAlpha(1f) {\n                Row(Modifier) {\n                    Text(\n                        library.name,\n                        fontSize = myTextSizes.base,\n                        overflow = TextOverflow.Ellipsis,\n                        maxLines = 1\n                    )\n                    Spacer(Modifier.width(2.dp))\n                    library.artifactVersion?.let { version ->\n                        Text(\n                            text = version,\n                            fontSize = myTextSizes.base,\n                            overflow = TextOverflow.Ellipsis,\n                            maxLines = 1,\n                        )\n                    }\n                }\n            }\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            WithContentAlpha(0.75f) {\n                Text(\n                    library.artifactId,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontSize = myTextSizes.sm,\n                )\n            }\n        }\n        val by = library.by()\n        if (by.isNotEmpty()) {\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            Row {\n                WithContentAlpha(0.7f) {\n                    ProvideTextStyle(\n                        TextStyle(fontSize = myTextSizes.sm)\n                    ) {\n                        for ((index, item) in by.withIndex()) {\n                            val (name, _) = item\n                            if (index != 0) {\n                                Spacer(Modifier.width(4.dp))\n                            }\n                            Text(\n                                text = name,\n                                fontSize = myTextSizes.sm,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        WithContentAlpha(0.75f) {\n            Text(\n                text = library.licenses.joinToString(\", \") { it.name },\n                fontSize = myTextSizes.sm,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n\n}\n\nprivate fun Library.by(): List<Pair<String, String?>> {\n    val d = developers.filter {\n        it.name != null\n    }.map {\n        it.name!! to it.organisationUrl\n    }.takeIf { it.isNotEmpty() }\n    if (d != null) return d\n    return organization?.let {\n        listOf(it.name to it.url)\n    } ?: emptyList()\n}\n\n@Composable\nprivate fun rememberLibs(): Libs {\n    val resources = LocalResources.current\n    return remember {\n        val jsonContent = resources\n            .openRawResource(com.abdownloadmanager.android.R.raw.aboutlibraries)\n            .bufferedReader()\n            .use { it.readText() }\n        Libs.Builder().withJson(jsonContent).build()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/credits/thirdpartylibraries/LibraryDialog.kt",
    "content": "package com.abdownloadmanager.android.pages.credits.thirdpartylibraries\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\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 com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MaybeLinkText\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.mikepenz.aboutlibraries.entity.Developer\nimport com.mikepenz.aboutlibraries.entity.Library\nimport com.mikepenz.aboutlibraries.entity.License\nimport com.mikepenz.aboutlibraries.entity.Organization\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.collections.immutable.ImmutableSet\n\n@Composable\nfun LibraryDialog(\n    library: Library?,\n    onCloseRequest: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    state.OnFullyDismissed(onCloseRequest)\n    LaunchedEffect(library) {\n        if (library == null) {\n            state.hide()\n        } else {\n            state.show()\n        }\n    }\n    val hideDialog: () -> Unit = state::hide\n    ResponsiveDialog(\n        state = state,\n        onDismiss = hideDialog\n    ) {\n        Column {\n            library?.let { library ->\n                SheetUI(\n                    header = {\n                        SheetHeader(\n                            headerTitle = {\n                                SheetTitle(myStringResource(Res.string.info))\n                            }\n                        )\n                    }\n                ) {\n                    Column(\n                        modifier = Modifier.padding(mySpacings.largeSpace)\n                    ) {\n                        Column(\n                            Modifier\n                                .weight(1f, false)\n                                .verticalScroll(rememberScrollState())\n                        ) {\n                            LibraryNameAndVersion(library.name, library.artifactVersion, library.artifactId)\n                            Spacer(Modifier.height(16.dp))\n                            library.description?.let {\n                                LibraryDescription(it)\n                                Spacer(Modifier.height(16.dp))\n                            }\n                            library.developers.takeIf { it.isNotEmpty() }?.let {\n                                LibraryDevelopers(it)\n                                Spacer(Modifier.height(8.dp))\n                            }\n                            library.organization?.let {\n                                LibraryOrganization(it)\n                                Spacer(Modifier.height(8.dp))\n                            }\n                            val links = buildList {\n                                library.scm?.url?.let {\n                                    add(Res.string.source_code.asStringSource() to it)\n                                }\n                                library.website?.let {\n                                    add(Res.string.website.asStringSource() to it)\n                                }\n                            }\n                            links.takeIf { it.isNotEmpty() }?.let {\n                                LibraryLinks(links)\n                                Spacer(Modifier.height(8.dp))\n                            }\n                            LibraryLicenseInfo(library.licenses)\n                        }\n                        Spacer(Modifier.height(8.dp))\n                        Row(\n                            Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.End,\n                        ) {\n                            ActionButton(\n                                text = myStringResource(Res.string.close),\n                                onClick = {\n                                    hideDialog()\n                                },\n                                modifier = Modifier.weight(1f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LibraryLinks(links: List<Pair<StringSource, String>>) {\n    KeyValue(myStringResource(Res.string.links)) {\n        ListOfNamesWithLinks(links)\n    }\n}\n\n@Composable\nprivate fun LibraryDescription(description: String) {\n    Text(\n        description,\n        modifier = Modifier\n            .fillMaxWidth()\n            .clip(myShapes.defaultRounded)\n            .background(myColors.onSurface / 0.1f)\n            .padding(8.dp),\n        color = myColors.onSurface,\n    )\n}\n\n@Composable\nprivate fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {\n    KeyValue(myStringResource(Res.string.license)) {\n        val l = licenses.map {\n            it.name.asStringSource() to it.url\n        }\n        if (l.isEmpty()) {\n            Text(myStringResource(Res.string.no_license_found))\n        } else {\n            ListOfNamesWithLinks(l)\n        }\n    }\n}\n\n@Composable\nprivate fun LibraryDevelopers(devs: List<Developer>) {\n    KeyValue(myStringResource(Res.string.developers)) {\n        ListOfNamesWithLinks(\n            devs\n                .filter { it.name != null }\n                .map {\n                    it.name!!.asStringSource() to it.organisationUrl\n                }\n        )\n    }\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable\nprivate fun ListOfNamesWithLinks(map: List<Pair<StringSource, String?>>) {\n    FlowRow {\n        for ((i, v) in map.withIndex()) {\n            val (name, link) = v\n            MaybeLinkText(name.rememberString(), link)\n            if (i < map.lastIndex) {\n                Text(\", \")\n            }\n        }\n    }\n}\n\n@Composable\nfun LibraryOrganization(organization: Organization) {\n    KeyValue(myStringResource(Res.string.organization)) {\n        MaybeLinkText(organization.name, organization.url)\n    }\n}\n\n@Composable\nprivate fun LibraryNameAndVersion(\n    name: String, version: String?,\n    artifactId: String,\n) {\n    val nameWithVersion = name + (version?.let { \" $it\" }.orEmpty())\n    Column {\n        Row {\n            Text(\n                \"$nameWithVersion\",\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.base,\n            )\n        }\n        Spacer(Modifier.height(4.dp))\n        WithContentAlpha(0.75f) {\n            Row {\n                Text(\n                    \"($artifactId)\",\n                    fontSize = myTextSizes.sm,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun KeyValue(\n    key: String,\n    value: @Composable () -> Unit,\n) {\n    Row {\n        WithContentAlpha(0.75f) {\n            Text(\n                \"$key:\",\n                maxLines = 1,\n            )\n        }\n        Spacer(Modifier.width(8.dp))\n        value()\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/credits/translators/TranslatorsPage.kt",
    "content": "package com.abdownloadmanager.android.pages.credits.translators\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.animation.togetherWith\nimport com.abdownloadmanager.android.di.Di\nimport com.abdownloadmanager.resources.ABDMResources\nimport com.abdownloadmanager.shared.ui.widget.MaybeLinkText\nimport kotlinx.coroutines.runBlocking\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\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.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.page.FooterFade\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.page.rememberHeaderAlpha\nimport com.abdownloadmanager.android.util.compose.useBack\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.credits.translators.LanguageTranslationInfo\nimport com.abdownloadmanager.shared.pages.credits.translators.TranslatorData\nimport com.abdownloadmanager.shared.ui.widget.PrimaryMainActionButton\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.dpToPx\nimport ir.amirab.util.compose.localizationmanager.LanguageNameProvider\nimport ir.amirab.util.compose.localizationmanager.MyLocale\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.get\n\n@Composable\nfun TranslatorsPage(onBack: () -> Unit) {\n    Translators(\n        Modifier\n            .fillMaxSize()\n            .background(myColors.background)\n    )\n}\n\n@Composable\ninternal fun Translators(modifier: Modifier) {\n    val listState = rememberLazyListState()\n    var contentPadding by remember {\n        mutableStateOf(PaddingValues.Zero)\n    }\n    val topPadding = contentPadding.calculateTopPadding()\n    val bottomPadding = contentPadding.calculateBottomPadding()\n    val density = LocalDensity.current\n    val headerAlpha by rememberHeaderAlpha(listState, topPadding.dpToPx(density))\n    PageUi(\n        modifier = modifier,\n        header = {\n            val onBack = useBack()\n            PageHeader(\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        MyIcons.back,\n                        Res.string.back.asStringSource(),\n                    ) {\n                        onBack?.onBackPressed()\n                    }\n                },\n                headerTitle = {\n                    PageTitle(\n                        myStringResource(Res.string.meet_the_translators)\n                    )\n                },\n                modifier = Modifier\n                    .background(\n                        myColors.background.copy(\n                            alpha = headerAlpha * 0.75f\n                        )\n                    )\n                    .statusBarsPadding()\n            )\n        },\n        footer = {\n            AnimatedContent(\n                headerAlpha == 0f,\n                transitionSpec = {\n                    fadeIn() + expandVertically() togetherWith fadeOut() + shrinkVertically()\n                },\n                contentAlignment = Alignment.BottomCenter,\n            ) {\n                if (it) {\n                    ContributionNotice(\n                        modifier = Modifier,\n                        onUserWantsToContribute = {\n                            URLOpener.openUrl(SharedConstants.projectTranslations)\n                        }\n                    )\n                } else {\n                    Spacer(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .navigationBarsPadding()\n                    )\n                }\n            }\n//            AnimatedVisibility(\n//                headerAlpha == 0f,\n//                enter = expandVertically() + fadeIn(),\n//                exit = shrinkVertically() + fadeOut(),\n//            ) {\n//\n//            }\n        },\n    ) {\n        contentPadding = it.paddingValues\n        Box {\n            DearTranslators(\n                Modifier\n                    .fillMaxWidth(),\n                state = listState,\n                contentPadding = it.paddingValues,\n            )\n            FooterFade(bottomPadding)\n        }\n    }\n}\n\n@Composable\nprivate fun ContributionNotice(\n    modifier: Modifier,\n    onUserWantsToContribute: () -> Unit,\n) {\n    Column(\n        modifier\n            .fillMaxWidth()\n            .background(myColors.surface),\n    ) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onSurface / 0.15f)\n        )\n        Column(\n            Modifier\n                .padding(mySpacings.largeSpace)\n                .navigationBarsPadding()\n        ) {\n            Text(\n                myStringResource(Res.string.translators_page_thanks),\n                modifier = Modifier,\n                fontSize = myTextSizes.lg,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 8.dp)\n                    .height(1.dp)\n                    .background(myColors.surface)\n            )\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Column(\n                    Modifier.weight(1f)\n                ) {\n                    Text(\n                        myStringResource(Res.string.translators_contribute_title),\n                        fontSize = myTextSizes.lg,\n                        fontWeight = FontWeight.Bold,\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    Text(\n                        myStringResource(Res.string.translators_contribute_description),\n                        fontSize = myTextSizes.base,\n                        color = LocalContentColor.current / 0.75f\n                    )\n                }\n            }\n            Spacer(Modifier.height(mySpacings.largeSpace))\n            PrimaryMainActionButton(\n                text = myStringResource(Res.string.contribute),\n                onClick = onUserWantsToContribute,\n                modifier = Modifier.fillMaxWidth(),\n                enabled = true,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun DearTranslators(\n    modifier: Modifier,\n    state: LazyListState,\n    contentPadding: PaddingValues,\n) {\n    val itemHorizontalPadding = 16.dp\n    val list = rememberLanguageTranslationInfo()\n\n    LazyColumn(\n        modifier,\n        state = state,\n        contentPadding = contentPadding,\n    ) {\n        itemsIndexed(list) { index, item ->\n            TranslatedLanguageItem(\n                item,\n                Modifier\n                    .fillMaxWidth()\n                    .ifThen(index % 2 == 1) {\n                        background(myColors.surface)\n                    }\n                    .padding(16.dp, itemHorizontalPadding)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun TranslatedLanguageItem(\n    translationInfo: LanguageTranslationInfo,\n    modifier: Modifier,\n) {\n    Column(modifier) {\n        Column {\n            WithContentAlpha(1f) {\n                Text(\n                    translationInfo.nativeName,\n                    fontSize = myTextSizes.base,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1\n                )\n            }\n            Spacer(Modifier.height(mySpacings.smallSpace))\n            WithContentAlpha(0.75f) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        translationInfo.englishName,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontSize = myTextSizes.base,\n                    )\n                    Spacer(Modifier.width(4.dp))\n                    Text(\n                        translationInfo.locale,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontSize = myTextSizes.base,\n                        color = myColors.primary,\n                        modifier = Modifier\n                            .background(myColors.primary / 10)\n                            .padding(vertical = 0.dp, horizontal = 4.dp)\n                    )\n                }\n            }\n        }\n        Spacer(Modifier.height(mySpacings.mediumSpace))\n        Column(\n            verticalArrangement = Arrangement.spacedBy(mySpacings.smallSpace)\n        ) {\n            translationInfo.translators.forEach {\n                MaybeLinkText(\n                    it.name,\n                    it.link,\n                )\n            }\n        }\n    }\n}\n\nprivate fun convertLanguageToMyLocale(language: String): MyLocale {\n    return language.split(\"-\").run {\n        MyLocale(\n            languageCode = get(0),\n            countryCode = getOrNull(1)\n        )\n    }\n}\n\n@Composable\nprivate fun rememberLanguageTranslationInfo(): List<LanguageTranslationInfo> {\n    return remember {\n        val json = Di.get<Json>()\n        val translatorData = runBlocking {\n            ABDMResources.getTranslatorsContent()\n        }.let {\n            json.decodeFromString<TranslatorData>(it)\n        }\n        translatorData.map {\n            val name = LanguageNameProvider.getName(convertLanguageToMyLocale(it.key))\n            LanguageTranslationInfo(\n                locale = it.key,\n                englishName = name.englishName,\n                nativeName = name.nativeName,\n                translators = it.value,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/directorypicker/ComposeExtension.kt",
    "content": "package com.abdownloadmanager.android.pages.directorypicker\n\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport ir.amirab.util.compose.StringSource\nimport okio.Path.Companion.toPath\n\nclass DirectoryPickerLauncher(\n    private val onLaunch: () -> Unit,\n) {\n    fun launch() {\n        onLaunch()\n    }\n}\n\n\n@Composable\nfun rememberAndroidDirectoryPickerLauncher(\n    initialDirectory: String?,\n    title: StringSource,\n    onDirectorySelected: (String?) -> Unit,\n): DirectoryPickerLauncher {\n    val pickFolderLauncher = rememberLauncherForActivityResult(\n        contract = DirectoryPickerActivity.Contract,\n    ) { directory ->\n        onDirectorySelected(directory?.toString())\n    }\n    val initialDirectory by rememberUpdatedState(initialDirectory)\n    val title by rememberUpdatedState(title)\n    return remember {\n        DirectoryPickerLauncher {\n            pickFolderLauncher.launch(\n                DirectoryPickerActivity.Inputs(\n                    title = title,\n                    initialDirectory = initialDirectory?.toPath(),\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/directorypicker/DirectoryPickerActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.directorypicker\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.Environment\nimport androidx.activity.result.contract.ActivityResultContract\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport okio.Path\nimport okio.Path.Companion.toPath\n\nclass DirectoryPickerActivity : ABDMActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val title = DirectoryPickerActivity.getTitle(intent).orEmpty().asStringSource()\n        val initialDirectory = (\n                DirectoryPickerActivity.getInitialDirectory(intent)\n                // default if there is no directory provided to us\n                    ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()\n                ).toPath()\n\n        setABDMContent {\n            DirectoryPicker(\n                title = title,\n                isVisible = true,\n                initialDirectory = initialDirectory,\n                onDirectorySelected = {\n                    returnTheActivityResult(it)\n                },\n            )\n        }\n    }\n\n    private fun returnTheActivityResult(path: Path?) {\n        if (path == null) {\n            setResult(RESULT_CANCELED)\n        } else {\n            setResult(RESULT_OK, Intent().putExtra(DIRECTORY_RESULT, path.toString()))\n        }\n        finish()\n    }\n\n    data class Inputs(\n        val title: StringSource,\n        val initialDirectory: Path?,\n    )\n\n    companion object {\n        const val TITLE_KEY = \"title\"\n        const val INITIAL_DIR_KEY = \"initialDirectory\"\n        const val DIRECTORY_RESULT = \"directory\"\n\n        val Contract = object : ActivityResultContract<Inputs, Path?>() {\n            override fun createIntent(\n                context: Context,\n                input: Inputs\n            ): Intent {\n                return Intent(context, DirectoryPickerActivity::class.java).apply {\n                    putExtra(TITLE_KEY, input.title.getString())\n                    putExtra(INITIAL_DIR_KEY, input.initialDirectory.toString())\n                }\n            }\n\n            override fun parseResult(resultCode: Int, intent: Intent?): Path? {\n                return if (resultCode == RESULT_OK) {\n                    intent?.getStringExtra(DIRECTORY_RESULT)?.toPath()\n                } else null\n            }\n\n        }\n\n\n        fun getTitle(intent: Intent): String? {\n            return intent.getStringExtra(TITLE_KEY)\n        }\n\n        fun getInitialDirectory(intent: Intent): String? {\n            return intent.getStringExtra(INITIAL_DIR_KEY)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/directorypicker/DirectoryPickerPage.kt",
    "content": "package com.abdownloadmanager.android.pages.directorypicker\n\nimport android.content.Context\nimport android.os.Environment\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.clickable\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.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\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.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.onboarding.permissions.ABDMPermissions\nimport com.abdownloadmanager.android.pages.onboarding.permissions.rememberAppPermissionState\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitleWithDescription\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.android.ui.configurable.RenderSpinnerInSheet\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.PrimaryMainActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.PathValidator\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.createDirectories\nimport ir.amirab.util.exists\nimport ir.amirab.util.isDirectory\nimport ir.amirab.util.listFiles\nimport ir.amirab.util.listFilesOrNull\nimport ir.amirab.util.startsWith\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport okio.Path\nimport okio.Path.Companion.toOkioPath\nimport okio.Path.Companion.toPath\n\nval alwaysAllowedPaths = listOf(\n    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toOkioPath(),\n)\n\n@Composable\nfun DirectoryPicker(\n    title: StringSource,\n    isVisible: Boolean,\n    initialDirectory: Path,\n    onDirectorySelected: (Path?) -> Unit\n) {\n    var changingRoot by remember { mutableStateOf(false) }\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    state.OnFullyDismissed {\n        onDirectorySelected(null)\n    }\n    val onDismiss = state::hide\n    ResponsiveDialog(\n        state,\n        onDismiss\n    ) {\n        var currentDirectory by remember(initialDirectory) {\n            mutableStateOf(initialDirectory)\n        }\n        var creatingNewFolder by remember { mutableStateOf(false) }\n        // update this counter in order to refresh directory list!\n        var updateDirectories by remember { mutableIntStateOf(0) }\n        fun refreshDirectories() {\n            updateDirectories++\n        }\n\n        val storagePermissionState = rememberAppPermissionState(ABDMPermissions.StoragePermission)\n        val directoryList = remember(\n            currentDirectory,\n            updateDirectories,\n            storagePermissionState.isGranted,\n        ) {\n            val weHaveFullAccess = storagePermissionState.isGranted\n            DirectoryList(\n                currentDirectory = currentDirectory,\n                directories = runCatching {\n                    currentDirectory\n                        .listFiles()\n                        .filter { it.isDirectory() }\n                }\n                    .getOrNull()\n                    .orEmpty()\n                    .map {\n                        DirectoryItem(it.name, it)\n                    },\n                backDirectory = currentDirectory\n                    .parent\n                    // don't go somewhere that we can't return\n                    ?.takeIf {\n                        it.listFilesOrNull()?.isNotEmpty() ?: false\n                    },\n                currentDirectoryCanWrite = if (weHaveFullAccess) {\n                    true\n                } else {\n                    alwaysAllowedPaths.any { allowedPath ->\n                        currentDirectory.startsWith(allowedPath)\n                    }\n                }\n            )\n        }\n        val coroutineScope = rememberCoroutineScope()\n        fun createNewFolderAndRefresh(newFolderName: String) {\n            creatingNewFolder = false\n            coroutineScope.launch(Dispatchers.IO) {\n                runCatching {\n                    currentDirectory.resolve(newFolderName).createDirectories()\n                }\n                delay(50)\n                refreshDirectories()\n                // schedule refresh\n            }\n        }\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitleWithDescription(\n                            title = title.rememberString(),\n                            description = currentDirectory.toString()\n                        )\n                    },\n                    headerActions = {\n                        TransparentIconActionButton(\n                            MyIcons.close,\n                            contentDescription = Res.string.close.asStringSource(),\n                            onClick = onDismiss\n                        )\n                    }\n                )\n            }\n        ) {\n            val horizontalPadding = mySpacings.largeSpace\n            val itemPadding = PaddingValues(\n                horizontal = mySpacings.largeSpace,\n                vertical = mySpacings.mediumSpace\n            )\n            Column {\n                val lazyListState = rememberLazyListState()\n                AnimatedContent(\n                    directoryList,\n                    modifier = Modifier\n                        .weight(1f, false)\n                ) { directoryList ->\n                    VerticalScrollableContent(\n                        lazyListState = lazyListState,\n                    ) {\n                        Box(\n                            Modifier.heightIn(250.dp)\n                        ) {\n                            LazyColumn {\n                                if (directoryList.backDirectory != null) {\n                                    item {\n                                        val backDirectoryItem = remember(directoryList.currentDirectory) {\n                                            DirectoryItem(\n                                                name = \"..\",\n                                                path = directoryList.backDirectory\n                                            )\n                                        }\n                                        RenderDirectoryItem(\n                                            modifier = Modifier\n                                                .animateItem()\n                                                .fillMaxWidth(),\n                                            item = backDirectoryItem,\n                                            onDirectorySelected = {\n                                                currentDirectory = backDirectoryItem.path\n                                            },\n                                            itemPadding = itemPadding,\n                                        )\n                                    }\n                                }\n                                items(directoryList.directories) { directoryItem ->\n                                    RenderDirectoryItem(\n                                        modifier = Modifier\n                                            .animateItem()\n                                            .fillMaxWidth(),\n                                        item = directoryItem,\n                                        onDirectorySelected = {\n                                            currentDirectory = directoryItem.path\n                                        },\n                                        itemPadding = itemPadding,\n                                    )\n                                }\n                            }\n                            if (directoryList.directories.isEmpty()) {\n                                Text(\n                                    myStringResource(Res.string.list_is_empty),\n                                    Modifier\n                                        .matchParentSize()\n                                        .wrapContentSize()\n                                )\n                            }\n                        }\n                    }\n\n                }\n                Spacer(Modifier.height(mySpacings.mediumSpace))\n                Column(\n                    Modifier.padding(horizontal = horizontalPadding)\n                ) {\n                    AnimatedVisibility(!directoryList.currentDirectoryCanWrite && !storagePermissionState.isGranted) {\n                        ActionButton(\n                            text = myStringResource(Res.string.give_storage_permission),\n                            onClick = {\n                                storagePermissionState.launchRequest()\n                            },\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(bottom = mySpacings.mediumSpace),\n                            borderColor = myColors.warningGradient,\n                            contentColor = myColors.warning,\n                            start = {\n                                MyIcon(\n                                    icon = storagePermissionState.appPermission.icon,\n                                    contentDescription = null,\n                                    modifier = Modifier\n                                        .size(24.dp)\n                                        .padding(end = mySpacings.mediumSpace)\n                                )\n                            }\n                        )\n                    }\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        IconActionButton(\n                            MyIcons.folder,\n                            contentDescription = Res.string.storage_roots.asStringSource(),\n                            onClick = {\n                                changingRoot = true\n                            }\n                        )\n                        Spacer(Modifier.width(mySpacings.mediumSpace))\n                        PrimaryMainActionButton(\n                            text = myStringResource(Res.string.ok),\n                            onClick = {\n                                onDirectorySelected(currentDirectory)\n                            },\n                            modifier = Modifier.weight(1f),\n                            enabled = directoryList.currentDirectoryCanWrite,\n                        )\n                        Spacer(Modifier.width(mySpacings.mediumSpace))\n                        IconActionButton(\n                            MyIcons.add,\n                            contentDescription = Res.string.new_folder.asStringSource(),\n                            enabled = directoryList.currentDirectoryCanWrite,\n                        ) {\n                            creatingNewFolder = true\n                        }\n                    }\n                }\n            }\n            SheetInput(\n                isOpened = creatingNewFolder,\n                title = Res.string.new_folder.asStringSource(),\n                initialValue = { \"\" },\n                validate = {\n                    val newFolder = runCatching { currentDirectory.resolve(it) }.getOrNull() ?: return@SheetInput false\n                    PathValidator.isValidPath(newFolder.toString()) && !newFolder.exists()\n                },\n                onConfirm = { newFolderName ->\n                    createNewFolderAndRefresh(newFolderName)\n                },\n                onDismiss = {\n                    creatingNewFolder = false\n                }\n            ) { params ->\n                MyTextField(\n                    text = params.editingValue,\n                    onTextChange = params.setEditingValue,\n                    modifier = params.modifier,\n                    placeholder = \"New Folder\",\n                    keyboardActions = params.keyboardActions,\n                )\n            }\n        }\n        StorageRoots(\n            onRequestChangeStorageRoot = {\n                currentDirectory = it\n                changingRoot = false\n            },\n            currentDirectory = currentDirectory,\n            isOpened = changingRoot,\n            onDismiss = {\n                changingRoot = false\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun RenderDirectoryItem(\n    modifier: Modifier,\n    item: DirectoryItem,\n    onDirectorySelected: () -> Unit,\n    itemPadding: PaddingValues,\n) {\n    Row(\n        modifier\n            .clickable(\n                onClick = onDirectorySelected\n            )\n            .heightIn(mySpacings.thumbSize)\n            .padding(itemPadding),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        MyIcon(MyIcons.folder, null)\n        Spacer(Modifier.width(mySpacings.mediumSpace))\n        Text(item.name)\n    }\n}\n\n@Composable\nprivate fun StorageRoots(\n    onRequestChangeStorageRoot: (Path) -> Unit,\n    currentDirectory: Path,\n    isOpened: Boolean,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val roots = remember {\n        runCatching { getRootPaths(context) }\n            .onFailure { it.printStackTrace() }\n            .getOrElse { emptyList() }\n    }\n    val currentRoot = remember(currentDirectory) {\n        roots.firstOrNull {\n            currentDirectory.startsWith(it)\n        }\n    }\n    RenderSpinnerInSheet(\n        title = Res.string.storage_roots.asStringSource(),\n        isOpened = isOpened,\n        onDismiss = onDismiss,\n        possibleValues = roots,\n        value = currentRoot,\n        onSelect = {\n            if (it == null) {\n                onDismiss()\n            } else {\n                onRequestChangeStorageRoot(it)\n            }\n        },\n    ) {\n        Row {\n            Text(it.toString())\n        }\n    }\n}\n\nprivate fun getRootPaths(context: Context): List<Path> {\n    val externalFilesDirs = context.getExternalFilesDirs(null)\n    if (externalFilesDirs.isNullOrEmpty()) {\n        return emptyList()\n    }\n    return externalFilesDirs\n        .map {\n            it\n                .absolutePath\n                .substringBefore(\"/Android/\")\n                .toPath()\n        }\n}\n\n@Immutable\nprivate data class DirectoryList(\n    val currentDirectory: Path,\n    val backDirectory: Path?,\n    val directories: List<DirectoryItem>,\n    val currentDirectoryCanWrite: Boolean,\n)\n\n@Immutable\nprivate data class DirectoryItem(\n    val name: String,\n    val path: Path,\n)\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/editdownload/AndroidEditDownloadComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.editdownload\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.editdownload.BaseEditDownloadComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.flow.*\n\nclass AndroidEditDownloadComponent(\n    ctx: ComponentContext,\n    onRequestClose: () -> Unit,\n    downloadId: Long,\n    acceptEdit: StateFlow<Boolean>,\n    onEdited: ((IDownloadItem) -> Unit, DownloadJobExtraConfig?) -> Unit,\n    downloadSystem: DownloadSystem,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    iconProvider: FileIconProvider,\n) : BaseEditDownloadComponent(\n    ctx = ctx,\n    downloadSystem = downloadSystem,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    iconProvider = iconProvider,\n    onEdited = onEdited,\n    onRequestClose = onRequestClose,\n    downloadId = downloadId,\n    acceptEdit = acceptEdit,\n),\n    ContainsEffects<AndroidEditDownloadComponent.Effects> by supportEffects() {\n    sealed interface Effects {\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/editdownload/EditDownload.kt",
    "content": "package com.abdownloadmanager.android.pages.editdownload\n\nimport androidx.compose.runtime.Composable\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.*\nimport androidx.compose.ui.window.*\nimport com.abdownloadmanager.android.pages.add.shared.ExtraConfig\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.ui.widget.*\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditDownloadResult\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditWarnings\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.edit.TAEditDownloadInputs\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun EditDownloadSheet(\n    component: AndroidEditDownloadComponent?,\n    onDismiss: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    state.OnFullyDismissed(onDismiss)\n    LaunchedEffect(component) {\n        if (component == null) {\n            state.hide()\n        } else {\n            state.show()\n        }\n    }\n    ResponsiveDialog(state, state::hide) {\n        component?.let {\n            EditDownloadPage(component, state::hide)\n        }\n    }\n}\n\n@Composable\nfun ResponsiveDialogScope.EditDownloadPage(\n    component: AndroidEditDownloadComponent,\n    onDismiss: () -> Unit,\n) {\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(myStringResource(Res.string.edit_download_title))\n                },\n                headerActions = {\n                    TransparentIconActionButton(\n                        MyIcons.close,\n                        contentDescription = Res.string.close.asStringSource(),\n                        onClick = onDismiss\n                    )\n                },\n            )\n        }\n    ) {\n        component.editDownloadUiChecker.collectAsState().value?.let { downloadInputs ->\n            Column(\n                Modifier\n                    .padding(mySpacings.mediumSpace)\n            ) {\n                val canAddResult by downloadInputs.canEditDownloadResult.collectAsState()\n                val link by downloadInputs.link.collectAsState()\n                fun setLink(link: String) {\n                    downloadInputs.setLink(link)\n                }\n\n                val linkFocus = remember { FocusRequester() }\n                LaunchedEffect(Unit) {\n                    linkFocus.requestFocus()\n                }\n\n                UrlTextField(\n                    text = link,\n                    setText = {\n                        setLink(it)\n                    },\n                    modifier = Modifier.focusRequester(linkFocus),\n                    errorText = when (canAddResult) {\n                        CanEditDownloadResult.InvalidURL -> Res.string.invalid_url\n                        else -> null\n                    }?.takeIf { link.isNotEmpty() }?.asStringSource()?.rememberString()\n                    // ATTENTION DO NOT use composable functions in when branches\n                    // it seems buggy (compose won't render ui properly)\n                    // stranger part is that in this case if we use ? before takeIf then it will work! (`}.takeIf {` is  buggy but `}?.takeIf {` works!)\n                    // maybe there is a bug in compose compiler, or maybe I'm missed something. if you read this ,and you know why! please let me know!\n                )\n                val name by downloadInputs.name.collectAsState()\n                Spacer(Modifier.size(8.dp))\n                NameTextField(\n                    text = name,\n                    setText = {\n                        downloadInputs.setName(it)\n                    },\n                    errorText = when (canAddResult) {\n                        CanEditDownloadResult.FileNameAlreadyExists -> Res.string.file_name_already_exists\n                        CanEditDownloadResult.InvalidFileName -> Res.string.invalid_file_name\n                        else -> null\n                    }?.takeIf { name.isNotEmpty() }?.asStringSource()?.rememberString()\n                )\n                Spacer(Modifier.size(8.dp))\n                Row(\n                    modifier = Modifier,\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    RenderFileTypeAndSize(component.iconProvider, downloadInputs)\n                    RenderResumeSupport(downloadInputs, Modifier.weight(1f))\n                    ConfigActionsButtons(downloadInputs)\n                }\n                Spacer(Modifier.size(8.dp))\n                MainActionButtons(component, downloadInputs)\n                val showMoreSettings by downloadInputs.showMoreSettings.collectAsState()\n                ExtraConfig(\n                    onDismiss = {\n                        downloadInputs.setShowMoreSettings(false)\n                    },\n                    configurables = downloadInputs.configurableList,\n                    isOpened = showMoreSettings,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BrowserImportButton(\n    downloadUiState: EditDownloadInputs<*, *, *, *, *, *>,\n) {\n    val downloadPage = downloadUiState.currentDownloadItem.collectAsState().value.downloadPage\n    IconActionButton(\n        MyIcons.earth,\n        Res.string.edit_download_update_from_download_page.asStringSource(),\n        enabled = downloadPage != null,\n        onClick = {\n            downloadPage?.let {\n                URLOpener.openUrl(it)\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun RenderResumeSupport(\n    editDownloadUiChecker: TAEditDownloadInputs,\n    modifier: Modifier,\n) {\n    val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier.height(16.dp)\n    ) {\n        val lineModifier = Modifier\n            .weight(1f)\n            .height(1.dp)\n            .background(myColors.onBackground / 10)\n        Box(lineModifier)\n        val canEditDownload by editDownloadUiChecker.canEdit.collectAsState()\n        AnimatedVisibility(\n            visible = canEditDownload && fileInfo != null,\n        ) {\n            fileInfo?.let { fileInfo ->\n                if (fileInfo.resumeSupport) {\n                    val iconModifier = Modifier\n                        .padding(horizontal = 2.dp)\n                        .size(10.dp)\n                    if (fileInfo.resumeSupport) {\n                        MyIcon(\n                            icon = MyIcons.check,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.success\n                        )\n                    } else {\n                        MyIcon(\n                            icon = MyIcons.clear,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.error,\n                        )\n                    }\n                }\n            }\n        }\n        Box(lineModifier)\n    }\n}\n\n@Composable\nprivate fun MainConfigActionButton(\n    text: String,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    ActionButton(text, modifier, enabled, onClick)\n}\n\n\n@Composable\nfun ConfigActionsButtons(\n    downloadInputs: TAEditDownloadInputs,\n) {\n    val showMoreSettings by downloadInputs.showMoreSettings.collectAsState()\n    val requiresAuth = downloadInputs.responseInfo.collectAsState().value?.requireBasicAuth ?: false\n    Row {\n        IconActionButton(MyIcons.refresh, Res.string.refresh.asStringSource()) {\n            downloadInputs.refresh()\n        }\n        Spacer(Modifier.width(6.dp))\n        BrowserImportButton(downloadInputs)\n        Spacer(Modifier.width(6.dp))\n        IconActionButton(\n            MyIcons.settings,\n            Res.string.settings.asStringSource(),\n            indicateActive = showMoreSettings,\n            requiresAttention = requiresAuth\n        ) {\n            downloadInputs.setShowMoreSettings(true)\n        }\n    }\n}\n\n@Composable\nprivate fun MainActionButtons(\n    component: AndroidEditDownloadComponent,\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    Row {\n        val canEditResult by editDownloadUiChecker.canEditDownloadResult.collectAsState()\n\n        val canEdit = run {\n            val canBeEdited = editDownloadUiChecker.canEdit.collectAsState().value\n            val componentAllowsEdit = component.acceptEdit.collectAsState().value\n            canBeEdited && componentAllowsEdit\n        }\n        val warnings = (canEditResult as? CanEditDownloadResult.CanEdit)?.warnings.orEmpty()\n        Spacer(Modifier.width(8.dp))\n        var showWarningPrompt by remember {\n            mutableStateOf(false)\n        }\n        MainConfigActionButton(\n            text = myStringResource(Res.string.cancel),\n            modifier = Modifier.weight(1f),\n            onClick = {\n                component.onRequestClose()\n            },\n        )\n        Spacer(Modifier.width(mySpacings.mediumSpace))\n        Box(Modifier.weight(1f)) {\n            if (showWarningPrompt) {\n                WarningPrompt(\n                    warnings = warnings,\n                    onClose = {\n                        showWarningPrompt = false\n                    },\n                    onConfirm = {\n                        if (canEdit) {\n                            component.onRequestEdit()\n                        }\n                    }\n                )\n            }\n            PrimaryMainActionButton(\n                text = myStringResource(Res.string.change),\n                modifier = Modifier.fillMaxWidth(),\n                enabled = canEdit,\n                onClick = {\n                    if (warnings.isNotEmpty()) {\n                        showWarningPrompt = true\n                    } else {\n                        component.onRequestEdit()\n                    }\n                },\n            )\n        }\n    }\n}\n\n@Composable\nfun WarningPrompt(\n    warnings: List<CanEditWarnings>,\n    onClose: () -> Unit,\n    onConfirm: () -> Unit,\n) {\n    Popup(\n        popupPositionProvider = rememberMyComponentRectPositionProvider(\n            anchor = Alignment.TopStart,\n            alignment = Alignment.TopEnd,\n        ),\n        onDismissRequest = onClose\n    ) {\n        val shape = myShapes.defaultRounded\n        Box(\n            Modifier\n                .padding(vertical = 4.dp)\n                .widthIn(max = 240.dp)\n                .shadow(24.dp)\n                .clip(shape)\n                .border(1.dp, myColors.surface, shape)\n                .background(myColors.menuGradientBackground)\n                .padding(8.dp)\n        ) {\n            WithContentColor(myColors.onSurface) {\n                Column {\n                    Text(\n                        myStringResource(Res.string.warning),\n                        fontWeight = FontWeight.Bold,\n                        color = myColors.warning\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    warnings.forEach {\n                        Text(\n                            it.asStringSource().rememberString(),\n                            fontSize = myTextSizes.base,\n                        )\n                    }\n                    Text(myStringResource(Res.string.warning_you_may_have_to_restart_the_download_later))\n                    Spacer(Modifier.height(8.dp))\n                    ActionButton(\n                        modifier = Modifier.align(Alignment.CenterHorizontally),\n                        text = myStringResource(Res.string.change_anyway),\n                        onClick = onConfirm,\n                        borderColor = SolidColor(myColors.error),\n                        contentColor = myColors.error,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderFileTypeAndSize(\n    iconProvider: FileIconProvider,\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    val isLinkLoading by editDownloadUiChecker.isLinkLoading.collectAsState()\n    val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()\n    val iconModifier = Modifier.size(mySpacings.iconSize)\n    Box(Modifier) {\n        AnimatedContent(\n            targetState = isLinkLoading,\n            transitionSpec = {\n                fadeIn() togetherWith fadeOut()\n            }\n        ) { loading ->\n            if (loading) {\n                LoadingIndicator(iconModifier)\n            } else {\n                val icon = iconProvider.rememberIcon(editDownloadUiChecker.name.collectAsState().value)\n                AnimatedContent(\n                    fileInfo,\n                ) { fileInfo ->\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(1f) {\n                            if (fileInfo != null) {\n                                if (fileInfo.requiresAuth) {\n                                    MyIcon(\n                                        MyIcons.lock,\n                                        null,\n                                        iconModifier,\n                                        tint = myColors.error\n                                    )\n                                }\n                                MyIcon(\n                                    icon,\n                                    null,\n                                    iconModifier\n                                )\n\n                                val size by editDownloadUiChecker.lengthStringFlow.collectAsState()\n                                Spacer(Modifier.width(8.dp))\n                                Text(\n                                    size.rememberString(),\n                                    fontSize = myTextSizes.sm,\n                                )\n                            } else {\n                                MyIcon(\n                                    icon = MyIcons.question,\n                                    contentDescription = null,\n                                    modifier = iconModifier,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MyTextFieldIcon(\n    icon: IconSource,\n    onClick: (() -> Unit)? = null,\n) {\n    MyIcon(\n        icon, null, Modifier\n            .fillMaxHeight()\n            .ifThen(onClick != null) {\n                pointerHoverIcon(PointerIcon.Default)\n                    .clickable { onClick?.invoke() }\n            }\n            .wrapContentHeight()\n            .padding(horizontal = 8.dp)\n            .size(16.dp))\n}\n\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    modifier: Modifier = Modifier,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        start = {\n            MyTextFieldIcon(MyIcons.link)\n        },\n        end = {\n            MyTextFieldIcon(MyIcons.paste) {\n                setText(\n                    ClipboardUtil.read()\n                        .orEmpty()\n                )\n            }\n        },\n        errorText = errorText\n    )\n}\n\n@Composable\nprivate fun NameTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.name),\n        modifier = Modifier.fillMaxWidth(),\n        errorText = errorText,\n    )\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/enterurl/AndroidEnterNewURLComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.enterurl\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.enterurl.BaseEnterNewURLComponent\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\n\nclass AndroidEnterNewURLComponent(\n    ctx: ComponentContext,\n    config: AndroidEnterNewURLComponent.Config,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    onCloseRequest: () -> Unit,\n    onRequestFinished: (IDownloadCredentials) -> Unit,\n) : BaseEnterNewURLComponent(\n    ctx = ctx,\n    config = config,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    onCloseRequest = onCloseRequest,\n    onRequestFinished = onRequestFinished,\n) {\n    object Config : BaseEnterNewURLComponent.Config\n\n    override val shouldFillWithClipboard: Boolean = false\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/enterurl/EnterURLPage.kt",
    "content": "package com.abdownloadmanager.android.pages.enterurl\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\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.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.TADownloaderInUI\nimport com.abdownloadmanager.shared.pages.enterurl.DownloaderSelection\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun ResponsiveDialogScope.EnterNewURLPage(\n    component: AndroidEnterNewURLComponent,\n    onCloseRequest: () -> Unit,\n) {\n    val linkFocus = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        linkFocus.requestFocus()\n        component.onPageOpen()\n    }\n    val text by component.url.collectAsState()\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(myStringResource(Res.string.new_download))\n                },\n                headerActions = {\n                    DownloaderSelectionSection(component)\n                    TransparentIconActionButton(\n                        MyIcons.close,\n                        contentDescription = Res.string.close.asStringSource(),\n                        onClick = onCloseRequest,\n                    )\n                }\n            )\n        },\n    ) {\n        Column(\n            Modifier\n                .padding(horizontal = mySpacings.mediumSpace)\n                .padding(bottom = mySpacings.mediumSpace)\n        ) {\n            UrlTextField(\n                text = text,\n                setText = component::setURL,\n                modifier = Modifier\n                    .focusRequester(linkFocus)\n                    .fillMaxWidth()\n            )\n            Spacer(Modifier.height(mySpacings.largeSpace))\n            Actions(component, onCloseRequest)\n        }\n    }\n\n}\n\n@Composable\nprivate fun DownloaderSelectionSection(\n    component: AndroidEnterNewURLComponent,\n) {\n    val downloaderSelection = component.downloaderSelection.collectAsState().value\n    val bestDownloader = component.bestDownloader.collectAsState().value\n    var isSelecting by remember { mutableStateOf(false) }\n    val selectedName = rememberDownloaderSelectionItemString(\n        downloaderSelection, bestDownloader\n    )\n    ActionButton(\n        text = selectedName,\n        end = {\n            Row(\n                Modifier.align(Alignment.CenterVertically)\n            ) {\n                Spacer(Modifier.width(4.dp))\n                MyIcon(MyIcons.down, null, Modifier.size(12.dp))\n            }\n        },\n        borderColor = SolidColor(Color.Transparent),\n        onClick = {\n            isSelecting = !isSelecting\n        }\n    )\n\n    if (isSelecting) {\n        Popup(\n            onDismissRequest = {\n                isSelecting = false\n            },\n        ) {\n            val shape = myShapes.defaultRounded\n            Column(\n                Modifier\n                    .clip(shape)\n                    .border(2.dp, myColors.onBackground / 10, shape)\n                    .background(\n                        Brush.linearGradient(\n                            listOf(\n                                myColors.surface,\n                                myColors.background,\n                            )\n                        )\n                    )\n            ) {\n                WithContentColor(myColors.onBackground) {\n                    Column(\n                        Modifier\n                            .widthIn(min = 100.dp, max = 300.dp)\n                            .width(IntrinsicSize.Max)\n                    ) {\n                        component.possibleValues.onEach {\n                            val text = rememberDownloaderSelectionItemString(\n                                it,\n                                bestDownloader,\n                            )\n                            Text(\n                                text,\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        component.selectDownloader(it)\n                                        isSelecting = false\n                                    }\n                                    .padding(vertical = 8.dp, horizontal = 16.dp)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun rememberDownloaderSelectionItemString(\n    downloaderSelection: DownloaderSelection,\n    bestDownloader: TADownloaderInUI?,\n): String {\n    return when (downloaderSelection) {\n        DownloaderSelection.Auto -> {\n            val autoText = myStringResource(Res.string.auto)\n            val bestDownloaderName = bestDownloader?.name?.rememberString()\n            buildString {\n                append(autoText)\n                if (bestDownloader != null) {\n                    append(\" ($bestDownloaderName)\")\n                }\n            }\n        }\n\n        is DownloaderSelection.Fixed -> {\n            downloaderSelection.downloaderInUi.name.rememberString()\n        }\n    }\n}\n\n@Composable\nprivate fun Actions(\n    component: AndroidEnterNewURLComponent,\n    onCloseRequest: () -> Unit,\n) {\n    Row {\n        ActionButton(\n            myStringResource(Res.string.cancel),\n            onClick = onCloseRequest,\n            modifier = Modifier.weight(1f)\n        )\n        Spacer(Modifier.width(8.dp))\n        ActionButton(\n            myStringResource(Res.string.ok),\n            enabled = component.canAdd.collectAsState().value,\n            onClick = {\n                component.newDownloadEntered()\n            },\n            modifier = Modifier.weight(1f)\n        )\n    }\n}\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    modifier: Modifier = Modifier,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        start = {\n            MyIcon(\n                MyIcons.link,\n                null,\n                Modifier\n                    .padding(horizontal = 8.dp)\n                    .size(16.dp),\n            )\n        },\n        end = {\n            MyTextFieldIcon(\n                icon = MyIcons.paste,\n                onClick = {\n                    setText(\n                        ClipboardUtil.read()\n                            .orEmpty()\n                    )\n                }\n            )\n        },\n        errorText = errorText\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/AndroidDownloadActions.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pages.home.AbstractDownloadActions\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\n\nclass AndroidDownloadActions(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    downloadDialogManager: DownloadDialogManager,\n    editDownloadDialogManager: EditDownloadDialogManager,\n    fileChecksumDialogManager: FileChecksumDialogManager,\n    selections: StateFlow<List<IDownloadItemState>>,\n    mainItem: StateFlow<Long?>,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n    openFile: (Long) -> Unit,\n    requestDelete: (List<Long>) -> Unit,\n    onRequestShareFiles: (ids: List<CompletedDownloadItemState>) -> Unit,\n) : AbstractDownloadActions(\n    scope = scope,\n    downloadSystem = downloadSystem,\n    downloadDialogManager = downloadDialogManager,\n    editDownloadDialogManager = editDownloadDialogManager,\n    fileChecksumDialogManager = fileChecksumDialogManager,\n    selections = selections,\n    mainItem = mainItem,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n    openFile = openFile,\n    requestDelete = requestDelete,\n) {\n    val shareAction = simpleAction(\n        title = Res.string.share.asStringSource(),\n        icon = MyIcons.share,\n        checkEnable = selections.mapStateFlow { list ->\n            list.any { it.statusOrFinished() is DownloadJobStatus.Finished }\n        },\n        onActionPerformed = {\n            scope.launch {\n                onRequestShareFiles(selections.value.filterIsInstance<CompletedDownloadItemState>())\n            }\n        }\n    )\n    private val mainOptions = buildMenu {\n        +resumeAction\n        +pauseAction\n        +deleteAction\n        +openDownloadDialogAction\n    }\n    private val extraMenu = buildMenu {\n        +openFileAction\n        +shareAction\n        separator()\n        +reDownloadAction\n        separator()\n        +moveToQueueItems\n        +moveToCategoryAction\n        separator()\n        subMenu(Res.string.copy.asStringSource(), MyIcons.copy) {\n            +(copyDownloadLinkAction)\n            +(copyDownloadCredentialsAsCurlAction)\n        }\n        +editDownloadAction\n        +fileChecksumAction\n    }\n    val androidMenu = buildMenu {\n        mainOptions.forEach {\n            +it\n        }\n        subMenu(\n            title = Res.string.more_options.asStringSource(),\n            icon = MyIcons.menu,\n        ) {\n            extraMenu.forEach { +it }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/BottomNavigation.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.detectHorizontalDragGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.layout.wrapContentHeight\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.home.sections.sort.DownloadSortBy\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\nobject BottomNavigationConstants {\n    const val DEFAULT_ICON_SIZE = 20\n    const val DEFAULT_ICON_PADDING = 16\n}\n\n@Composable\nfun BottomNavigation(\n    modifier: Modifier,\n    component: HomeComponent,\n) {\n    val isShowingSearch by component.isShowingSearch.collectAsState()\n    val shouldShowMainButton = !isShowingSearch\n    val isShowingAddMenu by component.isAddMenuShowing.collectAsState()\n    Row(\n        modifier\n            .padding(16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val shape = myShapes.defaultRounded\n        Box(\n            Modifier\n                .weight(1f)\n                .height(IntrinsicSize.Max)\n                .shadow(4.dp, shape)\n                .clip(shape)\n                .border(1.dp, myColors.onSurface / 0.1f, shape)\n                .background(myColors.surface)\n        ) {\n            AnimatedContent(\n                isShowingSearch,\n            ) {\n                Row(\n                    Modifier\n                        .fillMaxWidth()\n                        .height(IntrinsicSize.Max),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    if (it) {\n                        SearchBox(\n                            text = component.filterState.textToSearch,\n                            onValueChange = {\n                                component.filterState.textToSearch = it\n                            },\n                            modifier = Modifier.fillMaxWidth(),\n                            onDismissRequest = {\n                                component.setIsShowingSearch(false)\n                            }\n                        )\n                    } else {\n                        DefaultItems(component)\n                    }\n                }\n            }\n        }\n        AnimatedVisibility(\n            shouldShowMainButton\n        ) {\n            Row {\n                Spacer(Modifier.width(8.dp))\n                Column {\n                    RenderAddMenu(component)\n                    MainBottonNavigationItem(\n                        icon = MyIcons.add,\n                        contentDescription = Res.string.add.asStringSource(),\n                        onClick = {\n                            component.setIsAddMenuShowing(!isShowingAddMenu)\n                        },\n                        modifier = Modifier,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SearchBox(\n    text: String,\n    onValueChange: (search: String) -> Unit,\n    modifier: Modifier,\n    onDismissRequest: () -> Unit,\n) {\n    BackHandler {\n        onDismissRequest()\n    }\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n    MyTextField(\n        text = text,\n        onTextChange = onValueChange,\n        placeholder = myStringResource(Res.string.search),\n        modifier = modifier\n            .focusRequester(focusRequester),\n        start = {\n            MyIcon(\n                MyIcons.search, null,\n                Modifier\n                    .align(Alignment.CenterVertically)\n                    .padding(BottomNavigationConstants.DEFAULT_ICON_PADDING.dp)\n                    .size(BottomNavigationConstants.DEFAULT_ICON_SIZE.dp),\n                tint = LocalContentColor.current / 0.5f\n            )\n        },\n        end = {\n            MyIcon(\n                MyIcons.clear, null,\n                Modifier\n                    .fillMaxHeight()\n                    .clickable {\n                        if (text.isEmpty()) {\n                            onDismissRequest()\n                        } else {\n                            onValueChange(\"\")\n                        }\n                    }\n                    .padding(BottomNavigationConstants.DEFAULT_ICON_PADDING.dp)\n                    .size(BottomNavigationConstants.DEFAULT_ICON_SIZE.dp)\n                    .wrapContentHeight()\n                    .alpha(0.5f),\n                tint = LocalContentColor.current / 0.5f\n            )\n        }\n    )\n}\n\n@Composable\nfun RowScope.DefaultItems(\n    component: HomeComponent,\n) {\n    val isMainMenuShowing by component.isMainMenuShowing.collectAsState()\n    val isCategoryFilterMenuShowing by component.isCategoryFilterShowing.collectAsState()\n    val isSortMenuShowing by component.isSortMenuShowing.collectAsState()\n\n    val modifier = Modifier\n    Column {\n        RenderMainMenu(component)\n        BottonNavigationItem(\n            icon = MyIcons.menu,\n            contentDescription = Res.string.menu.asStringSource(),\n            onClick = {\n                component.setIsMainMenuShowing(!isMainMenuShowing)\n            },\n            modifier = modifier,\n            isSelected = isMainMenuShowing,\n        )\n    }\n    BottonNavigationItem(\n        icon = MyIcons.search,\n        contentDescription = Res.string.search.asStringSource(),\n        onClick = {\n            component.setIsShowingSearch(true)\n        },\n        modifier = modifier,\n        isSelected = false, // search bar replaced with total bottomNavigation\n    )\n    Spacer(\n        Modifier\n            .fillMaxHeight()\n            .width(1.dp)\n            .background(myColors.onSurface / 0.1f)\n    )\n    val filterMode = component.filterMode.value\n    when (filterMode) {\n        is HomeComponent.FilterMode.Queue -> {\n            QueueIndicator(\n                Modifier\n                    .weight(1f)\n                    .fillMaxHeight(),\n                filterMode,\n                isSelected = isCategoryFilterMenuShowing\n            ) {\n                component.setIsCategoryFilterShowing(!isCategoryFilterMenuShowing)\n            }\n        }\n\n        is HomeComponent.FilterMode.Status -> {\n            FilterStatusIndicator(\n                component,\n                Modifier\n                    .weight(1f)\n                    .fillMaxHeight(),\n                filterMode,\n                isSelected = isCategoryFilterMenuShowing,\n                onClick = {\n                    component.setIsCategoryFilterShowing(!isCategoryFilterMenuShowing)\n                }\n            )\n        }\n    }\n    Spacer(\n        Modifier\n            .fillMaxHeight()\n            .width(1.dp)\n            .background(myColors.onSurface / 0.1f)\n    )\n    when (filterMode) {\n        is HomeComponent.FilterMode.Status -> {\n            SortIndicator(\n                component.selectedSort.collectAsState().value,\n                {\n                    component.setIsSortMenuShowing(!isSortMenuShowing)\n                },\n                modifier = Modifier,\n                isSelected = isSortMenuShowing,\n            )\n        }\n\n        is HomeComponent.FilterMode.Queue -> {\n            ToggleQueueStatus(component, filterMode.queue)\n        }\n    }\n}\n\n@Composable\nfun ToggleQueueStatus(\n    homeComponent: HomeComponent,\n    queueModel: QueueModel\n) {\n    val queue = remember(queueModel.id) {\n        homeComponent.queueManager.getQueue(queueModel.id)\n    }\n    val isQueueActive by queue.activeFlow.collectAsState()\n    val icon: IconSource\n    val contentDescription: StringSource\n    val onClick: () -> Unit\n    if (isQueueActive) {\n        icon = MyIcons.queueStop\n        contentDescription = Res.string.stop_queue.asStringSource()\n        onClick = { homeComponent.stopQueue(queue.id) }\n    } else {\n        icon = MyIcons.queueStart\n        contentDescription = Res.string.start_queue.asStringSource()\n        onClick = { homeComponent.startQueue(queue.id) }\n    }\n    BottonNavigationItem(\n        icon = icon,\n        contentDescription = contentDescription,\n        onClick = onClick,\n        modifier = Modifier,\n        isSelected = isQueueActive,\n    )\n}\n\n@Composable\nprivate fun MainBottonNavigationItem(\n    icon: IconSource,\n    contentDescription: StringSource,\n    onClick: () -> Unit,\n    modifier: Modifier,\n) {\n    Box(modifier) {\n        val shape = myShapes.defaultRounded\n        MyIcon(\n            icon = icon,\n            contentDescription = contentDescription.rememberString(),\n            modifier = Modifier\n                .shadow(4.dp, shape)\n                .border(\n                    1.dp,\n                    myColors.primaryGradient,\n                    shape,\n                )\n                .clip(shape)\n                .background(myColors.surface)\n                .background(\n                    Brush.linearGradient(\n                        myColors.primaryGradientColors.map {\n                            it / 0.25f\n                        }\n                    )\n                )\n                .clickable(onClick = onClick)\n                .padding(BottomNavigationConstants.DEFAULT_ICON_PADDING.dp)\n                .size(BottomNavigationConstants.DEFAULT_ICON_SIZE.dp)\n        )\n    }\n}\n\n@Composable\nprivate fun BottonNavigationItem(\n    icon: IconSource,\n    contentDescription: StringSource,\n    onClick: () -> Unit,\n    modifier: Modifier,\n    isSelected: Boolean,\n) {\n    Box(modifier) {\n        MyIcon(\n            icon = icon,\n            contentDescription = contentDescription.rememberString(),\n            modifier = Modifier\n                .clickable(onClick = onClick)\n                .padding(BottomNavigationConstants.DEFAULT_ICON_PADDING.dp)\n                .size(BottomNavigationConstants.DEFAULT_ICON_SIZE.dp)\n        )\n        BottomNavigationSelectedIndicator(isSelected)\n    }\n}\n\n@Composable\nfun BoxScope.BottomNavigationSelectedIndicator(\n    isSelected: Boolean,\n) {\n    if (isSelected) {\n        Box(\n            Modifier\n                .matchParentSize()\n                .background(\n                    Brush.horizontalGradient(\n                        colors = myColors.primaryGradientColors.map { it / 0.15f }\n                    )\n                )\n        )\n        Box(\n            Modifier\n                .matchParentSize()\n                .wrapContentHeight(Alignment.Bottom)\n                .height(1.dp)\n                .background(\n                    Brush.horizontalGradient(\n                        myColors.primaryGradientColors\n                    )\n                )\n        )\n    }\n}\n\n@Composable\nprivate fun SortIndicator(\n    sort: Sort<DownloadSortBy>,\n    onClick: () -> Unit,\n    modifier: Modifier,\n    isSelected: Boolean,\n) {\n    val totalIcon = BottomNavigationConstants.DEFAULT_ICON_SIZE\n    val iconSize = (totalIcon * 0.7)\n    val sortDirectionSize = totalIcon - iconSize\n    Box(\n        modifier\n            .clickable(onClick = onClick)\n    ) {\n        Column(\n            Modifier\n                .padding(\n                    vertical = BottomNavigationConstants.DEFAULT_ICON_PADDING.dp,\n                    horizontal = (BottomNavigationConstants.DEFAULT_ICON_PADDING + (sortDirectionSize / 2)).dp\n                ),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            val color = LocalContentColor.current\n            val activeAlpha = color / 0.75f\n            if (sort.isAscending()) {\n                MyIcon(\n                    MyIcons.sortUp,\n                    null,\n                    Modifier\n                        .size(sortDirectionSize.dp),\n                    tint = activeAlpha\n                )\n            }\n            MyIcon(\n                sort.cell.icon,\n                null,\n                Modifier.size(iconSize.dp),\n            )\n            if (sort.isDescending()) {\n                MyIcon(\n                    MyIcons.sortDown,\n                    null,\n                    Modifier\n                        .size(sortDirectionSize.dp),\n                    tint = activeAlpha\n                )\n            }\n        }\n        BottomNavigationSelectedIndicator(isSelected)\n    }\n}\n\nprivate fun Modifier.changeStatusOnSwipe(\n    goToPrevious: () -> Unit,\n    goToNext: () -> Unit,\n): Modifier {\n    return pointerInput(Unit) {\n        val threshold = 100f\n        var drag = 0f\n        detectHorizontalDragGestures(\n            onDragEnd = {\n                if (drag > threshold) {\n                    goToPrevious()\n                    drag = 0f\n                } else if (drag < -threshold) {\n                    goToNext()\n                    drag = 0f\n                }\n            }\n        ) { _, dragAmount ->\n            drag += dragAmount\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/DownloadList.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n\n@Composable\nfun DownloadList(\n    downloadList: List<IDownloadItemState>,\n    selectionList: List<Long>,\n    onItemSelectionChange: (Long, Boolean) -> Unit,\n    onItemClicked: (IDownloadItemState) -> Unit,\n    fileIconProvider: FileIconProvider,\n    onNewSelection: (List<Long>) -> Unit,\n    lazyListState: LazyListState,\n    modifier: Modifier,\n    contentPadding: PaddingValues,\n) {\n\n    fun newSelection(ids: List<Long>, isSelected: Boolean) {\n        onNewSelection(ids.filter { isSelected })\n    }\n\n    fun changeAllSelection(isSelected: Boolean) {\n        newSelection(downloadList.map { it.id }, isSelected)\n    }\n\n    val isInSelectMode = selectionList.isNotEmpty()\n    BackHandler(\n        isInSelectMode\n    ) {\n        changeAllSelection(false)\n    }\n    val dividerColor = myColors.onBackground / 0.5f\n    Box {\n        LazyColumn(\n            state = lazyListState,\n            modifier = modifier,\n            contentPadding = contentPadding\n        ) {\n            itemsIndexed(\n                items = downloadList,\n                key = { _, item -> item.id }\n            ) { index, item ->\n                val isFirstItem = index == 0\n                Column(\n                    modifier = Modifier.animateItem()\n                ) {\n                    RenderDownloadItem(\n                        downloadItem = item,\n                        checked = if (isInSelectMode) {\n                            item.id in selectionList\n                        } else {\n                            null\n                        },\n                        onClick = {\n                            if (isInSelectMode) {\n                                val wasInSelections = item.id in selectionList\n                                onItemSelectionChange(item.id, !wasInSelections)\n                            } else {\n                                onItemClicked(item)\n                            }\n                        },\n                        onLongClick = {\n                            val wasInSelections = item.id in selectionList\n                            onItemSelectionChange(item.id, !wasInSelections)\n                        },\n                        fileIconProvider = fileIconProvider,\n                        modifier = Modifier.ifThen(!isFirstItem) {\n                            drawBehind {\n                                drawLine(\n                                    brush = Brush.horizontalGradient(\n                                        listOf(\n                                            Color.Transparent,\n                                            dividerColor,\n                                            Color.Transparent,\n                                        )\n                                    ),\n                                    start = Offset.Zero,\n                                    end = Offset(size.width, 0f)\n                                )\n                            }\n                        },\n                    )\n                }\n            }\n        }\n        if (downloadList.isEmpty()) {\n            Box(\n                Modifier\n                    .padding()\n                    .fillMaxSize()\n            ) {\n                WithContentAlpha(0.75f) {\n                    Text(\n                        myStringResource(Res.string.list_is_empty),\n                        Modifier.align(Alignment.Center),\n                        maxLines = 1,\n                    )\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/FilterStatusIndicator.kt",
    "content": "package com.abdownloadmanager.android.pages.home\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.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.text.style.LineHeightStyle\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.modifiers.autoMirror\n\n\n@Composable\nfun FilterStatusIndicator(\n    component: HomeComponent,\n    modifier: Modifier,\n    filterMode: HomeComponent.FilterMode.Status,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n) {\n    val filter = component.filterState\n    val categoryName = filter.typeCategoryFilter?.name\n    Box(\n        modifier = modifier\n            .clickable(onClick = onClick)\n    ) {\n        StatusFilterSideButton(\n            icon = MyIcons.back,\n            modifier = Modifier\n                .padding(start = 3.dp)\n                .align(Alignment.CenterStart)\n                .autoMirror()\n        )\n        SimplePager(\n            modifier = Modifier\n                .align(Alignment.Center)\n                .fillMaxHeight()\n                .fillMaxWidth(),\n            pageCount = component.allStatuseFilters.size,\n            currentPage = component.currentStatusIndexInList,\n            onPageChanged = {\n                component.switchToNewStatus(it)\n            }\n        ) {\n            val status = component.allStatuseFilters[it]\n            val statusName = status.name.rememberString()\n            Column(\n                Modifier.fillMaxSize(),\n                verticalArrangement = Arrangement.Center,\n            ) {\n                Text(\n                    statusName,\n                    fontSize = myTextSizes.sm,\n                    modifier = Modifier\n                        .fillMaxWidth(),\n                    textAlign = TextAlign.Center,\n                    maxLines = 1,\n                )\n                categoryName?.let { categoryName ->\n                    Spacer(Modifier.height(4.dp))\n                    Text(\n                        categoryName,\n                        fontSize = myTextSizes.xs,\n                        modifier = Modifier.fillMaxWidth(),\n                        textAlign = TextAlign.Center,\n                    )\n                }\n            }\n        }\n        StatusFilterSideButton(\n            icon = MyIcons.next,\n            modifier = Modifier\n                .align(Alignment.CenterEnd)\n                .padding(end = 3.dp)\n                .autoMirror()\n        )\n        BottomNavigationSelectedIndicator(isSelected)\n    }\n}\n\n@Composable\nfun QueueIndicator(\n    modifier: Modifier,\n    filterMode: HomeComponent.FilterMode.Queue,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .clickable(onClick = onClick)\n            .fillMaxSize()\n    ) {\n        Text(\n            filterMode.queue.name,\n            fontSize = myTextSizes.sm,\n            modifier = Modifier\n                .align(Alignment.Center)\n                .fillMaxWidth(),\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        BottomNavigationSelectedIndicator(isSelected)\n    }\n}\n\n@Composable\nprivate fun StatusFilterSideButton(\n    icon: IconSource,\n    modifier: Modifier,\n) {\n    MyIcon(\n        icon = icon,\n        contentDescription = null,\n        modifier = modifier\n            .size(12.dp)\n            .alpha(0.55f),\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/HomeComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.snapshotFlow\nimport com.abdownloadmanager.android.action.createOpenBrowserAction\nimport com.abdownloadmanager.android.pages.enterurl.AndroidEnterNewURLComponent\nimport com.abdownloadmanager.android.pages.home.sections.sort.DownloadSortBy\nimport com.abdownloadmanager.android.storage.HomePageStorage\nimport com.abdownloadmanager.android.util.AppInfo\nimport com.abdownloadmanager.android.util.pagemanager.IBrowserPageManager\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.action.createCheckForUpdateAction\nimport com.abdownloadmanager.shared.action.createDownloadFromClipboardAction\nimport com.abdownloadmanager.shared.action.createDummyExceptionAction\nimport com.abdownloadmanager.shared.action.createDummyMessageAction\nimport com.abdownloadmanager.shared.action.createNewDownloadAction\nimport com.abdownloadmanager.shared.action.createOpenAboutPage\nimport com.abdownloadmanager.shared.action.createOpenBatchDownloadAction\nimport com.abdownloadmanager.shared.action.createOpenOpenSourceThirdPartyLibrariesPage\nimport com.abdownloadmanager.shared.action.createOpenSettingsAction\nimport com.abdownloadmanager.shared.action.createOpenTranslatorsPageAction\nimport com.abdownloadmanager.shared.action.createPerHostSettingsPage\nimport com.abdownloadmanager.shared.action.createStartQueueGroupAction\nimport com.abdownloadmanager.shared.action.createStopAllAction\nimport com.abdownloadmanager.shared.action.createStopQueueGroupAction\nimport com.abdownloadmanager.shared.action.donate\nimport com.abdownloadmanager.shared.action.supportActionGroup\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pagemanager.AboutPageManager\nimport com.abdownloadmanager.shared.pagemanager.AddDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.BatchDownloadPageManager\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EnterNewURLDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.OpenSourceLibrariesPageManager\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pagemanager.SettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.TranslatorsPageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.home.BaseHomeComponent\nimport com.abdownloadmanager.shared.pages.home.category.DefinedStatusCategories\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.ui.widget.sort.sorted\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.activate\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.dismiss\nimport ir.amirab.SelectionUtil\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.queue.activeQueuesFlow\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.io.File\nimport kotlin.collections.plus\n\nclass HomeComponent(\n    componentContext: ComponentContext,\n    downloadItemOpener: DownloadItemOpener,\n    downloadDialogManager: DownloadDialogManager,\n    editDownloadDialogManager: EditDownloadDialogManager,\n    addDownloadDialogManager: AddDownloadDialogManager,\n    fileChecksumDialogManager: FileChecksumDialogManager,\n    queuePageManager: QueuePageManager,\n    categoryDialogManager: CategoryDialogManager,\n    notificationSender: NotificationSender,\n    downloadSystem: DownloadSystem,\n    categoryManager: CategoryManager,\n    queueManager: QueueManager,\n    openSourceLibrariesPageManager: OpenSourceLibrariesPageManager,\n    translatorsPageManager: TranslatorsPageManager,\n    settingsPageManager: SettingsPageManager,\n    perHostSettingsPageManager: PerHostSettingsPageManager,\n    browserPageManager: IBrowserPageManager,\n    aboutPageManager: AboutPageManager,\n    batchDownloadPageManager: BatchDownloadPageManager,\n    defaultCategories: DefaultCategories,\n    fileIconProvider: FileIconProvider,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    private val updateComponent: UpdateComponent,\n    private val homePageStorage: HomePageStorage,\n) : BaseHomeComponent(\n    componentContext,\n    downloadItemOpener,\n    downloadDialogManager,\n    editDownloadDialogManager,\n    addDownloadDialogManager,\n    fileChecksumDialogManager,\n    queuePageManager,\n    categoryDialogManager,\n    notificationSender,\n    downloadSystem,\n    categoryManager,\n    queueManager,\n    defaultCategories,\n    fileIconProvider,\n), EnterNewURLDialogManager {\n    private val enterNewLinkNavigation = SlotNavigation<AndroidEnterNewURLComponent.Config>()\n    val enterNewLinkSlot = childSlot(\n        source = enterNewLinkNavigation,\n        serializer = null,\n        key = \"enterNewLinkSlot\",\n        childFactory = { configuration, context ->\n            AndroidEnterNewURLComponent(\n                ctx = context,\n                config = configuration,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n                onCloseRequest = {\n                    closeEnterNewURLWindow()\n                },\n                onRequestFinished = {\n                    addDownloadDialogManager.openAddDownloadDialog(\n                        links = listOf(AddDownloadCredentialsInUiProps(it))\n                    )\n                }\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override fun closeEnterNewURLWindow() {\n        scope.launch {\n            enterNewLinkNavigation.dismiss()\n        }\n    }\n\n    override fun openEnterNewURLWindow() {\n        scope.launch {\n            enterNewLinkNavigation.activate(AndroidEnterNewURLComponent.Config)\n        }\n    }\n\n    val downloadActions = AndroidDownloadActions(\n        scope = scope,\n        downloadSystem = downloadSystem,\n        downloadDialogManager = downloadDialogManager,\n        editDownloadDialogManager = editDownloadDialogManager,\n        fileChecksumDialogManager = fileChecksumDialogManager,\n        selections = selectionListItems,\n        mainItem = selectionList.mapStateFlow {\n            if (it.size == 1) it[0]\n            else null\n        },\n        queueManager = queueManager,\n        categoryManager = categoryManager,\n        openFile = ::openFile,\n        requestDelete = ::requestDelete,\n        onRequestShareFiles = ::shareFiles,\n    )\n\n    private fun shareFiles(finishedDownloads: List<CompletedDownloadItemState>) {\n        finishedDownloads.mapNotNull {\n            File(it.folder, it.name).takeIf { file -> file.exists() }\n        }.takeIf { it.isNotEmpty() }?.let {\n            sendEffect(Effects.ShareFiles(it))\n        }\n\n    }\n\n    fun onItemClicked(itemState: IDownloadItemState) {\n        scope.launch {\n            if (itemState is ProcessingDownloadItemState) {\n                toggleDownload(itemState)\n                return@launch\n            }\n            downloadItemOpener.openDownloadItem(itemState.id)\n        }\n    }\n\n    suspend fun toggleDownload(dItem: ProcessingDownloadItemState) {\n        when {\n            dItem.canBeResumed() -> downloadSystem.userManualResume(dItem.id)\n            dItem.canBePaused() -> downloadSystem.manualPause(dItem.id)\n        }\n    }\n\n    private val _selectedSort = homePageStorage.sortBy\n    val selectedSort = _selectedSort.asStateFlow()\n    fun setSelectedSort(\n        sort: Sort<DownloadSortBy>\n    ) {\n        if (sort.cell in possibleSorts) {\n            _selectedSort.value = sort\n        }\n    }\n\n    val filterMode = derivedStateOf {\n        val queueFilter = filterState.queueFilter\n        val statusFilter = filterState.statusFilter\n        val categoryFilter = filterState.typeCategoryFilter\n        if (queueFilter != null) {\n            FilterMode.Queue(queueFilter)\n        } else {\n            FilterMode.Status(statusFilter, categoryFilter)\n        }\n    }\n\n    val sortedDownloadList = combine(\n        downloadList,\n        selectedSort,\n        snapshotFlow { filterMode.value },\n    ) { downloadList, sortBy, filterMode ->\n        when (filterMode) {\n            is FilterMode.Status -> {\n                sortBy.sorted(downloadList)\n            }\n\n            is FilterMode.Queue -> {\n                filterMode.queue.queueItems.mapNotNull { id ->\n                    downloadList.find { it.id == id }\n                }\n            }\n        }\n    }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    fun onRequestSelectInside() {\n        SelectionUtil.toggleSelectInside(\n            selectionList = selectionList.value,\n            fullSortedList = sortedDownloadList.value,\n            getId = {\n                it.id\n            }\n        )?.let {\n            newSelection(it)\n        }\n    }\n\n    fun onRequestInvertSelection() {\n        newSelection(\n            SelectionUtil.invertSelection(\n                selectionList = selectionList.value,\n                all = sortedDownloadList.value,\n                getId = { it.id }\n            )\n        )\n    }\n\n    val allStatuseFilters = DefinedStatusCategories.values()\n    val currentStatusIndexInList by derivedStateOf {\n        allStatuseFilters.indexOf(filterState.statusFilter)\n    }\n\n    fun switchToNewStatus(value: Int) {\n        filterState.statusFilter = allStatuseFilters[\n            value.coerceIn(allStatuseFilters.indices)\n        ]\n    }\n\n    private val _isShowingSearch: MutableStateFlow<Boolean> = MutableStateFlow(false)\n    val isShowingSearch = _isShowingSearch.asStateFlow()\n    fun setIsShowingSearch(shown: Boolean) {\n        if (!shown) {\n            filterState.textToSearch = \"\"\n        } else {\n            closePopups()\n        }\n        _isShowingSearch.value = shown\n    }\n\n    private val currentActivePopup = MutableStateFlow<HomePopups?>(null)\n    fun onOverlayClicked() {\n        closePopups()\n    }\n\n    fun closePopups() {\n        currentActivePopup.value = null\n    }\n\n    val isMainMenuShowing = currentActivePopup.mapStateFlow {\n        it == HomePopups.MainMenu\n    }\n\n    fun setIsMainMenuShowing(value: Boolean) {\n        currentActivePopup.value = HomePopups.MainMenu.takeIf { value }\n    }\n\n    val isCategoryFilterShowing = currentActivePopup.mapStateFlow {\n        it == HomePopups.FilterMenu\n    }\n\n    fun setIsCategoryFilterShowing(value: Boolean) {\n        currentActivePopup.value = HomePopups.FilterMenu.takeIf { value }\n    }\n\n    val isSortMenuShowing = currentActivePopup.mapStateFlow {\n        it == HomePopups.SortMenu\n    }\n\n    fun setIsSortMenuShowing(value: Boolean) {\n        currentActivePopup.value = HomePopups.SortMenu.takeIf { value }\n    }\n\n    val isAddMenuShowing = currentActivePopup.mapStateFlow {\n        it == HomePopups.AddMenu\n    }\n\n    fun setIsAddMenuShowing(value: Boolean) {\n        currentActivePopup.value = HomePopups.AddMenu.takeIf { value }\n    }\n\n\n    val activeQueuesFlow = queueManager.activeQueuesFlow()\n        .stateIn(scope, SharingStarted.Eagerly, emptyList())\n    val mainMenu = buildMenu {\n        +createOpenBrowserAction(browserPageManager = browserPageManager)\n        separator()\n        +createStopAllAction(scope, downloadSystem, {}, activeQueuesFlow)\n        separator()\n        subMenu(\n            title = Res.string.delete.asStringSource(),\n            icon = MyIcons.remove\n        ) {\n            item(Res.string.all_missing_files.asStringSource()) {\n                requestDelete(downloadSystem.getListOfDownloadThatMissingFileOrHaveNotProgress().map { it.id })\n            }\n            item(Res.string.all_finished.asStringSource()) {\n                requestDelete(downloadSystem.getFinishedDownloadIds())\n            }\n            item(Res.string.all_unfinished.asStringSource()) {\n                requestDelete(downloadSystem.getUnfinishedDownloadIds())\n            }\n            item(Res.string.entire_list.asStringSource()) {\n                requestDelete(downloadSystem.getAllDownloadIds())\n            }\n        }\n        separator()\n        +createStartQueueGroupAction(scope, queueManager)\n        +createStopQueueGroupAction(scope, activeQueuesFlow)\n        if (AppInfo.isInDebugMode) {\n            separator()\n            +createDummyMessageAction(notificationSender)\n            +createDummyExceptionAction()\n        }\n        separator()\n        +createPerHostSettingsPage(perHostSettingsPageManager = perHostSettingsPageManager)\n        +createOpenSettingsAction(settingsPageManager = settingsPageManager)\n        separator()\n        subMenu(\n            Res.string.help.asStringSource(),\n            MyIcons.question,\n        ) {\n            +supportActionGroup\n            separator()\n            +createOpenOpenSourceThirdPartyLibrariesPage(openSourceLibrariesPageManager = openSourceLibrariesPageManager)\n            +createOpenTranslatorsPageAction(opeTranslatorsPageManager = translatorsPageManager)\n            +donate\n            separator()\n            +createCheckForUpdateAction(updateComponent)\n            +createOpenAboutPage(aboutPageManager)\n        }\n    }\n    val addMenu = buildMenu {\n        +createDownloadFromClipboardAction(addDownloadDialogManager = addDownloadDialogManager)\n        +createNewDownloadAction(enterNewURLDialogManager = enterNewURLDialogManager)\n        +createOpenBatchDownloadAction(batchDownloadPageManager = batchDownloadPageManager)\n    }\n\n    val isOverlayVisible = currentActivePopup.mapStateFlow {\n        it != null\n    }\n\n    val possibleSorts = listOf(\n        DownloadSortBy.DataAdded,\n        DownloadSortBy.Name,\n        DownloadSortBy.Size,\n        DownloadSortBy.Status,\n    )\n\n    fun startQueue(id: Long) {\n        scope.launch {\n            queueManager.getQueue(id).start()\n        }\n    }\n\n    fun stopQueue(id: Long) {\n        scope.launch {\n            queueManager.getQueue(id).stop()\n        }\n    }\n\n    private fun getCurrentDownloadQueue(): DownloadQueue? {\n        val queueId = (filterMode.value as? FilterMode.Queue)?.queue?.id ?: return null\n        return runCatching { queueManager.getQueue(queueId) }.getOrNull()\n    }\n\n    fun reorderQueueItemsUp() {\n        val downloadQueue = getCurrentDownloadQueue() ?: return\n        val itemsToMove = selectionList.value\n        downloadQueue.moveUp(itemsToMove)\n        val queueItems = downloadQueue.queueModel.value.queueItems\n        val firstItemId = queueItems.firstOrNull { itemsToMove.contains(it) }\n        firstItemId?.let {\n            scope.launch {\n                sendEffect(BaseHomeComponent.Effects.Common.ScrollToDownloadItem(it, true))\n            }\n        }\n    }\n\n    fun reorderQueueItemsDown() {\n        val downloadQueue = getCurrentDownloadQueue() ?: return\n        val itemsToMove = selectionList.value\n        downloadQueue.moveDown(itemsToMove)\n        val queueItems = downloadQueue.queueModel.value.queueItems\n        val lastItemId = queueItems.lastOrNull { itemsToMove.contains(it) }\n        lastItemId?.let {\n            sendEffect(BaseHomeComponent.Effects.Common.ScrollToDownloadItem(it, true))\n        }\n    }\n\n    fun reorderQueueItems(fromIndex: Int, toIndex: Int) {\n        val downloadQueue = getCurrentDownloadQueue() ?: return\n        val currentDraggingItem = runCatching {\n            downloadQueue.getQueueItemFromOrder(fromIndex)\n        }.getOrNull()\n        val listOfIds = selectionList.value\n            .let {\n                if (currentDraggingItem != null && !it.contains(currentDraggingItem)) {\n                    it.plus(currentDraggingItem)\n                } else {\n                    it\n                }\n            }\n\n        val delta = toIndex - fromIndex\n        downloadQueue.move(\n            listOfIds, delta\n        )\n        val queueItems = downloadQueue.queueModel.value.queueItems\n        val itemToScroll = if (delta > 0) {\n            queueItems.lastOrNull { listOfIds.contains(it) }\n        } else {\n            queueItems.firstOrNull { listOfIds.contains(it) }\n        }\n        itemToScroll?.let {\n            sendEffect(BaseHomeComponent.Effects.Common.ScrollToDownloadItem(it))\n        }\n    }\n\n    fun removeQueueItems() {\n        val downloadQueue = getCurrentDownloadQueue() ?: return\n        val itemsToRemove = selectionList.value\n        downloadQueue.removeFromQueue(itemsToRemove)\n    }\n\n    fun revealItem(downloadId: Long) {\n        scope.launch {\n            sendEffect(BaseHomeComponent.Effects.Common.ScrollToDownloadItem(downloadId))\n        }\n    }\n\n    override val enterNewURLDialogManager: EnterNewURLDialogManager\n        get() = this\n\n    sealed interface FilterMode {\n        data class Status(\n            val downloadStatus: DownloadStatusCategoryFilter,\n            val category: Category?,\n        ) : FilterMode\n\n        data class Queue(\n            val queue: QueueModel,\n        ) : FilterMode\n    }\n\n    sealed interface Effects : BaseHomeComponent.Effects.PlatformEffects {\n        data class ShareFiles(\n            val files: List<File>,\n        ) : Effects\n    }\n}\n\nsealed interface HomePopups {\n    data object AddMenu : HomePopups\n    data object MainMenu : HomePopups\n    data object SortMenu : HomePopups\n    data object FilterMenu : HomePopups\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/HomePage.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandIn\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\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.statusBarsPadding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.boundsInWindow\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.roundToIntRect\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.android.pages.enterurl.EnterNewURLPage\nimport com.abdownloadmanager.android.pages.home.sections.sort.RenderSortMenu\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSinglePage\nimport com.abdownloadmanager.android.ui.page.PageFooter\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.rememberHeaderAlpha\nimport com.abdownloadmanager.android.util.AndroidIntentUtils\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.home.BaseHomeComponent\nimport com.abdownloadmanager.shared.pages.home.CategoryDeletePromptState\nimport com.abdownloadmanager.shared.pages.home.ConfirmPromptState\nimport com.abdownloadmanager.shared.pages.home.DeletePromptState\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentCustomRectPositionProvider\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.modifiers.silentClickable\nimport ir.amirab.util.compose.resources.myStringResource\nimport kotlinx.coroutines.launch\n\n\n@Composable\nfun HomePage(component: HomeComponent) {\n    val selectionList by component.selectionList.collectAsState()\n    val density = LocalDensity.current\n    var contentPaddingValues by remember {\n        mutableStateOf(PaddingValues.Zero)\n    }\n    val topPaddingInDp = contentPaddingValues.calculateTopPadding()\n    val bottomPaddingInDp = contentPaddingValues.calculateBottomPadding()\n    var showDeletePromptState by remember {\n        mutableStateOf(null as DeletePromptState?)\n    }\n    var showDeleteCategoryPromptState by remember {\n        mutableStateOf(null as CategoryDeletePromptState?)\n    }\n    var showConfirmPrompt by remember {\n        mutableStateOf(null as ConfirmPromptState?)\n    }\n    val lazyListState = rememberLazyListState()\n    val downloadList by component.sortedDownloadList.collectAsState()\n    val coroutineScope = rememberCoroutineScope()\n    val context = LocalContext.current\n    val direction = LocalLayoutDirection.current\n    HandleEffects(component) { effect ->\n        when (effect) {\n            is BaseHomeComponent.Effects.Common -> {\n                when (effect) {\n                    is BaseHomeComponent.Effects.Common.DeleteItems -> {\n                        if (effect.list.isNotEmpty()) {\n                            showDeletePromptState = DeletePromptState(\n                                downloadList = effect.list,\n                                finishedCount = effect.finishedCount,\n                                unfinishedCount = effect.unfinishedCount,\n                            )\n                        }\n                    }\n\n                    is BaseHomeComponent.Effects.Common.DeleteCategory -> {\n                        showDeleteCategoryPromptState = CategoryDeletePromptState(effect.category)\n                    }\n\n                    is BaseHomeComponent.Effects.Common.AutoCategorize -> {\n                        showConfirmPrompt = ConfirmPromptState(\n                            title = Res.string.confirm_auto_categorize_downloads_title.asStringSource(),\n                            description = Res.string.confirm_auto_categorize_downloads_description.asStringSource(),\n                            onConfirm = component::onConfirmAutoCategorize\n                        )\n                    }\n\n                    is BaseHomeComponent.Effects.Common.ResetCategoriesToDefault -> {\n                        showConfirmPrompt = ConfirmPromptState(\n                            title = Res.string.confirm_reset_to_default_categories_title.asStringSource(),\n                            description = Res.string.confirm_reset_to_default_categories_description.asStringSource(),\n                            onConfirm = component::onConfirmResetCategories\n                        )\n                    }\n\n                    is BaseHomeComponent.Effects.Common.ScrollToDownloadItem -> {\n                        val id = effect.downloadId\n                        val positionOrNull = downloadList\n                            .indexOfFirst { it.id == id }\n                            .takeIf { it != -1 }\n\n                        positionOrNull?.let { index ->\n                            if (effect.skipIfVisible) {\n                                val isVisible = lazyListState.layoutInfo.visibleItemsInfo.any {\n                                    it.index == index\n                                }\n                                if (isVisible) {\n                                    return@let\n                                }\n                            }\n                            coroutineScope.launch {\n                                lazyListState.scrollToItem(index)\n                            }\n                        }\n                    }\n                }\n            }\n\n            is HomeComponent.Effects -> {\n                when (effect) {\n                    is HomeComponent.Effects.ShareFiles -> {\n                        AndroidIntentUtils.shareFiles(context, effect.files)\n                    }\n                }\n            }\n            else -> {}\n        }\n    }\n    val isOverlayVisible by component.isOverlayVisible.collectAsState()\n    Box(\n        Modifier.fillMaxSize()\n    ) {\n        PageUi(\n            header = {\n                val headerAlpha = rememberHeaderAlpha(\n                    lazyListState,\n                    density.run {\n                        topPaddingInDp.toPx()\n                    },\n                ).value * 0.75f\n                PageHeader(\n                    modifier = Modifier\n                        .background(\n                            myColors.background.copy(\n                                alpha = headerAlpha\n                            )\n                        )\n                        .statusBarsPadding()\n                        .padding(horizontal = mySpacings.largeSpace),\n                    leadingIcon = {\n                        MyIcon(\n                            MyIcons.appIcon,\n                            null,\n                            Modifier.size(mySpacings.iconSize),\n                        )\n                    },\n                    headerTitle = {\n                        PageTitle(\n                            myStringResource(Res.string.app_title)\n                        )\n                    }\n                )\n            },\n            footer = {\n                PageFooter {\n                    Footer(\n                        Modifier,\n                        component,\n                    )\n                }\n            },\n        ) { params ->\n            contentPaddingValues = params.paddingValues\n            Box {\n                Column(\n                    Modifier\n                        .fillMaxSize()\n                        .background(myColors.background),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    val filterMode by component.filterMode\n                    DownloadList(\n                        downloadList = downloadList,\n                        selectionList = selectionList,\n                        onItemSelectionChange = { id, checked ->\n                            component.onItemSelectionChange(id, checked)\n                        },\n                        onItemClicked = {\n                            component.onItemClicked(it)\n                        },\n                        fileIconProvider = component.fileIconProvider,\n                        onNewSelection = {\n                            component.newSelection(ids = it)\n                        },\n                        lazyListState = lazyListState,\n                        modifier = Modifier\n                            .weight(1f),\n                        contentPadding = params.paddingValues,\n                    )\n                }\n                AnimatedVisibility(\n                    isOverlayVisible,\n                    enter = fadeIn(),\n                    exit = fadeOut(),\n                ) {\n                    Box(\n                        Modifier\n                            .align(Alignment.Center)\n                            .fillMaxSize()\n                            .background(\n                                Color.Black.copy(alpha = 0.5f),\n                            )\n                            .silentClickable {\n                                component.onOverlayClicked()\n                            }\n                    )\n                }\n                Box(\n                    Modifier\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth()\n                        .height(bottomPaddingInDp)\n                        .background(\n                            Brush.verticalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    myColors.background,\n                                )\n                            )\n                        )\n                )\n            }\n        }\n        RenderAboveBottonNavigation(\n            component, Modifier\n                .align(Alignment.BottomCenter)\n                .padding(\n                    start = contentPaddingValues.calculateLeftPadding(direction),\n                    end = contentPaddingValues.calculateRightPadding(direction),\n                )\n                .padding(horizontal = 16.dp)\n                .statusBarsPadding()\n                .padding(bottom = bottomPaddingInDp)\n        )\n    }\n\n    val enterNewURLComponent = component.enterNewLinkSlot.rememberChild()\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(enterNewURLComponent) {\n        if (enterNewURLComponent == null) {\n            state.hide()\n        } else {\n            state.show()\n        }\n    }\n    state.OnFullyDismissed {\n        component.closeEnterNewURLWindow()\n    }\n    val onDismissEnterNewURLComponent = {\n        state.hide()\n    }\n    ResponsiveDialog(\n        state = state,\n        onDismiss = onDismissEnterNewURLComponent\n    ) {\n        enterNewURLComponent?.let {\n            EnterNewURLPage(it, onDismissEnterNewURLComponent)\n        }\n    }\n    RenderPrompts(\n        component = component,\n        showDeletePromptState = showDeletePromptState,\n        showDeleteCategoryPrompt = showDeleteCategoryPromptState,\n        showConfirmPrompt = showConfirmPrompt,\n        closeConfirmPrompt = {\n            showConfirmPrompt = null\n        },\n        closeDeleteCategoryPrompt = {\n            showDeleteCategoryPromptState = null\n        },\n        closeDeletePrompt = {\n            showDeletePromptState = null\n        },\n    )\n}\n\n@Composable\nfun Footer(\n    modifier: Modifier,\n    component: HomeComponent,\n) {\n    Column(\n        modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        val selectionList by component.selectionList.collectAsState()\n        AnimatedContent(\n            selectionList.isNotEmpty(),\n            transitionSpec = {\n                val enter = slideInVertically(tween()) { it } + fadeIn(tween())\n                val exit = slideOutVertically(tween()) { it } + fadeOut(tween())\n                enter togetherWith exit\n            },\n            modifier = Modifier\n        ) { hasSelection ->\n            val commonModifier = Modifier\n                .navigationBarsPadding()\n                .padding(bottom = 8.dp)\n            if (hasSelection) {\n                RenderDownloadOptions(\n                    modifier = commonModifier,\n                    component = component,\n                )\n            } else {\n                BottomNavigation(\n                    commonModifier,\n                    component,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun RenderAboveBottonNavigation(component: HomeComponent, modifier: Modifier) {\n    val enter = fadeIn() + expandIn { IntSize(it.width, 0) }\n    val exit = fadeOut() + shrinkOut { IntSize(it.width, 0) }\n    RenderSortMenu(component, modifier, enter, exit)\n    RenderStatusFilterMenu(component, modifier, enter, exit)\n}\n\n@Composable\nprivate fun RenderDownloadOptions(\n    modifier: Modifier,\n    component: HomeComponent,\n) {\n    val selection by component.selectionList.collectAsState()\n    val downloadList by component.sortedDownloadList.collectAsState()\n    val filterMode by component.filterMode\n    val selectedQueue = (filterMode as? HomeComponent.FilterMode.Queue)?.queue\n    SelectionMenuBox(\n        modifier = modifier,\n        options = component.downloadActions.androidMenu,\n        onRequestClose = component::clearSelection,\n        renderSubMenu = { optionMenuProps, onRequestClose ->\n            val state = rememberResponsiveDialogState(false)\n            val onDismiss = {\n                state.hide()\n            }\n            LaunchedEffect(optionMenuProps) {\n                if (optionMenuProps == null) {\n                    state.hide()\n                } else {\n                    state.show()\n                }\n            }\n            state.OnFullyDismissed {\n                onRequestClose()\n            }\n            optionMenuProps?.let {\n                Popup(\n                    popupPositionProvider = rememberMyComponentCustomRectPositionProvider(\n                        providedAnchorBounds = it.layoutCoordinates.boundsInWindow().roundToIntRect(),\n                        anchor = Alignment.TopEnd,\n                        alignment = Alignment.TopStart,\n                        offset = DpOffset(0.dp, (-4).dp)\n                    ),\n                    properties = PopupProperties(\n                        focusable = true,\n                        dismissOnClickOutside = true,\n                    ),\n                    onDismissRequest = onDismiss,\n                ) {\n                    RenderMenuInSinglePage(\n                        optionMenuProps.subMenu,\n                        onDismiss,\n                        Modifier.width(IntrinsicSize.Max)\n                    )\n                }\n            }\n        },\n        onRequestSelectAll = component::selectAll,\n        onRequestSelectInside = component::onRequestSelectInside,\n        onRequestInvertSelection = component::onRequestInvertSelection,\n        selectionCount = selection.size,\n        total = downloadList.size,\n        queueItemsMenu = selectedQueue?.let {\n            QueueSelectedItemsMenuProps(\n                queueName = it.name,\n                onRequestQueueItemsUp = component::reorderQueueItemsUp,\n                onRequestQueueItemsDown = component::reorderQueueItemsDown,\n                onRequestRemoveItemsFromQueue = component::removeQueueItems,\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/HomePageStateToPersist.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport arrow.optics.optics\nimport com.abdownloadmanager.android.pages.home.sections.sort.DownloadSortBy\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport kotlinx.serialization.Serializable\n\n@optics\n@Serializable\ndata class HomePageStateToPersist(\n    val sortBy: Sort<DownloadSortBy> = Sort<DownloadSortBy>(DownloadSortBy.DataAdded, Sort.DEFAULT_IS_DESCENDING)\n) {\n    companion object {}\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/Prompts.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\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.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\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.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\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.focus.FocusRequester\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.home.CategoryDeletePromptState\nimport com.abdownloadmanager.shared.pages.home.ConfirmPromptState\nimport com.abdownloadmanager.shared.pages.home.DeletePromptState\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun RenderPrompts(\n    component: HomeComponent,\n    showConfirmPrompt: ConfirmPromptState?,\n    closeConfirmPrompt: () -> Unit,\n    showDeleteCategoryPrompt: CategoryDeletePromptState?,\n    closeDeleteCategoryPrompt: () -> Unit,\n    showDeletePromptState: DeletePromptState?,\n    closeDeletePrompt: () -> Unit,\n) {\n    showDeletePromptState?.let {\n        ShowDeletePrompts(\n            deletePromptState = it,\n            onCancel = {\n                closeDeletePrompt()\n            },\n            onConfirm = {\n                closeDeletePrompt()\n                component.confirmDelete(it)\n            })\n    }\n    showDeleteCategoryPrompt?.let {\n        ShowDeleteCategoryPrompt(\n            deletePromptState = it,\n            onCancel = {\n                closeDeleteCategoryPrompt()\n            },\n            onConfirm = {\n                closeDeleteCategoryPrompt()\n                component.onConfirmDeleteCategory(it)\n            })\n    }\n    showConfirmPrompt?.let {\n        ShowConfirmPrompt(\n            promptState = it,\n            onCancel = {\n                closeConfirmPrompt()\n            },\n            onConfirm = {\n                closeConfirmPrompt()\n                showConfirmPrompt.onConfirm.invoke()\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun ShowDeletePrompts(\n    deletePromptState: DeletePromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(Unit) {\n        state.show()\n    }\n    state.OnFullyDismissed(onCancel)\n    // shadow the actual parameter\n    ResponsiveDialog(state, state::hide) {\n        deletePromptState?.let { deletePromptState ->\n            SheetUI(\n                header = {\n                    SheetHeader(\n                        headerTitle = {\n                            SheetTitle(myStringResource(Res.string.confirm_delete_download_items_title))\n                        }\n                    )\n                }\n            ) {\n                Column(\n                    Modifier\n                        .padding(horizontal = mySpacings.largeSpace)\n                        .padding(bottom = mySpacings.largeSpace)\n                ) {\n                    val finishedCount = deletePromptState.finishedCount\n                    val unfinishedCount = deletePromptState.unfinishedCount\n                    Text(\n                        when {\n                            deletePromptState.hasBothFinishedAndUnfinished() -> {\n                                Res.string.confirm_delete_download_finished_and_unfinished_items_description.asStringSourceWithARgs(\n                                    Res.string.confirm_delete_download_finished_and_unfinished_items_description_createArgs(\n                                        finishedCount = finishedCount.toString(),\n                                        unfinishedCount = unfinishedCount.toString(),\n                                    )\n                                )\n                            }\n\n                            deletePromptState.hasUnfinishedDownloads -> {\n                                Res.string.confirm_delete_download_unfinished_items_description.asStringSourceWithARgs(\n                                    Res.string.confirm_delete_download_unfinished_items_description_createArgs(\n                                        count = unfinishedCount.toString(),\n                                    )\n                                )\n                            }\n\n                            else -> {\n                                Res.string.confirm_delete_download_items_description.asStringSourceWithARgs(\n                                    Res.string.confirm_delete_download_items_description_createArgs(\n                                        count = finishedCount.toString()\n                                    ),\n                                )\n                            }\n                        }.rememberString(),\n                        fontSize = myTextSizes.base,\n                        color = myColors.onBackground,\n                    )\n                    if (deletePromptState.hasFinishedDownloads) {\n                        Spacer(Modifier.height(12.dp))\n                        val alsoDeleteFileInteractionSource = remember { MutableInteractionSource() }\n                        Row(\n                            Modifier\n                                .clickable(\n                                    interactionSource = alsoDeleteFileInteractionSource,\n                                    indication = null\n                                ) {\n                                    deletePromptState.alsoDeleteFile = !deletePromptState.alsoDeleteFile\n                                },\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            CheckBox(\n                                value = deletePromptState.alsoDeleteFile,\n                                onValueChange = {\n                                    deletePromptState.alsoDeleteFile = it\n                                },\n                                modifier = Modifier\n                                    // the Row itself is clickable (focusable) so we don't need to focus this checkbox\n                                    // is there a better way?\n                                    .focusProperties { canFocus = false },\n                                interactionSource = alsoDeleteFileInteractionSource,\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(\n                                myStringResource(Res.string.also_delete_file_from_disk),\n                                fontSize = myTextSizes.base,\n                                color = myColors.onBackground,\n                            )\n                        }\n                    }\n                    Spacer(Modifier.height(12.dp))\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        ActionButton(\n                            text = myStringResource(Res.string.cancel),\n                            onClick = onCancel,\n                            modifier = Modifier.weight(1f),\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        ActionButton(\n                            text = myStringResource(Res.string.delete),\n                            onClick = onConfirm,\n                            borderColor = SolidColor(myColors.error),\n                            contentColor = myColors.error,\n                            modifier = Modifier.weight(1f)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ShowConfirmPrompt(\n    promptState: ConfirmPromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(Unit) {\n        state.show()\n    }\n    state.OnFullyDismissed(onCancel)\n    ResponsiveDialog(\n        state, state::hide,\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(promptState.title.rememberString())\n                    }\n                )\n            }\n        ) {\n            Column(\n                Modifier\n                    .padding(horizontal = mySpacings.largeSpace)\n                    .padding(bottom = mySpacings.largeSpace)\n            ) {\n                Text(\n                    text = promptState.description.rememberString(),\n                    fontSize = myTextSizes.base,\n                    color = myColors.onBackground,\n                )\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    ActionButton(\n                        text = myStringResource(Res.string.cancel),\n                        onClick = onCancel,\n                        modifier = Modifier.weight(1f),\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    ActionButton(\n                        text = myStringResource(Res.string.ok),\n                        onClick = onConfirm,\n                        modifier = Modifier.weight(1f),\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ShowDeleteCategoryPrompt(\n    deletePromptState: CategoryDeletePromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(Unit) {\n        state.show()\n    }\n    state.OnFullyDismissed(onCancel)\n    ResponsiveDialog(state, state::hide) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\n                            myStringResource(\n                                Res.string.confirm_delete_category_item_title,\n                                Res.string.confirm_delete_category_item_title_createArgs(\n                                    name = deletePromptState.category.name\n                                ),\n                            )\n                        )\n                    }\n                )\n            }\n        ) {\n            Column(\n                Modifier\n                    .padding(horizontal = mySpacings.largeSpace)\n                    .padding(bottom = mySpacings.largeSpace)\n            ) {\n                Text(\n                    myStringResource(\n                        Res.string.confirm_delete_category_item_description,\n                        Res.string.confirm_delete_category_item_description_createArgs(\n                            value = deletePromptState.category.name\n                        )\n                    ),\n                    fontSize = myTextSizes.base,\n                    color = myColors.onBackground,\n                )\n                Spacer(Modifier.height(12.dp))\n                Text(\n                    myStringResource(Res.string.your_download_will_not_be_deleted),\n                    fontSize = myTextSizes.base,\n                    color = myColors.onBackground,\n                )\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    ActionButton(\n                        text = myStringResource(Res.string.cancel),\n                        onClick = onCancel,\n                        modifier = Modifier.weight(1f)\n                    )\n                    Spacer(Modifier.width(mySpacings.mediumSpace))\n                    ActionButton(\n                        text = myStringResource(Res.string.delete),\n                        onClick = onConfirm,\n                        borderColor = SolidColor(myColors.error),\n                        modifier = Modifier.weight(1f),\n                        contentColor = myColors.error,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/RenderAddMenu.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSinglePage\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\n\n@Composable\nfun RenderAddMenu(component: HomeComponent) {\n    val mainMenuShowing by component.isAddMenuShowing.collectAsState()\n    if (mainMenuShowing) {\n        val onDismissRequest = {\n            component.setIsAddMenuShowing(false)\n        }\n        Popup(\n            popupPositionProvider = rememberMyComponentRectPositionProvider(\n                anchor = Alignment.TopEnd,\n                alignment = Alignment.TopStart,\n                offset = DpOffset(x = 0.dp, y = (-8).dp)\n            ),\n            onDismissRequest = onDismissRequest,\n            properties = PopupProperties(\n                focusable = true,\n            )\n        ) {\n            RenderMenuInSinglePage(\n                menu = component.addMenu,\n                onDismissRequest = onDismissRequest,\n                modifier = Modifier.width(IntrinsicSize.Max),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/RenderDownloadItem.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.animateFloat\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.keyframes\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.combinedClickable\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.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\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.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.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.BlurredEdgeTreatment.Companion.Unbounded\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.singledownloadpage.createStatusString\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.LocalSpeedUnit\nimport com.abdownloadmanager.shared.util.LocalUseRelativeDateTime\nimport com.abdownloadmanager.shared.util.MyDateAndTimeFormats\nimport com.abdownloadmanager.shared.util.TimeNames\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.convertTimeRemainingToHumanReadable\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.formatTime\nimport com.abdownloadmanager.shared.util.prettifyRelativeTime\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.LocalTextStyle\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.isFinished\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.format\nimport kotlinx.datetime.periodUntil\nimport kotlinx.datetime.toLocalDateTime\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\nprivate const val PROGRESS_HEIGHT = 6\n\n@Composable\nfun RenderDownloadItem(\n    checked: Boolean?,\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    downloadItem: IDownloadItemState,\n    fileIconProvider: FileIconProvider,\n    modifier: Modifier\n) {\n    Row(\n        modifier\n    ) {\n        WithContentColor(\n            myColors.onSurface,\n        ) {\n            Column(\n                Modifier\n                    .weight(1f)\n                    .let {\n                        if (checked == true) {\n                            val selectionColor = myColors.onBackground\n                            it.background(myColors.selectionGradient(0.15f, 0.03f, selectionColor))\n                        } else {\n                            it.border(1.dp, Color.Transparent)\n                        }\n                    }\n                    .combinedClickable(\n                        onClick = onClick,\n                        onLongClick = onLongClick,\n                    )\n                    .padding(16.dp)\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    AnimatedVisibility(\n                        checked != null\n                    ) {\n                        Row {\n                            val isChecked = checked ?: false\n                            CheckBox(\n                                value = isChecked,\n                                onValueChange = { onLongClick() },\n                                size = 18.dp,\n                            )\n                            Spacer(Modifier.width(8.dp))\n                        }\n                    }\n                    RenderFileIcon(\n                        downloadItem = downloadItem,\n                        fileIconProvider = fileIconProvider,\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Column(Modifier.weight(1f)) {\n                        Text(\n                            downloadItem.name,\n                            maxLines = 1,\n                        )\n                        Spacer(Modifier.height(8.dp))\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            RenderProgressBar(\n                                downloadItem, Modifier\n                                    .weight(1f)\n                                    .height(PROGRESS_HEIGHT.dp)\n                            )\n                            if (downloadItem is ProcessingDownloadItemState) {\n                                Spacer(Modifier.width(2.dp))\n                                RenderProgressLight(downloadItem)\n                            }\n                        }\n                    }\n                }\n                Spacer(Modifier.height(8.dp))\n                RenderSubTexts(downloadItem)\n            }\n        }\n    }\n}\n\n@Composable\nfun RenderProgressLight(itemState: IDownloadItemState) {\n    val color = when (val status = itemState.statusOrFinished()) {\n        is DownloadJobStatus.IsActive -> {\n            myColors.primaryGradient\n        }\n\n        is DownloadJobStatus.CanBeResumed -> {\n            if (status is DownloadJobStatus.Canceled && !ExceptionUtils.isNormalCancellation(status.e)) {\n                myColors.errorGradient\n            } else {\n                myColors.warningGradient\n            }\n        }\n\n        DownloadJobStatus.Finished -> {\n            myColors.successGradient\n        }\n    }\n    Box(\n        modifier = Modifier\n            .size((PROGRESS_HEIGHT).dp)\n            .background(color, CircleShape),\n    )\n}\n\n@Composable\nfun RenderSubTexts(itemState: IDownloadItemState) {\n    CompositionLocalProvider(\n        LocalTextStyle provides LocalTextStyle.current.copy(fontSize = myTextSizes.xs),\n        LocalContentAlpha provides 0.8f\n    ) {\n        Box(\n            Modifier.fillMaxWidth()\n        ) {\n            RenderLeftSubText(itemState, Modifier.align(Alignment.CenterStart))\n            RenderCenterSubText(itemState, Modifier.align(Alignment.Center))\n            RenderRightSubText(itemState, Modifier.align(Alignment.CenterEnd))\n        }\n    }\n}\n\n@Composable\nprivate fun RenderEta(itemState: ProcessingDownloadItemState, modifier: Modifier) {\n    val eta = remember(itemState.remainingTime) {\n        itemState.remainingTime?.let {\n            convertTimeRemainingToHumanReadable(\n                it,\n                TimeNames.ShortNames\n            )\n        }.orEmpty()\n    }\n    Text(eta, modifier)\n}\n\n@OptIn(ExperimentalTime::class)\n@Composable\nprivate fun RenderAddedTime(itemState: IDownloadItemState, modifier: Modifier) {\n    var dateAddedString by remember { mutableStateOf(\"\") }\n    val useRelativeDateTime = LocalUseRelativeDateTime.current\n\n    LaunchedEffect(\n        itemState.dateAdded,\n        useRelativeDateTime,\n    ) {\n        val instant = Instant.fromEpochMilliseconds(itemState.dateAdded)\n        if (useRelativeDateTime) {\n            while (isActive) {\n                val now = Clock.System.now()\n                val period = now.periodUntil(instant, TimeZone.UTC)\n                val relativeTime = prettifyRelativeTime(period)\n                dateAddedString = relativeTime\n                delay(1000)\n            }\n        } else {\n            val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())\n            dateAddedString = dateTime.format(MyDateAndTimeFormats.fullDateTime)\n        }\n    }\n    Text(dateAddedString, modifier)\n}\n\n@Composable\nfun RenderRightSubText(itemState: IDownloadItemState, modifier: Modifier) {\n    if (itemState is ProcessingDownloadItemState && itemState.status is DownloadJobStatus.IsActive) {\n        RenderEta(itemState, modifier)\n    } else {\n        RenderAddedTime(itemState, modifier)\n    }\n}\n\n@Composable\nfun RenderCenterSubText(itemState: IDownloadItemState, modifier: Modifier) {\n    if (itemState is ProcessingDownloadItemState) {\n        if (itemState.status is DownloadJobStatus.IsActive) {\n            RenderSpeed(itemState.speed, modifier)\n        } else {\n            RenderTextStatus(itemState, modifier)\n        }\n    }\n}\n\n@Composable\nfun RenderTextStatus(itemState: IDownloadItemState, modifier: Modifier) {\n    val status = createStatusString(itemState)\n    Text(\n        status.rememberString(),\n        color = if (itemState.isFinished()) {\n            myColors.success\n        } else {\n            LocalContentColor.current\n        },\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun RenderSpeed(speed: Long, modifier: Modifier) {\n    val target = LocalSpeedUnit.current\n    val speedString = remember(speed) {\n        convertPositiveSpeedToHumanReadable(speed, target)\n    }\n    Text(speedString, modifier)\n}\n\n@Composable\nfun RenderLeftSubText(itemState: IDownloadItemState, modifier: Modifier) {\n    val totalSize = itemState.contentLength\n    val sizeUnit = LocalSizeUnit.current\n    val totalSizeString = remember(totalSize, sizeUnit) {\n        convertPositiveSizeToHumanReadable(totalSize, sizeUnit, true)\n    }\n    val progress = (itemState as? ProcessingDownloadItemState)?.progress\n    val progressStringOrNull = remember(progress, sizeUnit) {\n        progress?.let {\n            convertPositiveSizeToHumanReadable(progress, sizeUnit, true)\n        }\n    }\n    val text = when {\n        else -> {\n            buildString {\n                progressStringOrNull?.let {\n                    append(it.rememberString())\n                    append(\"/\")\n                }\n                append(totalSizeString.rememberString())\n            }\n        }\n    }\n    Text(text, modifier = modifier)\n}\n\n\n@Composable\nprivate fun RenderFileIcon(\n    downloadItem: IDownloadItemState,\n    fileIconProvider: FileIconProvider,\n) {\n    MyIcon(\n        icon = fileIconProvider.rememberIcon(downloadItem.name),\n        contentDescription = null,\n        modifier = Modifier.size(24.dp),\n    )\n}\n\n@Composable\nprivate fun RenderProgressBar(\n    itemState: IDownloadItemState,\n    modifier: Modifier,\n) {\n    val progress = when (itemState) {\n        is CompletedDownloadItemState -> 100\n        is ProcessingDownloadItemState -> when (val status = itemState.status) {\n            is DownloadJobStatus.PreparingFile -> status.percent\n            else -> itemState.percent\n        }\n    }?.let {\n        it / 100f\n    }\n\n    val status = itemState.statusOrFinished()\n    val background = when (status) {\n        is DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.Canceled -> if (ExceptionUtils.isNormalCancellation(status.e)) {\n            myColors.warningGradient\n        } else {\n            myColors.errorGradient\n        }\n\n        DownloadJobStatus.IDLE -> myColors.warningGradient\n        is DownloadJobStatus.Retrying -> myColors.errorGradient\n        DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.PreparingFile -> myColors.infoGradient\n        DownloadJobStatus.Resuming,\n        DownloadJobStatus.Downloading,\n            -> myColors.primaryGradient\n    }\n\n    Box(\n        modifier\n            .fillMaxSize()\n            .clip(myShapes.defaultRounded)\n            .background(myColors.onBackground / 15)\n    ) {\n        progress?.let { progress ->\n            Box(\n                Modifier\n                    .clip(myShapes.defaultRounded)\n                    .background(background)\n                    .fillMaxHeight()\n                    .fillMaxWidth(\n                        animateFloatAsState(\n                            progress,\n                            tween(100, easing = LinearEasing)\n                        ).value\n                    )\n            ) {\n//                if (status is DownloadJobStatus.Downloading) {\n//                    JetFade(\n//                        Modifier\n//                            .fillMaxSize()\n//                            .padding(end = 1.dp)\n//                    )\n//                }\n            }\n        }\n        if (progress == null && status is DownloadJobStatus.IsActive) {\n            val anim = rememberInfiniteTransition()\n            val l = 2000\n            val endPos by anim.animateFloat(\n                0f,\n                1f,\n                infiniteRepeatable(tween(l), RepeatMode.Restart)\n            )\n            val width by anim.animateFloat(\n                6f, 16f, infiniteRepeatable(\n                    keyframes {\n                        durationMillis = l\n                        0f atFraction 0f\n                        0.75f atFraction 0.25f\n                        0f atFraction 1f\n                    },\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n            Box(\n                Modifier\n                    .fillMaxHeight()\n                    .fillMaxWidth(endPos)\n            ) {\n                Box(\n                    Modifier\n                        .background(background)\n                        .fillMaxHeight()\n                        .align(Alignment.CenterEnd)\n                        .fillMaxWidth(width)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun JetFade(modifier: Modifier) {\n    val color = myColors.onContrast / 0.80f\n    Box(\n        modifier,\n        contentAlignment = Alignment.CenterEnd,\n    ) {\n        Box(\n            Modifier\n                .blur(2.dp, edgeTreatment = Unbounded)\n                .aspectRatio(1f)\n                .clip(CircleShape)\n                .background(\n                    Brush.radialGradient(\n                        listOf(color, Color.Transparent)\n                    )\n                )\n        )\n        Box(\n            Modifier\n                .blur(1.dp, edgeTreatment = Unbounded)\n                .fillMaxHeight(0.6f)\n                .fillMaxWidth(0.4f)\n                .background(\n                    Brush.horizontalGradient(\n                        listOf(\n                            Color.Transparent,\n                            color,\n                        )\n                    )\n                )\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/RenderMainMenu.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSinglePage\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\n\n@Composable\nfun RenderMainMenu(component: HomeComponent) {\n    val mainMenuShowing by component.isMainMenuShowing.collectAsState()\n    if (mainMenuShowing) {\n        val onDismissRequest = {\n            component.setIsMainMenuShowing(false)\n        }\n        Popup(\n            popupPositionProvider = rememberMyComponentRectPositionProvider(\n                anchor = Alignment.TopStart,\n                alignment = Alignment.TopEnd,\n                offset = DpOffset(x = 0.dp, y = (-8).dp)\n            ),\n            onDismissRequest = onDismissRequest,\n            properties = PopupProperties(\n                focusable = true,\n            )\n        ) {\n            RenderMenuInSinglePage(\n                menu = component.mainMenu,\n                onDismissRequest = onDismissRequest,\n                modifier = Modifier.width(IntrinsicSize.Max),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/RenderStatusFilterMenu.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Column\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.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.home.sections.Categories\nimport com.abdownloadmanager.android.pages.home.sections.queues.QueuesSection\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.modifiers.hijackClick\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun RenderStatusFilterMenu(\n    component: HomeComponent,\n    modifier: Modifier,\n    enter: EnterTransition,\n    exit: ExitTransition,\n) {\n    val isShowingStatusFilterMenu by component.isCategoryFilterShowing.collectAsState()\n    AnimatedVisibility(\n        modifier = modifier,\n        visible = isShowingStatusFilterMenu,\n        enter = enter,\n        exit = exit,\n    ) {\n        BackHandler {\n            component.setIsCategoryFilterShowing(false)\n        }\n        val shape = myShapes.defaultRounded\n        Column(\n            Modifier\n                .clip(shape)\n                .hijackClick()\n                .background(myColors.surface, shape)\n                .border(1.dp, myColors.onSurface / 0.2f, shape)\n        ) {\n            Column(\n                Modifier\n                    .weight(1f, false)\n                    .verticalScroll(rememberScrollState())\n            ) {\n                Categories(component, Modifier)\n                Spacer(\n                    Modifier\n                        .padding(4.dp)\n                        .height(1.dp)\n                        .fillMaxWidth()\n                        .background(myColors.onSurface / 0.1f)\n                )\n                QueuesSection(component, Modifier)\n            }\n            ActionButton(\n                text = myStringResource(Res.string.ok),\n                onClick = {\n                    component.setIsSortMenuShowing(false)\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(\n                        mySpacings.largeSpace\n                    ),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/SelectedQueueItemsOption.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.text.style.TextAlign\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.asStringSource\n\n@Immutable\ndata class QueueSelectedItemsMenuProps(\n    val queueName: String,\n    val onRequestQueueItemsUp: () -> Unit,\n    val onRequestQueueItemsDown: () -> Unit,\n    val onRequestRemoveItemsFromQueue: () -> Unit,\n)\n\n@Composable\nfun RenderSelectedQueueItemsOption(\n    props: QueueSelectedItemsMenuProps,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        TransparentIconActionButton(\n            icon = MyIcons.up,\n            contentDescription = Res.string.move_up.asStringSource(),\n            onClick = props.onRequestQueueItemsUp,\n            shape = RectangleShape,\n        )\n        TransparentIconActionButton(\n            icon = MyIcons.down,\n            contentDescription = Res.string.move_down.asStringSource(),\n            onClick = props.onRequestQueueItemsDown,\n            shape = RectangleShape,\n        )\n        Text(\n            props.queueName,\n            modifier = Modifier.weight(1f),\n            textAlign = TextAlign.Center,\n        )\n        TransparentIconActionButton(\n            MyIcons.minus,\n            Res.string.remove.asStringSource(),\n            onClick = props.onRequestRemoveItemsFromQueue,\n            shape = RectangleShape,\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/Selection.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.LocalIndication\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.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.layout.wrapContentSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.layout.LayoutCoordinates\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.RenderControlSelections\nimport com.abdownloadmanager.android.ui.SelectionControlButton\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.alphaFlicker\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\n\n@Immutable\ndata class OpenOptionMenuProps(\n    val subMenu: MenuItem.SubMenu,\n    val layoutCoordinates: LayoutCoordinates,\n)\n\n@Composable\nfun SelectionMenuBox(\n    modifier: Modifier,\n    options: List<MenuItem>,\n    queueItemsMenu: QueueSelectedItemsMenuProps?,\n    onRequestSelectAll: () -> Unit,\n    onRequestSelectInside: () -> Unit,\n    onRequestInvertSelection: () -> Unit,\n    selectionCount: Int,\n    total: Int,\n    onRequestClose: () -> Unit,\n    renderSubMenu: @Composable (menu: OpenOptionMenuProps?, close: () -> Unit) -> Unit\n) {\n    var submenuToOpen: OpenOptionMenuProps? by remember { mutableStateOf(null) }\n    val dismissExtraMenu = {\n        submenuToOpen = null\n    }\n    val shape = myShapes.defaultRounded\n    Column(\n        modifier\n            .padding(16.dp)\n            .shadow(4.dp, shape)\n            .clip(shape)\n            .border(1.dp, myColors.onSurface / 0.1f, shape)\n            .background(myColors.surface),\n    ) {\n        AnimatedVisibility(\n            submenuToOpen == null,\n            enter = expandVertically(),\n            exit = shrinkVertically(),\n        ) {\n            RenderDownloadControlSelections(\n                onRequestSelectAll = onRequestSelectAll,\n                onRequestSelectInside = onRequestSelectInside,\n                onRequestInvertSelection = onRequestInvertSelection,\n                selectionCount = selectionCount,\n                total = total,\n                onRequestClose = onRequestClose,\n            )\n        }\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.1f)\n        )\n        RenderSelectionMenuActions(options, {\n            submenuToOpen = it\n        })\n        if (queueItemsMenu != null) {\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .height(1.dp)\n                    .background(myColors.onBackground / 0.1f)\n            )\n            RenderSelectedQueueItemsOption(queueItemsMenu)\n        }\n    }\n    renderSubMenu(submenuToOpen, dismissExtraMenu)\n}\n\n@Composable\nfun RenderDownloadControlSelections(\n    onRequestSelectAll: () -> Unit,\n    onRequestSelectInside: () -> Unit,\n    onRequestInvertSelection: () -> Unit,\n    onRequestClose: () -> Unit,\n    selectionCount: Int,\n    total: Int,\n) {\n    RenderControlSelections(\n        onRequestSelectAll = onRequestSelectAll,\n        onRequestSelectInside = onRequestSelectInside,\n        onRequestInvertSelection = onRequestInvertSelection,\n        selectionCount = selectionCount,\n        total = total,\n        otherActions = {\n            SelectionControlButton(\n                icon = MyIcons.close,\n                contentDescription = Res.string.close.asStringSource(),\n                modifier = Modifier,\n                enabled = true,\n                toggledOff = false,\n                onClick = {\n                    onRequestClose()\n                },\n            )\n        }\n    )\n}\n\n@Composable\nfun RenderSelectionMenuActions(\n    options: List<MenuItem>,\n    onRequestOpenSubmenu: (OpenOptionMenuProps) -> Unit,\n) {\n    Row(\n        Modifier.height(IntrinsicSize.Max)\n    ) {\n        val reactableItemModifier = Modifier\n            .weight(1f)\n        for (action in options) {\n            when (action) {\n                MenuItem.Separator -> {\n                    Spacer(\n                        Modifier\n                            .fillMaxHeight()\n                            .width(1.dp)\n                            .background(myColors.onBackground / 0.2f)\n                    )\n                }\n\n                is MenuItem.SingleItem -> {\n                    val icon = action.icon.collectAsState().value\n                    VerticalMenuOption(\n                        title = action.title.collectAsState().value,\n                        icon = requireNotNull(icon) {\n                            \"use an action that has icon in the HorizontalMenu\"\n                        },\n                        enabled = action.isEnabled.collectAsState().value,\n                        onClick = {\n                            action()\n                        },\n                        modifier = reactableItemModifier,\n                    )\n                }\n\n                is MenuItem.SubMenu -> {\n                    val icon = action.icon.collectAsState().value\n                    VerticalMenuOption(\n                        title = action.title.collectAsState().value,\n                        icon = requireNotNull(icon) {\n                            \"use an action that has icon in the HorizontalMenu\"\n                        },\n                        enabled = action.isEnabled.collectAsState().value,\n                        onClick = {\n                            onRequestOpenSubmenu(OpenOptionMenuProps(action, it))\n                        },\n                        modifier = reactableItemModifier,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun VerticalMenuOption(\n    title: StringSource,\n    icon: IconSource,\n    enabled: Boolean,\n    onClick: (LayoutCoordinates) -> Unit,\n    modifier: Modifier,\n) {\n    SelectionActionButton(\n        icon,\n        contentDescription = title.rememberString(),\n        enabled = enabled,\n        onClick = onClick,\n        modifier = modifier,\n        size = 24.dp,\n        padding = PaddingValues(12.dp),\n    )\n}\n\n\n@Composable\nprivate fun SelectionActionButton(\n    icon: IconSource,\n    contentDescription: String,\n    modifier: Modifier = Modifier,\n    indicateActive: Boolean = false,\n    requiresAttention: Boolean = false,\n    enabled: Boolean = true,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    onClick: (LayoutCoordinates) -> Unit,\n    padding: PaddingValues,\n    size: Dp,\n    shape: Shape = RectangleShape,\n) {\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    val isActiveOrFocused = indicateActive || isFocused\n    var layoutCoordinates by remember { mutableStateOf(null as LayoutCoordinates?) }\n    Box(\n        modifier\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .ifThen(isActiveOrFocused || requiresAttention) {\n                border(\n                    1.dp,\n                    myColors.focusedBorderColor / if (isActiveOrFocused) 1f else alphaFlicker(),\n                    shape\n                )\n            }\n            .clip(shape)\n            .onGloballyPositioned {\n                layoutCoordinates = it\n            }\n            .clickable(\n                enabled = enabled,\n                indication = LocalIndication.current,\n                interactionSource = interactionSource,\n                role = Role.Button,\n                onClick = {\n                    layoutCoordinates?.let(onClick)\n                },\n            )\n            .padding(padding)\n            .wrapContentSize()\n    ) {\n        MyIcon(\n            icon,\n            contentDescription,\n            Modifier\n                .size(size)\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/SimplePager.kt",
    "content": "package com.abdownloadmanager.android.pages.home\n\nimport android.util.Log\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\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.Modifier\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport kotlin.math.roundToInt\n\n@Composable\nfun SimplePager(\n    pageCount: Int,\n    currentPage: Int,\n    onPageChanged: (Int) -> Unit,\n    modifier: Modifier = Modifier,\n    content: @Composable (Int) -> Unit,\n) {\n    val scrollState = rememberScrollState()\n    var pageWidth by remember { mutableStateOf(0) }\n\n    // Snap scroll when page changes externally\n    LaunchedEffect(currentPage, pageWidth) {\n        val target = currentPage * pageWidth\n        if (scrollState.value != target) {\n            scrollState.animateScrollTo(target)\n        }\n    }\n\n    // When user finishes scrolling -> snap to nearest page\n    LaunchedEffect(scrollState.isScrollInProgress) {\n        if (!scrollState.isScrollInProgress && pageWidth > 0) {\n            val pos = scrollState.value\n            val newPage = (pos.toFloat() / pageWidth).roundToInt().coerceIn(0, pageCount - 1)\n\n            val snapPos = newPage * pageWidth\n            if (snapPos != pos) {\n                scrollState.animateScrollTo(snapPos)\n            }\n\n            if (newPage != currentPage) {\n                onPageChanged(newPage)\n            }\n        }\n    }\n\n    Box(\n        modifier\n            .onSizeChanged { pageWidth = it.width }\n            .horizontalScroll(scrollState)\n    ) {\n        Row {\n            repeat(pageCount) { index ->\n                Box(\n                    Modifier\n                        .width(with(LocalDensity.current) { pageWidth.toDp() })\n                        .fillMaxHeight()\n                ) {\n                    content(index)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/sections/Categories.kt",
    "content": "package com.abdownloadmanager.android.pages.home.sections\n\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.LayoutCoordinates\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.android.pages.home.HomeComponent\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSinglePage\nimport com.abdownloadmanager.android.ui.myCombinedClickable\nimport com.abdownloadmanager.shared.pages.home.category.DefinedStatusCategories\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.rememberIconPainter\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.home.CategoryActions\nimport com.abdownloadmanager.shared.ui.widget.rememberMyPopupPositionProviderAtPosition\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport sh.calvin.reorderable.ReorderableColumn\nimport sh.calvin.reorderable.ReorderableListItemScope\n\n@Composable\nfun Categories(\n    component: HomeComponent,\n    modifier: Modifier,\n) {\n    val filterMode = component.filterMode.value\n    val currentStatusFilter = (filterMode as? HomeComponent.FilterMode.Status)?.downloadStatus\n    val currentTypeFilter = (filterMode as? HomeComponent.FilterMode.Status)?.category\n    val categories by component.categoryManager.categoriesFlow.collectAsState()\n    val clipShape = myShapes.defaultRounded\n    val showCategoryOption by component.categoryActions.collectAsState()\n    var popupOffset by remember { mutableStateOf(Offset.Zero) }\n    fun showCategoryOption(item: Category?, offset: Offset) {\n        popupOffset = offset\n        component.showCategoryOptions(item)\n    }\n\n    fun closeCategoryOptions() {\n        component.closeCategoryOptions()\n    }\n    Column(\n        modifier\n            .clip(clipShape)\n            .border(1.dp, myColors.surface, clipShape)\n            .padding(1.dp)\n    ) {\n        var expendedItem: DownloadStatusCategoryFilter? by remember {\n            mutableStateOf(currentStatusFilter.takeIf { currentTypeFilter != null })\n        }\n        for (statusCategoryFilter in DefinedStatusCategories.values()) {\n            StatusFilterItem(\n                isExpanded = expendedItem == statusCategoryFilter,\n                currentTypeCategoryFilter = currentTypeFilter,\n                currentStatusCategoryFilter = currentStatusFilter,\n                statusFilter = statusCategoryFilter,\n                categories = categories,\n                onFilterChange = {\n                    component.onCategoryFilterChange(statusCategoryFilter, it)\n                },\n                onRequestExpand = { expand ->\n                    expendedItem = statusCategoryFilter.takeIf { expand }\n                },\n                onItemsDroppedInCategory = { category, ids ->\n                    component.moveItemsToCategory(category, ids)\n                },\n                onRequestOpenOptionMenu = { category, offset ->\n                    showCategoryOption(category, offset)\n                },\n                onCategoryReorderRequest = { index, delta ->\n                    component.reorderCategory(index, delta)\n                }\n            )\n        }\n    }\n    showCategoryOption?.let {\n        CategoryOption(\n            categoryOptionMenuState = it,\n            onDismiss = {\n                closeCategoryOptions()\n            },\n            offset = popupOffset,\n        )\n    }\n}\n\n@Composable\nfun CategoryOption(\n    categoryOptionMenuState: CategoryActions,\n    onDismiss: () -> Unit,\n    offset: Offset,\n) {\n    ShowOptionsInPopupWithOffset(\n        MenuItem.SubMenu(\n            icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),\n            title = categoryOptionMenuState.categoryItem?.name?.asStringSource()\n                ?: Res.string.categories.asStringSource(),\n            categoryOptionMenuState.menu,\n        ),\n        popupOffset = offset,\n        onDismissRequest = onDismiss,\n    )\n}\n\n@Composable\nprivate fun ShowOptionsInPopupWithOffset(\n    menu: MenuItem.SubMenu,\n    popupOffset: Offset,\n    onDismissRequest: () -> Unit\n) {\n    Popup(\n        popupPositionProvider = rememberMyPopupPositionProviderAtPosition(\n            popupOffset\n        ),\n        onDismissRequest = onDismissRequest\n    ) {\n        RenderMenuInSinglePage(\n            menu, onDismissRequest,\n            Modifier.width(IntrinsicSize.Max),\n        )\n    }\n}\n\n\n@Composable\nprivate fun ReorderableListItemScope.CategoryFilterItem(\n    modifier: Modifier,\n    category: Category,\n    isSelected: Boolean,\n    onItemsDropped: (ids: List<Long>) -> Unit,\n    onClick: () -> Unit,\n    isDragging: Boolean,\n    onRequestOpenOptionMenu: (Category?, Offset) -> Unit,\n) {\n//    var isDraggingOnMe by remember { mutableStateOf(false) }\n    var layoutCoordinates by remember {\n        mutableStateOf<LayoutCoordinates?>(null)\n    }\n    val shouldShowDrag = isSelected || isDragging\n    Box(\n        modifier\n//            .dropDownloadItemsHere(\n//                onDragIn = { isDraggingOnMe = true },\n//                onDragDone = { isDraggingOnMe = false },\n//                onItemsDropped = onItemsDropped,\n//            )\n            .background(\n                if (isSelected) {\n                    myColors.onBackground / 0.05f\n                } else Color.Transparent\n            )\n//            .ifThen(isDraggingOnMe) {\n//                val infiniteTransition = rememberInfiniteTransition()\n//                val color by infiniteTransition.animateColor(\n//                    initialValue = myColors.primary,\n//                    targetValue = myColors.secondary,\n//                    animationSpec = infiniteRepeatable(\n//                        animation = tween(1000, easing = LinearEasing),\n//                        repeatMode = RepeatMode.Reverse\n//                    )\n//                )\n//                border(1.dp, color)\n//            }\n            .heightIn(mySpacings.thumbSize)\n            .onGloballyPositioned {\n                layoutCoordinates = it\n            }\n            .myCombinedClickable(\n                onLongClick = {\n                    onRequestOpenOptionMenu(\n                        category,\n                        layoutCoordinates?.localToWindow(it)\n                            ?: Offset.Zero\n                    )\n                },\n                onClick = {\n                    onClick()\n                },\n                interactionSource = remember { MutableInteractionSource() },\n                indication = LocalIndication.current,\n            ),\n        contentAlignment = Alignment.CenterStart,\n    ) {\n//        if (isDraggingOnMe) {\n//            DelayedTooltipPopup(\n//                {},\n//                myStringResource(Res.string.move_to_this_category),\n//            )\n//        }\n        Row(\n            modifier = Modifier\n                .padding(start = 24.dp)\n                .padding(horizontal = 4.dp, vertical = 6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                val iconPainter = category.rememberIconPainter()\n                MyIcon(\n                    iconPainter ?: MyIcons.folder,\n                    null,\n                    Modifier.size(mySpacings.iconSize),\n                )\n                Spacer(Modifier.width(8.dp))\n                Text(\n                    category.name,\n                    Modifier.weight(1f),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.base\n                )\n                AnimatedVisibility(shouldShowDrag) {\n                    MyIcon(\n                        MyIcons.grip,\n                        null,\n                        Modifier\n                            .draggableHandle()\n                            .size(mySpacings.iconSize)\n                            .alpha(if (isDragging) 1f else 0.5f),\n                    )\n                }\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier.align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n    }\n}\n\n@Composable\nfun StatusFilterItem(\n    isExpanded: Boolean,\n    onRequestExpand: (Boolean) -> Unit,\n    currentTypeCategoryFilter: Category?,\n    currentStatusCategoryFilter: DownloadStatusCategoryFilter?,\n    statusFilter: DownloadStatusCategoryFilter,\n    categories: List<Category>,\n    onCategoryReorderRequest: (index: Int, delta: Int) -> Unit,\n    onItemsDroppedInCategory: (category: Category, downloadIds: List<Long>) -> Unit,\n    onFilterChange: (\n        typeFilter: Category?,\n    ) -> Unit,\n    onRequestOpenOptionMenu: (Category?, Offset) -> Unit,\n) {\n    val isStatusSelected = currentStatusCategoryFilter == statusFilter\n    val isSelected = isStatusSelected && currentTypeCategoryFilter == null\n    var layoutCoordinates by remember {\n        mutableStateOf<LayoutCoordinates?>(null)\n    }\n    ExpandableItem(\n        modifier = Modifier,\n        isExpanded = isExpanded,\n        header = {\n            Box(\n                Modifier\n                    .height(IntrinsicSize.Max)\n                    .heightIn(mySpacings.thumbSize)\n                    .background(\n                        if (isSelected) {\n                            myColors.onBackground / 0.05f\n                        } else Color.Transparent\n                    )\n                    .onGloballyPositioned {\n                        layoutCoordinates = it\n                    }\n                    .myCombinedClickable(\n                        onClick = {\n                            onRequestExpand(!isExpanded)\n                            onFilterChange(null)\n                        },\n                        onLongClick = {\n                            onRequestOpenOptionMenu(\n                                null,\n                                layoutCoordinates?.localToWindow(it) ?: Offset.Zero\n                            )\n                        },\n                        interactionSource = remember { MutableInteractionSource() },\n                        indication = LocalIndication.current,\n                    )\n            ) {\n                Row(\n                    Modifier\n                        .padding(vertical = 4.dp)\n                        .padding(start = 16.dp)\n                        .padding(end = 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                        MyIcon(\n                            statusFilter.icon,\n                            null,\n                            Modifier.size(mySpacings.iconSize)\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        Text(\n                            statusFilter.name.rememberString(),\n                            Modifier.weight(1f),\n                            fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                            fontSize = myTextSizes.lg,\n                            overflow = TextOverflow.Ellipsis,\n                            maxLines = 1,\n                        )\n                        MyIcon(\n                            MyIcons.up, null, Modifier\n                                .fillMaxHeight()\n                                .wrapContentHeight()\n                                .clip(CircleShape)\n                                .clickable {\n                                    onRequestExpand(!isExpanded)\n                                }\n                                .padding(6.dp)\n                                .size(16.dp)\n                                .rotate(if (isExpanded) 0f else 180f)\n                        )\n                    }\n                }\n                AnimatedVisibility(\n                    isSelected,\n                    modifier = Modifier.align(Alignment.CenterStart),\n                    enter = scaleIn(),\n                    exit = scaleOut(),\n                ) {\n                    Spacer(\n                        Modifier\n                            .height(16.dp)\n                            .width(3.dp)\n                            .clip(\n                                RoundedCornerShape(\n                                    topStart = 0.dp,\n                                    bottomStart = 0.dp,\n                                    bottomEnd = 12.dp,\n                                    topEnd = 12.dp,\n                                )\n                            )\n                            .background(myColors.primary)\n                    )\n                }\n            }\n        },\n        body = {\n            ReorderableColumn(\n                list = categories,\n                onSettle = { from, to ->\n                    onCategoryReorderRequest(from, to - from)\n                },\n            ) { index, category, isDragging ->\n                key(category.id) {\n                    ReorderableItem {\n                        CategoryFilterItem(\n                            modifier = Modifier,\n                            category = category,\n                            isSelected = isStatusSelected && currentTypeCategoryFilter == category,\n                            onItemsDropped = {\n                                onItemsDroppedInCategory(category, it)\n                            },\n                            onClick = {\n                                onFilterChange(category)\n                            },\n                            onRequestOpenOptionMenu = onRequestOpenOptionMenu,\n                            isDragging = isDragging,\n                        )\n                    }\n                    Spacer(Modifier.height(2.dp))\n                }\n            }\n        }\n    )\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/sections/queues/Queues.kt",
    "content": "package com.abdownloadmanager.android.pages.home.sections.queues\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.LocalIndication\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.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\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.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\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.draw.rotate\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.LayoutCoordinates\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.android.pages.home.HomeComponent\nimport com.abdownloadmanager.android.ui.menu.RenderMenuInSinglePage\nimport com.abdownloadmanager.android.ui.myCombinedClickable\nimport com.abdownloadmanager.shared.pages.home.queue.QueueActions\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.rememberMyPopupPositionProviderAtPosition\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\ninternal fun QueuesSection(\n    component: HomeComponent,\n    modifier: Modifier,\n) {\n\n    val currentSelectedQueue = component.filterState.queueFilter\n    val filterMode by component.filterMode\n    val queues by component.queueManager.queues.collectAsState()\n    val clipShape = myShapes.defaultRounded\n    val showQueueOption by component.queueActions.collectAsState()\n    var lastPointerPosition by remember { mutableStateOf(Offset.Zero) }\n    fun showQueueOption(downloadQueue: DownloadQueue?, pointerPosition: Offset) {\n        lastPointerPosition = pointerPosition\n        component.showCategoryOptions(downloadQueue)\n    }\n\n    fun closeQueueOptions() {\n        component.closeQueueOptions()\n    }\n\n    var isExpanded by remember {\n        mutableStateOf(\n            filterMode is HomeComponent.FilterMode.Queue\n        )\n    }\n    Column(\n        modifier\n            .border(1.dp, myColors.surface, clipShape)\n            .clip(clipShape)\n            .padding(1.dp),\n    ) {\n        var layoutCoordinates by remember {\n            mutableStateOf(null as LayoutCoordinates?)\n        }\n        ExpandableItem(\n            isExpanded = isExpanded,\n            modifier = Modifier,\n            header = {\n                Box(\n                    Modifier\n                        .height(IntrinsicSize.Max)\n                        .heightIn(mySpacings.thumbSize)\n                        .onGloballyPositioned {\n                            layoutCoordinates = it\n                        }\n                        .myCombinedClickable(\n                            onClick = {\n                                isExpanded = !isExpanded\n                            },\n                            onLongClick = {\n                                showQueueOption(null, layoutCoordinates?.localToWindow(it) ?: Offset.Zero)\n                            },\n                            interactionSource = remember { MutableInteractionSource() },\n                            indication = LocalIndication.current,\n                        )\n                ) {\n                    Row(\n                        Modifier\n                            .padding(vertical = 4.dp)\n                            .padding(start = 16.dp)\n                            .padding(end = 8.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(0.75f) {\n                            MyIcon(\n                                MyIcons.queue,\n                                null,\n                                Modifier.size(mySpacings.iconSize)\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(\n                                myStringResource(Res.string.queues),\n                                Modifier.weight(1f),\n                                fontWeight = FontWeight.Normal,\n                                fontSize = myTextSizes.lg,\n                                overflow = TextOverflow.Ellipsis,\n                                maxLines = 1,\n                            )\n                            MyIcon(\n                                MyIcons.up, null, Modifier\n                                    .fillMaxHeight()\n                                    .wrapContentHeight()\n                                    .clip(CircleShape)\n                                    .clickable {\n                                        isExpanded = !isExpanded\n                                    }\n                                    .padding(6.dp)\n                                    .size(16.dp)\n                                    .rotate(if (isExpanded) 0f else 180f))\n                        }\n                    }\n                }\n            },\n            body = {\n                Column {\n                    queues.forEachIndexed { index, queue ->\n                        key(queue.id) {\n                            QueueFilterItem(\n                                modifier = Modifier,\n                                isSelected = currentSelectedQueue?.id == queue.id,\n                                onSelect = {\n                                    component.onQueueFilterChange(queue.queueModel.value)\n                                },\n//                                onItemsDroppedInQueue = { downloadIds ->\n//                                    component.moveItemsToQueue(queue, downloadIds)\n//                                },\n                                queueModel = queue.queueModel.collectAsState().value,\n                                isActive = queue.activeFlow.collectAsState().value,\n                                showQueueOption = { position ->\n                                    showQueueOption(queue, position)\n                                }\n//                                parentShape = clipShape,\n//                                isLast = queues.lastIndex == index\n                            )\n                        }\n                    }\n                }\n            },\n        )\n    }\n    showQueueOption?.let {\n        QueueOption(\n            queueOptionMenuState = it,\n            onDismiss = {\n                closeQueueOptions()\n            },\n            position = lastPointerPosition,\n        )\n    }\n}\n\n@Composable\nprivate fun QueueFilterItem(\n    isSelected: Boolean,\n    onSelect: () -> Unit,\n//    onItemsDroppedInQueue: (List<Long>) -> Unit,\n    queueModel: QueueModel,\n    isActive: Boolean,\n    modifier: Modifier = Modifier,\n    showQueueOption: (offset: Offset) -> Unit,\n    // I add this to properly create border on drag when the item is in the last position\n//    isLast: Boolean,\n//    parentShape: RoundedCornerShape,\n) {\n//    var isDraggingOnMe by remember { mutableStateOf(false) }\n    var layoutCoordinates by remember { mutableStateOf(null as LayoutCoordinates?) }\n    Box(\n        modifier\n//            .dropDownloadItemsHere(\n//                onDragIn = { isDraggingOnMe = true },\n//                onDragDone = { isDraggingOnMe = false },\n//                onItemsDropped = onItemsDroppedInQueue,\n//            )\n            .background(\n                if (isSelected) {\n                    myColors.onBackground / 0.05f\n                } else Color.Transparent\n            )\n//            .ifThen(isDraggingOnMe) {\n//                val infiniteTransition = rememberInfiniteTransition()\n//                val color by infiniteTransition.animateColor(\n//                    initialValue = myColors.primary,\n//                    targetValue = myColors.secondary,\n//                    animationSpec = infiniteRepeatable(\n//                        animation = tween(1000, easing = LinearEasing),\n//                        repeatMode = RepeatMode.Reverse\n//                    )\n//                )\n//                val shape = RoundedCornerShape(0.dp).let {\n//                    when {\n//                        isLast -> it.copy(\n//                            bottomStart = parentShape.bottomStart,\n//                            bottomEnd = parentShape.bottomEnd,\n//                        )\n//\n//                        else -> it\n//                    }\n//                }\n//                border(1.dp, color, shape)\n//            }\n            .onGloballyPositioned {\n                layoutCoordinates = it\n            }\n            .myCombinedClickable(\n                onClick = {\n                    onSelect()\n                },\n                onLongClick = {\n                    showQueueOption(layoutCoordinates?.localToWindow(it) ?: Offset.Zero)\n                },\n                interactionSource = remember { MutableInteractionSource() },\n                indication = LocalIndication.current,\n            )\n    ) {\n//        if (isDraggingOnMe) {\n//            DelayedTooltipPopup(\n//                {},\n//                myStringResource(Res.string.move_to_this_queue),\n//            )\n//        }\n        Row(\n            Modifier\n                .heightIn(mySpacings.thumbSize)\n                .padding(start = 24.dp)\n                .padding(end = 8.dp)\n                .padding(horizontal = 4.dp, vertical = 6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                MyIcon(\n                    MyIcons.folder,\n                    null,\n                    Modifier.size(mySpacings.iconSize)\n                )\n                Spacer(Modifier.width(8.dp))\n                Text(\n                    queueModel.name,\n                    Modifier.weight(1f),\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.lg,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                )\n                val counterColor = animateColorAsState(\n                    if (isActive) {\n                        myColors.success\n                    } else {\n                        LocalContentColor.current / LocalContentAlpha.current\n                    }\n                ).value\n                Text(\n                    text = \"${queueModel.queueItems.size}\",\n                    modifier = Modifier.padding(horizontal = 6.dp),\n                    color = counterColor\n                )\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier.align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun QueueOption(\n    queueOptionMenuState: QueueActions,\n    onDismiss: () -> Unit,\n    position: Offset,\n) {\n    ShowOptionsInPopup(\n        MenuItem.SubMenu(\n            icon = MyIcons.queue,\n            title = queueOptionMenuState.mainQueueModel?.name?.asStringSource() ?: Res.string.queues.asStringSource(),\n            items = queueOptionMenuState.menu,\n        ),\n        onDismiss,\n        position,\n    )\n}\n\n@Composable\nprivate fun ShowOptionsInPopup(\n    subMenu: MenuItem.SubMenu,\n    onDismiss: () -> Unit,\n    position: Offset,\n) {\n    Popup(\n        popupPositionProvider = rememberMyPopupPositionProviderAtPosition(position),\n        onDismissRequest = onDismiss,\n    ) {\n        RenderMenuInSinglePage(\n            menu = subMenu,\n            onDismissRequest = onDismiss,\n            modifier = Modifier.width(IntrinsicSize.Max)\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/sections/sort/DownloadSortBy.kt",
    "content": "package com.abdownloadmanager.android.pages.home.sections.sort\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.sort.ComparatorProvider\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed class DownloadSortBy(\n    val selector: (IDownloadItemState) -> Comparable<*>,\n    val icon: IconSource,\n    val name: StringSource,\n) : ComparatorProvider<IDownloadItemState> {\n    override fun comparator(): Comparator<IDownloadItemState> {\n        return compareBy(selector)\n    }\n\n    @Serializable\n    @SerialName(\"name\")\n    object Name : DownloadSortBy(\n        selector = { it.name },\n        icon = MyIcons.alphabet,\n        name = Res.string.name.asStringSource(),\n    )\n\n    @Serializable\n    @SerialName(\"dateAdded\")\n    object DataAdded : DownloadSortBy(\n        selector = { it.dateAdded },\n        icon = MyIcons.clock,\n        name = Res.string.date_added.asStringSource(),\n    )\n\n    @Serializable\n    @SerialName(\"status\")\n    data object Status : DownloadSortBy(\n        selector = { it.statusOrFinished().order },\n        icon = MyIcons.info,\n        name = Res.string.status.asStringSource(),\n    )\n\n    @Serializable\n    @SerialName(\"size\")\n    data object Size : DownloadSortBy(\n        selector = { it.contentLength },\n        icon = MyIcons.data,\n        name = Res.string.size.asStringSource(),\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/home/sections/sort/RenderSortMenu.kt",
    "content": "package com.abdownloadmanager.android.pages.home.sections.sort\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\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.heightIn\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.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.home.HomeComponent\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.ui.widget.sort.SortIndicatorMode\nimport com.abdownloadmanager.shared.ui.widget.sort.isDescending\nimport com.abdownloadmanager.shared.ui.widget.sort.toSortIndicatorMode\nimport com.abdownloadmanager.shared.ui.widget.sort.next\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.modifiers.hijackClick\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\nfun RenderSortMenu(\n    component: HomeComponent,\n    modifier: Modifier,\n    enter: EnterTransition,\n    exit: ExitTransition,\n) {\n    val isShowingSortMenu by component.isSortMenuShowing.collectAsState()\n    AnimatedVisibility(\n        modifier = modifier,\n        visible = isShowingSortMenu,\n        enter = enter,\n        exit = exit,\n    ) {\n        BackHandler {\n            component.setIsSortMenuShowing(false)\n        }\n        val shape = myShapes.defaultRounded\n        Column(\n            Modifier\n                .clip(shape)\n                .hijackClick()\n                .background(myColors.surface, shape)\n                .border(1.dp, myColors.onSurface / 0.2f, shape)\n        ) {\n            val selectedSort by component.selectedSort.collectAsState()\n            Text(\n                text = myStringResource(Res.string.sort_by),\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.xl,\n                modifier = Modifier.padding(\n                    mySpacings.largeSpace\n                )\n            )\n            Column(\n                Modifier\n                    .weight(1f, false)\n                    .verticalScroll(rememberScrollState()),\n            ) {\n                for (downloadSortBy in component.possibleSorts) {\n                    val isSelected = downloadSortBy == selectedSort.cell\n                    key(downloadSortBy) {\n                        SortItem(\n                            downloadSortBy,\n                            sortIndicatorMode = if (isSelected) {\n                                selectedSort.toSortIndicatorMode()\n                            } else {\n                                SortIndicatorMode.None\n                            },\n                            onSortChange = {\n                                component.setSelectedSort(\n                                    Sort(\n                                        cell = downloadSortBy,\n                                        isDescending = it.isDescending()\n                                    )\n                                )\n                            },\n                            Modifier.fillMaxWidth(),\n                        )\n                    }\n                }\n            }\n            ActionButton(\n                text = myStringResource(Res.string.ok),\n                onClick = {\n                    component.setIsSortMenuShowing(false)\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(\n                        mySpacings.largeSpace\n                    ),\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SortItem(\n    sortBy: DownloadSortBy,\n    sortIndicatorMode: SortIndicatorMode,\n    onSortChange: (SortIndicatorMode) -> Unit,\n    modifier: Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .clickable {\n                onSortChange(sortIndicatorMode.next())\n            }\n            .ifThen(sortIndicatorMode == SortIndicatorMode.None) {\n                alpha(0.6f)\n            }\n            .heightIn(min = mySpacings.thumbSize)\n            .padding(horizontal = mySpacings.largeSpace),\n    ) {\n        MyIcon(sortBy.icon, null, Modifier.size(24.dp))\n        Spacer(Modifier.width(8.dp))\n        Text(\n            sortBy.name.rememberString(),\n            Modifier.weight(1f)\n        )\n        Spacer(Modifier.width(8.dp))\n        RenderSortIndicatorMode(sortIndicatorMode)\n    }\n}\n\n@Composable\nfun RenderSortIndicatorMode(sortIndicatorMode: SortIndicatorMode) {\n    val icon = when (sortIndicatorMode) {\n        SortIndicatorMode.None -> null\n        SortIndicatorMode.Ascending -> MyIcons.sortUp\n        SortIndicatorMode.Descending -> MyIcons.sortDown\n    }\n    icon?.let {\n        MyIcon(\n            it,\n            null,\n            Modifier\n                .size(16.dp)\n                .alpha(0.75f)\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/newqueue/NewQueue.kt",
    "content": "package com.abdownloadmanager.android.pages.newqueue\n\nimport androidx.compose.runtime.Composable\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun NewQueueSheet(\n    onQueueCreate: (String) -> Unit,\n    isOpened: Boolean,\n    onCloseRequest: () -> Unit,\n) {\n    SheetInput(\n        title = Res.string.add_new_queue.asStringSource(),\n        validate = { it.isNotEmpty() },\n        isOpened = isOpened,\n        initialValue = { \"\" },\n        onDismiss = onCloseRequest,\n        onConfirm = onQueueCreate,\n        inputContent = {\n            MyTextField(\n                modifier = it.modifier,\n                text = it.editingValue,\n                onTextChange = it.setEditingValue,\n                placeholder = myStringResource(Res.string.queue_name),\n            )\n        },\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/StartUpPageTemplate.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.layout.wrapContentWidth\nimport androidx.compose.foundation.shape.RoundedCornerShape\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.draw.shadow\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.layout.RelativeAlignment\n\n@Composable\nfun StartUpPageTemplate(\n    header: @Composable () -> Unit,\n    actions: @Composable () -> Unit,\n    content: @Composable () -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .background(myColors.background)\n            .statusBarsPadding()\n            .navigationBarsPadding(),\n\n        ) {\n        header()\n        Box(\n            modifier = Modifier.weight(1f),\n        ) {\n            content()\n        }\n        actions()\n    }\n}\n\n@Composable\nfun StartUpPageHeader(\n    title: StringSource,\n    onBackPressed: (() -> Unit)? = null,\n) {\n    BackHandler(onBackPressed != null) {\n        onBackPressed?.invoke()\n    }\n    Row(\n        modifier = Modifier.padding(\n            horizontal = mySpacings.largeSpace, vertical = mySpacings.largeSpace\n        ),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        var backButtonWidth by remember { mutableStateOf(0) }\n        if (onBackPressed != null) {\n            TransparentIconActionButton(\n                MyIcons.back,\n                contentDescription = Res.string.back.asStringSource(),\n                onClick = { onBackPressed() },\n                modifier = Modifier.onSizeChanged {\n                    backButtonWidth = it.width\n                }\n            )\n        } else {\n            backButtonWidth = 0\n        }\n        Text(\n            text = title.rememberString(),\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.xl,\n            modifier = Modifier\n                .weight(1f)\n                .wrapContentWidth(\n                    RelativeAlignment.Horizontal(\n                        mainAlignment = Alignment.CenterHorizontally,\n                        relative = -backButtonWidth / 2\n                    ),\n                )\n        )\n    }\n}\n\n@Composable\nfun StartUpPageActions(\n    content: @Composable () -> Unit\n) {\n    Box(\n        Modifier\n            .padding(horizontal = mySpacings.largeSpace, vertical = mySpacings.largeSpace)\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun AppIcon(\n    modifier: Modifier = Modifier,\n    size: Dp = 52.dp,\n) {\n    val shape = RoundedCornerShape(24.dp)\n    Image(\n        MyIcons.appIcon.rememberPainter(),\n        null,\n        modifier\n            .shadow(12.dp, shape, spotColor = myColors.primary)\n            .clip(shape)\n            .border(\n                1.dp,\n                Brush.linearGradient(\n                    listOf(myColors.primary, myColors.secondary)\n                ),\n                shape\n            )\n            .background(myColors.surface)\n            .padding(16.dp)\n            .size(size)\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/initialsetup/InitialSetupComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.initialsetup\n\nimport com.abdownloadmanager.shared.settings.CommonSettings\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\n\nclass InitialSetupComponent(\n    ctx: ComponentContext,\n    private val languageManager: LanguageManager,\n    private val themeManager: ThemeManager,\n    private val onFinish: () -> Unit\n) : BaseComponent(ctx) {\n    val configurables = listOf(\n            CommonSettings.languageConfig(languageManager, scope),\n            CommonSettings.themeConfig(themeManager, scope),\n        )\n\n    fun onUserPressFinish() {\n        onFinish()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/initialsetup/InitialSetupPage.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.initialsetup\n\nimport androidx.compose.foundation.background\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.padding\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.layout.wrapContentWidth\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.layout.AlignmentLine\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.onboarding.AppIcon\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageActions\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageHeader\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageTemplate\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.shared.ui.widget.PrimaryMainActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun InitialSetupPage(\n    component: InitialSetupComponent,\n) {\n    StartUpPageTemplate(\n        header = {\n            StartUpPageHeader(\n                title = Res.string.app_title.asStringSource(),\n                onBackPressed = null\n            )\n        },\n        actions = {\n            StartUpPageActions {\n                Column {\n                    Text(\n                        text = myStringResource(Res.string.initial_setup_notice),\n                        color = LocalContentColor.current.copy(alpha = 0.75f),\n                        modifier = Modifier\n                            .padding(mySpacings.smallSpace)\n                    )\n                    Spacer(modifier = Modifier.height(mySpacings.mediumSpace))\n                    Row {\n                        PrimaryMainActionButton(\n                            onClick = component::onUserPressFinish,\n                            text = myStringResource(Res.string.next),\n                            modifier = Modifier\n                                .fillMaxWidth(),\n                        )\n                    }\n                }\n            }\n        },\n        content = {\n            Column {\n                Column(\n                    Modifier\n                        .weight(1f)\n                        .wrapContentHeight()\n                ) {\n                    AppIcon(\n                        Modifier\n                            .fillMaxWidth()\n                            .wrapContentWidth(),\n                        size = 72.dp,\n                    )\n                    Spacer(Modifier.height(mySpacings.largeSpace))\n                    Spacer(Modifier.height(mySpacings.largeSpace))\n                    Text(\n                        text = myStringResource(Res.string.welcome),\n                        fontWeight = FontWeight.Bold,\n                        fontSize = myTextSizes.x2l,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .wrapContentWidth(),\n                    )\n                    Spacer(Modifier.height(mySpacings.mediumSpace))\n                    Text(\n                        text = myStringResource(Res.string.initial_setup_description),\n                        fontSize = myTextSizes.lg,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .wrapContentWidth(),\n                    )\n                }\n                Column(\n                    Modifier\n                        .weight(1f)\n                        .wrapContentHeight(Alignment.Bottom)\n                        .padding(vertical = mySpacings.largeSpace),\n                    verticalArrangement = Arrangement.spacedBy(mySpacings.largeSpace),\n                ) {\n                    for (configurable in component.configurables) {\n                        RenderConfigurable(\n                            cfg = configurable,\n                            configurableUiProps = ConfigurableUiProps(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = mySpacings.largeSpace)\n                                    .clip(myShapes.defaultRounded)\n                                    .background(myColors.surface),\n                                itemPaddingValues = PaddingValues(\n                                    horizontal = mySpacings.largeSpace,\n                                    vertical = mySpacings.mediumSpace,\n                                ),\n                            )\n                        )\n                    }\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/ABDMPermissions.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.Manifest\nimport android.content.Context\nimport android.os.Build\nimport android.os.Environment\nimport android.os.PowerManager\nimport androidx.annotation.RequiresApi\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.asStringSource\n\nobject ABDMPermissions {\n    private fun getReadRightStorage(): AppPermission {\n\n        return AppPermission(\n            title = Res.string.permission_read_write_external_storage_title.asStringSource(),\n            description = Res.string.permission_read_write_external_storage_reason.asStringSource(),\n            icon = MyIcons.data,\n            isOptional = false,\n            permissions = listOf(\n                Manifest.permission.READ_EXTERNAL_STORAGE,\n                Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            ),\n        )\n    }\n\n    @RequiresApi(Build.VERSION_CODES.R)\n    private fun createManageStorage(): AppPermission {\n        return AppPermission(\n            title = Res.string.permissions_manage_storage_title.asStringSource(),\n            description = Res.string.permissions_manage_storage_reason.asStringSource(),\n            icon = MyIcons.data,\n            isOptional = true,\n            permissions = listOf(\n                Manifest.permission.MANAGE_EXTERNAL_STORAGE,\n            ),\n            permissionRequestFactory = CustomPermissionActivityLauncher(::requestManageStoragePermission),\n            permissionChecker = object : PermissionRequestChecker {\n                override fun isGranted(\n                    context: Context,\n                    appPermission: AppPermission\n                ) = Environment.isExternalStorageManager()\n            }\n        )\n    }\n\n    val StoragePermission = run {\n        val isManageStorageAvailable = Build.VERSION.SDK_INT >= 30\n        if (isManageStorageAvailable) {\n            createManageStorage()\n        } else {\n            getReadRightStorage()\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    fun createPostNotificationPermission(): AppPermission {\n        return AppPermission(\n            title = Res.string.permissions_post_notification_title.asStringSource(),\n            description = Res.string.permissions_post_notification_reason.asStringSource(),\n            icon = MyIcons.speaker,\n            isOptional = false,\n            permissions = listOf(\n                Manifest.permission.POST_NOTIFICATIONS\n            )\n        )\n    }\n\n    val importantPermissions = buildList {\n        add(StoragePermission)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            add(createPostNotificationPermission())\n        }\n    }\n\n    // these are not introduced in the main screen.\n    val BatteryOptimizationPermission = AppPermission(\n        title = Res.string.permissions_ignore_battery_optimization_title.asStringSource(),\n        description = Res.string.permissions_ignore_battery_optimization_reason.asStringSource(),\n        icon = MyIcons.settings,\n        isOptional = true,\n        permissions = listOf(),\n        permissionRequestFactory = CustomPermissionActivityLauncher(::requestIgnoreBatteryOptimizationPermission),\n        permissionChecker = object : PermissionRequestChecker {\n            override fun isGranted(\n                context: Context,\n                appPermission: AppPermission\n            ): Boolean {\n                return isBatteryOptimizationDisabled(context)\n            }\n        }\n    )\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/AppPermission.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport androidx.activity.compose.LocalActivity\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.app.ActivityCompat\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\ndata class AppPermission(\n    val title: StringSource,\n    val description: StringSource,\n    val icon: IconSource,\n    val isOptional: Boolean,\n    val permissions: List<String>, // Manifest.Permissions\n    val permissionRequestFactory: PermissionRequestLauncherFactory = DefaultPermissionRequesterFactory,\n    val permissionChecker: PermissionRequestChecker = DefaultPermissionRequestChecker,\n)\n\n@Composable\nfun rememberAppPermissionState(\n    appPermission: AppPermission,\n    onNewResult: (Boolean) -> Unit = {},\n): AppPermissionState {\n    val activity = requireNotNull(LocalActivity.current) {\n        \"We should query permissions from activity\"\n    }\n    val state = remember(\n        appPermission,\n        activity,\n    ) {\n        AppPermissionState(\n            appPermission = appPermission,\n            context = activity\n        )\n    }\n    val launcher = appPermission.permissionRequestFactory.rememberLauncher(appPermission) {\n        state.refreshStatus()\n        onNewResult(it)\n    }\n    ListenForPermissionChangesInLifecycle(appPermission) {\n        state.refreshStatus()\n    }\n    LaunchedEffect(state, launcher) {\n        state.setLauncher(launcher)\n    }\n    return state\n}\n\ninterface PermissionRequestLauncherFactory {\n    @Composable\n    fun rememberLauncher(\n        appPermission: AppPermission,\n        onNewResult: (Boolean) -> Unit\n    ): PermissionRequestLauncher\n}\n\ninterface PermissionRequestLauncher {\n    fun launchPermissionRequest()\n}\n\ninterface PermissionRequestChecker {\n    fun isGranted(\n        context: Context,\n        appPermission: AppPermission,\n    ): Boolean\n\n    // in case of deny access\n    fun shouldShowRationale(\n        activity: Activity,\n        appPermission: AppPermission,\n    ): Boolean {\n        return false\n    }\n}\n\nsealed interface PermissionStatus {\n    data object Granted : PermissionStatus\n    data class NotGranted(\n        val shouldShowRationale: Boolean,\n    ) : PermissionStatus\n}\n\nclass AppPermissionState(\n    val appPermission: AppPermission,\n    private val context: Activity,\n) {\n\n    var permissionStatus by mutableStateOf(checkGranted())\n        private set\n\n    fun refreshStatus() {\n        permissionStatus = checkGranted()\n    }\n\n    fun checkGranted(): PermissionStatus {\n        val permissionChecker = appPermission.permissionChecker\n        return when {\n            permissionChecker.isGranted(context, appPermission) -> PermissionStatus.Granted\n            else -> PermissionStatus.NotGranted(\n                permissionChecker.shouldShowRationale(context, appPermission),\n            )\n        }\n    }\n\n    val isGranted by derivedStateOf {\n        permissionStatus == PermissionStatus.Granted\n    }\n    val requiresRational by derivedStateOf {\n        permissionStatus.let {\n            it is PermissionStatus.NotGranted && it.shouldShowRationale\n        }\n    }\n\n    private var launcher: PermissionRequestLauncher? = null\n    fun setLauncher(requestLauncher: PermissionRequestLauncher) {\n        launcher = requestLauncher\n    }\n\n    fun launchRequest() {\n        launcher?.launchPermissionRequest()\n    }\n}\n\n\nobject DefaultPermissionRequesterFactory : PermissionRequestLauncherFactory {\n    @Composable\n    override fun rememberLauncher(\n        appPermission: AppPermission,\n        onNewResult: (Boolean) -> Unit\n    ): PermissionRequestLauncher {\n        val launcher = rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.RequestMultiplePermissions(),\n        ) {\n            onNewResult(it.all { entry -> entry.value })\n        }\n        return remember(appPermission) {\n            object : PermissionRequestLauncher {\n                override fun launchPermissionRequest() {\n                    launcher.launch(appPermission.permissions.toTypedArray())\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ListenForPermissionChangesInLifecycle(\n    appPermission: AppPermission,\n    onRequestRefreshStatus: () -> Unit,\n) {\n    val context = LocalContext.current\n    val lifecycleOwner = LocalLifecycleOwner.current\n    DisposableEffect(appPermission, context, lifecycleOwner) {\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_RESUME) {\n                onRequestRefreshStatus()\n            }\n        }\n        lifecycleOwner.lifecycle.addObserver(observer)\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n}\n\n\nobject DefaultPermissionRequestChecker : PermissionRequestChecker {\n    override fun isGranted(\n        context: Context,\n        appPermission: AppPermission\n    ): Boolean {\n        return appPermission.permissions.all {\n            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED\n        }\n    }\n\n    override fun shouldShowRationale(\n        activity: Activity,\n        appPermission: AppPermission\n    ): Boolean {\n        return appPermission.permissions.any {\n            ActivityCompat.shouldShowRequestPermissionRationale(activity, it)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/BatteryOptimizationUtil.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.PowerManager\nimport android.provider.Settings\nimport androidx.core.net.toUri\nimport ir.amirab.util.ifThen\n\nfun requestIgnoreBatteryOptimizationPermission(\n    context: Context,\n    startNewTask: Boolean = false,\n) {\n    try {\n        val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {\n            data = (\"package:\" + context.packageName).toUri()\n        }.ifThen(startNewTask) {\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n        context.startActivity(intent)\n    } catch (e: Exception) {\n        // Fallback\n        val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)\n            .ifThen(startNewTask) {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        context.startActivity(intent)\n    }\n}\n\nfun isBatteryOptimizationDisabled(\n    context: Context\n): Boolean {\n    val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager\n    return powerManager.isIgnoringBatteryOptimizations(context.packageName)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/CustomPermissions.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.content.Context\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.platform.LocalContext\n\n\nclass CustomPermissionActivityLauncher(\n    private val openActivity: (Context) -> Unit,\n) : PermissionRequestLauncherFactory {\n    @Composable\n    override fun rememberLauncher(\n        appPermission: AppPermission,\n        onNewResult: (Boolean) -> Unit\n    ): PermissionRequestLauncher {\n        val context = LocalContext.current\n        return remember(context) {\n            object : PermissionRequestLauncher {\n                override fun launchPermissionRequest() {\n                    openActivity(context)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/PermissionComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass PermissionComponent(\n    componentContext: ComponentContext,\n    private val permissionManager: PermissionManager,\n    private val onReady: () -> Unit,\n    private val onDismiss: () -> Unit,\n) : BaseComponent(componentContext) {\n    val currentPermission: MutableStateFlow<PermissionsPageSteps> = MutableStateFlow(PermissionsPageSteps.Initial)\n    val permissionsToAsk = permissionManager.permissions.sortedBy {\n        !it.isOptional\n    }\n\n    fun goToNextPermissionPage() {\n        when (val currentPermission = currentPermission.value) {\n            is PermissionsPageSteps.AtPermission -> {\n                val index = permissionsToAsk.indexOf(currentPermission.appPermission)\n                val nextIndex = index + 1\n                if (nextIndex > permissionsToAsk.lastIndex) {\n                    this.currentPermission.value = PermissionsPageSteps.Done\n                } else {\n                    if (permissionManager.isReady(currentPermission.appPermission)) {\n                        val appPermission = permissionsToAsk[nextIndex]\n                        this.currentPermission.value = PermissionsPageSteps.AtPermission(appPermission)\n                    }\n                }\n            }\n\n            PermissionsPageSteps.Done -> {\n                onReady()\n            }\n\n            PermissionsPageSteps.Initial -> {\n                this.currentPermission.value = permissionsToAsk.firstOrNull()?.let {\n                    PermissionsPageSteps.AtPermission(it)\n                } ?: PermissionsPageSteps.Done\n            }\n        }\n    }\n\n    fun goToPreviousPermissionPage() {\n        when (val currentPermission = currentPermission.value) {\n            is PermissionsPageSteps.AtPermission -> {\n                val index = permissionsToAsk.indexOf(currentPermission.appPermission)\n                val previousIndex = index - 1\n                this.currentPermission.value = if (previousIndex < 0) {\n                    PermissionsPageSteps.Initial\n                } else {\n                    PermissionsPageSteps.AtPermission(permissionsToAsk[previousIndex])\n                }\n            }\n\n            PermissionsPageSteps.Done -> {\n                // we don't reach this!\n//                onReady()\n            }\n\n            PermissionsPageSteps.Initial -> {\n                onDismiss()\n            }\n        }\n    }\n}\n\nsealed interface PermissionsPageSteps {\n    data object Initial : PermissionsPageSteps\n    data class AtPermission(\n        val appPermission: AppPermission,\n    ) : PermissionsPageSteps\n\n    data object Done : PermissionsPageSteps\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/PermissionManager.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport androidx.core.content.ContextCompat\n\nclass PermissionManager(\n    val permissions: List<AppPermission>,\n    private val context: Context,\n) {\n    fun isReady(): Boolean {\n        return permissions.all {\n            isReady(it)\n        }\n    }\n\n    fun isGranted(appPermission: AppPermission): Boolean {\n        return appPermission.permissionChecker.isGranted(context, appPermission)\n    }\n    fun isReady(appPermission: AppPermission): Boolean {\n        return appPermission.isOptional || isGranted(appPermission)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/PermissionsPage.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.provider.Settings\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.wrapContentHeight\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageActions\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageHeader\nimport com.abdownloadmanager.android.pages.onboarding.StartUpPageTemplate\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.PrimaryMainActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n\n@Composable\nfun PermissionsPage(\n    component: PermissionComponent,\n) {\n    val currentPermission by component.currentPermission.collectAsState()\n    StartUpPageTemplate(\n        header = {\n            StartUpPageHeader(\n                title = Res.string.permissions.asStringSource(),\n                onBackPressed = component::goToPreviousPermissionPage,\n            )\n        },\n        content = {\n            AnimatedContent(\n                targetState = currentPermission,\n            ) {\n                val modifier = Modifier\n                    .fillMaxSize()\n                    .wrapContentHeight()\n                    .padding(horizontal = 24.dp)\n                    .padding(bottom = 24.dp)\n                when (it) {\n                    is PermissionsPageSteps.AtPermission -> {\n                        RenderPermissionContent(it.appPermission, modifier)\n                    }\n\n                    PermissionsPageSteps.Done -> {\n                        RenderDonePermissionGranting(modifier)\n                    }\n\n                    PermissionsPageSteps.Initial -> {\n                        RenderInitialPermissionGranting(modifier)\n                    }\n                }\n            }\n        },\n        actions = {\n            StartUpPageActions {\n                AnimatedContent(\n                    targetState = currentPermission,\n                ) {\n                    val modifier = Modifier\n                        .fillMaxWidth()\n                    when (it) {\n                        is PermissionsPageSteps.AtPermission -> {\n                            RenderPermissionActions(\n                                component, it.appPermission, modifier,\n                            )\n                        }\n\n                        PermissionsPageSteps.Done -> {\n                            PrimaryMainActionButton(\n                                text = myStringResource(Res.string.lets_go),\n                                onClick = component::goToNextPermissionPage,\n                                modifier = modifier,\n                            )\n                        }\n\n                        PermissionsPageSteps.Initial -> {\n                            PrimaryMainActionButton(\n                                text = myStringResource(Res.string.next),\n                                onClick = component::goToNextPermissionPage,\n                                modifier = modifier,\n                            )\n                        }\n                    }\n                }\n            }\n        },\n\n    )\n}\n\n@Composable\nfun RenderInitialPermissionGranting(\n    modifier: Modifier,\n) {\n    BasePermissionPageContent(\n        title = Res.string.permissions_initial_title.asStringSource(),\n        description = Res.string.permissions_initial_description.asStringSource(),\n        icon = MyIcons.permission,\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun RenderDonePermissionGranting(\n    modifier: Modifier,\n) {\n    BasePermissionPageContent(\n        title = Res.string.permissions_done_title.asStringSource(),\n        description = Res.string.permissions_done_description.asStringSource(),\n        icon = MyIcons.check,\n        modifier = modifier,\n        iconColor = myColors.success,\n    )\n}\n\n\n@Composable\nfun RenderPermissionContent(\n    appPermission: AppPermission,\n    modifier: Modifier,\n) {\n    BasePermissionPageContent(\n        title = appPermission.title,\n        description = appPermission.description,\n        modifier = modifier,\n        icon = appPermission.icon,\n    )\n}\n\n@Composable\nfun RenderPermissionActions(\n    permissionComponent: PermissionComponent,\n    appPermission: AppPermission,\n    modifier: Modifier,\n) {\n    var rejectedOnce by remember(appPermission) { mutableStateOf(false) }\n    val permissionState = rememberAppPermissionState(appPermission) {\n        if (!it) {\n            rejectedOnce = true\n        }\n    }\n    val userProbablyPressedOnDontAskAgain = !permissionState.requiresRational && rejectedOnce\n    Column(modifier) {\n        val permissionStatus = permissionState.permissionStatus\n        val isGranted = permissionStatus is PermissionStatus.Granted\n        AnimatedVisibility(isGranted) {\n            Text(\n                myStringResource(\n                    Res.string.permission_granted,\n                ),\n                color = myColors.success,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(\n                        start = mySpacings.mediumSpace,\n                        bottom = mySpacings.mediumSpace,\n                    )\n            )\n        }\n        AnimatedVisibility(\n            userProbablyPressedOnDontAskAgain && !isGranted && !appPermission.isOptional\n        ) {\n            Text(\n                myStringResource(\n                    Res.string.permission_not_granted,\n                ),\n                color = myColors.error,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(\n                        start = mySpacings.mediumSpace,\n                        bottom = mySpacings.mediumSpace,\n                    )\n            )\n        }\n        val activity = requireNotNull(LocalActivity.current) {\n            \"Activity is required to open app details\"\n        }\n        if (permissionStatus is PermissionStatus.NotGranted) {\n            PrimaryMainActionButton(\n                text = myStringResource(\n                    if (userProbablyPressedOnDontAskAgain) {\n                        Res.string.open_settings\n                    } else {\n                        Res.string.give_permission\n                    },\n                ),\n                modifier = Modifier.fillMaxWidth(),\n                onClick = {\n                    if (userProbablyPressedOnDontAskAgain) {\n                        openApplicationDetailsInSettings(activity)\n                    } else {\n                        permissionState.launchRequest()\n                    }\n                },\n            )\n        } else {\n            PrimaryMainActionButton(\n                text = myStringResource(Res.string.next),\n                modifier = Modifier.fillMaxWidth(),\n                onClick = {\n                    permissionComponent.goToNextPermissionPage()\n                },\n            )\n        }\n        if (appPermission.isOptional && permissionStatus !is PermissionStatus.Granted) {\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            ActionButton(\n                text = myStringResource(Res.string.skip),\n                modifier = Modifier.fillMaxWidth(),\n                onClick = {\n                    permissionComponent.goToNextPermissionPage()\n                },\n            )\n        }\n    }\n}\n\nfun openApplicationDetailsInSettings(activity: Activity) {\n    activity.startActivity(\n        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n            data = Uri.fromParts(\"package\", activity.packageName, null)\n        }\n    )\n}\n\n@Composable\nprivate fun BasePermissionPageContent(\n    title: StringSource,\n    description: StringSource,\n    icon: IconSource,\n    iconColor: Color = LocalContentColor.current,\n    modifier: Modifier = Modifier,\n) {\n    Column(modifier) {\n        val shape = RoundedCornerShape(24.dp)\n        MyIcon(\n            icon = icon,\n            null,\n            Modifier\n                .weight(1f)\n                .wrapContentHeight()\n                .align(Alignment.CenterHorizontally)\n                .clip(shape)\n                .background(color = myColors.menuGradientBackground)\n                .border(1.dp, myColors.menuBorderColor / 0.1f, shape)\n                .padding(16.dp)\n                .size(72.dp),\n            tint = iconColor\n        )\n        Spacer(Modifier.height(mySpacings.largeSpace))\n        Text(\n            text = title.rememberString(),\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.x2l,\n        )\n        Spacer(Modifier.height(mySpacings.largeSpace))\n        Text(\n            text = description.rememberString(),\n            fontWeight = FontWeight.Normal,\n            fontSize = myTextSizes.base,\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/onboarding/permissions/StoragePermissionUtil.kt",
    "content": "package com.abdownloadmanager.android.pages.onboarding.permissions\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.provider.Settings\nimport androidx.core.net.toUri\n\n\nfun requestManageStoragePermission(context: Context) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        try {\n            val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {\n                data = (\"package:\" + context.packageName).toUri()\n            }\n            context.startActivity(intent)\n        } catch (e: Exception) {\n            // Fallback\n            val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)\n            context.startActivity(intent)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/perhostsettings/AndroidPerHostSettingsComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.perhostsettings\n\nimport com.abdownloadmanager.shared.pages.perhostsettings.BasePerHostSettingsComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.serialization.Serializable\n\nclass AndroidPerHostSettingsComponent(\n    ctx: ComponentContext,\n    perHostSettingsManager: PerHostSettingsManager,\n    appRepository: BaseAppRepository,\n    appScope: CoroutineScope,\n    closeRequested: () -> Unit,\n) : BasePerHostSettingsComponent(\n    ctx = ctx,\n    perHostSettingsManager = perHostSettingsManager,\n    appRepository = appRepository,\n    appScope = appScope,\n    closeRequested = closeRequested,\n) {\n    @Serializable\n    data class Config(\n        override val openedHost: String?\n    ) : BasePerHostSettingsComponent.Config\n\n    sealed interface Effects : BasePerHostSettingsComponent.Effects.Platform {\n    }\n\n    fun reset() {\n        editedPerHostSettings.value = savedPerHostSettings.value\n        onIdSelected(null)\n    }\n\n    fun saveAndReturn() {\n        save()\n        onIdSelected(null)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/perhostsettings/PerHostSettingsPage.kt",
    "content": "package com.abdownloadmanager.android.pages.perhostsettings\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\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.Color\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.perhostsettings.PerHostSettingsItemWithId\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.*\n\n@Composable\nfun PerHostSettingsPage(component: AndroidPerHostSettingsComponent) {\n    val perHostSettings by component.editedPerHostSettings.collectAsState()\n    val selectedItemId by component.selectedId.collectAsState()\n//    WindowTitle(myStringResource(Res.string.settings_per_host_settings))\n    val configurableList = component.selectedItemConfigurableList.collectAsState().value\n    val canSave by component.canSave.collectAsState()\n    val scope = rememberCoroutineScope()\n    val backDispatcher = LocalOnBackPressedDispatcherOwner.current\n    BackHandler(\n        configurableList != null\n    ) {\n        component.reset()\n    }\n    PageUi(\n        header = {\n            PageHeader(\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        icon = MyIcons.back,\n                        contentDescription = Res.string.back.asStringSource(),\n                        onClick = {\n                            backDispatcher?.onBackPressedDispatcher?.onBackPressed()\n                        }\n                    )\n                },\n                headerTitle = {\n                    PageTitle(myStringResource(Res.string.settings_per_host_settings))\n                },\n                headerActions = {\n                    if (configurableList == null) {\n                        TransparentIconActionButton(\n                            icon = MyIcons.add,\n                            contentDescription = Res.string.add.asStringSource(),\n                            onClick = {\n                                component.onRequestAddNewHostSettingsItem()\n                            }\n                        )\n                    } else {\n                        TransparentIconActionButton(\n                            icon = MyIcons.remove,\n                            contentDescription = Res.string.remove.asStringSource(),\n                            onClick = {\n                                component.onRequestDeleteConfig(configurableList.id)\n                            }\n                        )\n                        TransparentIconActionButton(\n                            icon = MyIcons.check,\n                            enabled = canSave,\n                            onClick = {\n                                scope.launch {\n                                    component.saveAndReturn()\n                                }\n                            },\n                            contentDescription = Res.string.update.asStringSource()\n                        )\n                    }\n                }\n            )\n        },\n        footer = {\n\n        },\n        modifier = Modifier\n            .systemBarsPadding()\n            .navigationBarsPadding()\n    ) {\n        // TODO improvement make it tablet friendly\n        AnimatedContent(\n            configurableList,\n            modifier = Modifier.padding(it.paddingValues)\n        ) { configurableList ->\n            if (configurableList != null) {\n                RenderPerHostSettingsItem(\n                    modifier = Modifier\n                        .padding(8.dp),\n                    itemId = configurableList.id,\n                    configurableList = configurableList.configurableGroups,\n                )\n            } else {\n                Column {\n                    HostList(\n                        modifier = Modifier\n                            .padding(8.dp)\n                            .fillMaxWidth()\n                            .weight(1f),\n                        hosts = perHostSettings,\n                        selectedId = selectedItemId,\n                        setSelected = { id ->\n                            component.onIdSelected(id)\n                        },\n                        component = component\n                    )\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderPerHostSettingsItem(\n    modifier: Modifier,\n    itemId: String,\n    configurableList: List<ConfigurableGroup>,\n) {\n    val fm = LocalFocusManager.current\n    //remove focus to prevent accidentally change config in different queue\n    LaunchedEffect(itemId) {\n        fm.clearFocus()\n    }\n    Column(modifier) {\n        val pageModifier = Modifier\n            .fillMaxSize()\n        RenderPerHostSettingsConfigurableGroup(pageModifier, configurableList)\n    }\n}\n\n@Composable\nprivate fun RenderPerHostSettingsConfigurableGroup(\n    modifier: Modifier,\n    configurableGroups: List<ConfigurableGroup>,\n) {\n    Column(\n        modifier\n            .verticalScroll(rememberScrollState())\n    ) {\n        for ((index, cfgGroup) in configurableGroups.withIndex()) {\n            RenderConfigurableGroup(\n                group = cfgGroup,\n                modifier = Modifier,\n                itemPadding = PaddingValues(\n                    vertical = 8.dp,\n                    horizontal = 16.dp\n                )\n            )\n            if (index != configurableGroups.lastIndex) {\n                Spacer(Modifier.height(8.dp))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HostList(\n    modifier: Modifier,\n    hosts: List<PerHostSettingsItemWithId>,\n    selectedId: String?,\n    setSelected: (String) -> Unit,\n    component: AndroidPerHostSettingsComponent,\n) {\n    val shape = myShapes.defaultRounded\n    val borderColor = myColors.surface / 0.5f\n    var search by remember { mutableStateOf(\"\") }\n    val defaultEmptyName = myStringResource(Res.string.settings_per_host_settings_new_host)\n    val filteredHosts = remember(hosts, search) {\n        hosts.ifThen(search.isNotEmpty()) {\n            filter {\n                it.perHostSettingsItem.host.contains(search, true)\n            }\n        }\n    }\n    Column(\n        modifier\n            .border(1.dp, borderColor, shape)\n            .clip(shape)\n    ) {\n        Box(\n            Modifier\n                .weight(1f)\n                .fillMaxWidth()\n        ) {\n            LazyColumn {\n                items(filteredHosts, key = { it.id }) { s ->\n                    val isSelected = selectedId == s.id\n                    SideBarItem(\n                        isSelected = isSelected,\n                        onClick = { setSelected(s.id) },\n                        name = s.perHostSettingsItem.host.takeIf { it.isNotBlank() } ?: defaultEmptyName,\n                        modifier = Modifier.animateItem(),\n                    )\n                }\n            }\n            if (filteredHosts.isEmpty()) {\n                WithContentAlpha(0.75f) {\n                    Text(\n                        myStringResource(Res.string.list_is_empty),\n                        modifier = Modifier.align(Alignment.Center)\n                    )\n                }\n            }\n        }\n\n//        Row(\n//            modifier = Modifier\n//                .padding(vertical = 4.dp)\n//                .padding(horizontal = 8.dp)\n//                .height(IntrinsicSize.Max)\n//                .fillMaxWidth(),\n//            verticalAlignment = Alignment.CenterVertically,\n//            horizontalArrangement = Arrangement.End\n//        ) {\n//            SearchBox(\n//                search,\n//                onTextChange = {\n//                    search = it\n//                },\n//                placeholder = myStringResource(Res.string.search),\n//                modifier = Modifier.weight(1f).fillMaxHeight(),\n//            )\n//        }\n    }\n}\n\n@Composable\nprivate fun SideBarItem(\n    name: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Box(\n        modifier\n            .height(IntrinsicSize.Max)\n            .heightIn(mySpacings.thumbSize)\n            .ifThen(isSelected) {\n                background(myColors.onBackground / 0.05f)\n            }\n            .selectable(\n                selected = isSelected,\n                onClick = onClick\n            ),\n        contentAlignment = Alignment.Center,\n    ) {\n        Row(\n            Modifier\n                .padding(vertical = 8.dp)\n                .padding(start = 16.dp)\n                .padding(end = 2.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                Text(\n                    name,\n                    Modifier.weight(1f),\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.lg,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                )\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier\n                .align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n        if (isSelected) {\n            listOf(\n                Alignment.TopCenter,\n                Alignment.BottomCenter,\n            ).forEach {\n                Spacer(\n                    Modifier\n                        .align(it)\n                        .fillMaxWidth()\n                        .height(1.dp)\n                        .background(\n                            Brush.horizontalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    myColors.onBackground / 0.1f,\n                                    myColors.onBackground / 0.1f,\n                                    Color.Transparent,\n                                )\n                            )\n                        )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/queue/QueueConfigurationComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.queue\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.DayOfWeekConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.TimeConfigurable\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.newScopeBasedOn\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.datetime.LocalTime\n\nclass QueueConfigurationComponent(\n    ctx: ComponentContext,\n    id: Long,\n    queueManager: QueueManager,\n) : BaseComponent(ctx) {\n    val downloadQueue = queueManager.queues.value.find {\n        it.id == id\n    }!!\n\n    val configurations: List<ConfigurableGroup> =\n        createConfigurableList(downloadQueue, scope)\n\n\n    private fun createConfigurableList(\n        downloadQueue: DownloadQueue, parentScope: CoroutineScope,\n    ): List<ConfigurableGroup> {\n        val scope = newScopeBasedOn(parentScope)\n        val enabledStartTimeFlow = downloadQueue.queueModel.mapStateFlow() {\n            it.scheduledTimes.enabledStartTime\n        }\n        val enabledEndTimeFlow = downloadQueue.queueModel.mapStateFlow() {\n            it.scheduledTimes.enabledEndTime\n        }\n        val enabledSchedulerFlow = combineStateFlows(enabledStartTimeFlow, enabledEndTimeFlow) { start, end ->\n            start || end\n        }\n        return listOf(\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.general.asStringSource()),\n                nestedConfigurable = listOf(\n                    StringConfigurable(\n                        Res.string.name.asStringSource(),\n                        Res.string.queue_name_help.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.name\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setName(newValue)\n                            },\n                        ),\n                        validate = {\n                            it.length in 1..32\n                        },\n                        describe = {\n                            Res.string.queue_name_describe\n                                .asStringSourceWithARgs(\n                                    Res.string.queue_name_describe_createArgs(\n                                        value = it\n                                    )\n                                )\n                        },\n                    ),\n                    IntConfigurable(\n                        Res.string.queue_max_concurrent_download.asStringSource(),\n                        Res.string.queue_max_concurrent_download_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.maxConcurrent\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setMaxConcurrent(newValue)\n                            },\n                        ),\n                        describe = { \"$it\".asStringSource() },\n                        range = 1..32,\n                        renderMode = IntConfigurable.RenderMode.TextField,\n                    ),\n                ),\n            ),\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.on_completion.asStringSource()),\n                nestedConfigurable = listOf(\n                    BooleanConfigurable(\n                        Res.string.queue_automatic_stop.asStringSource(),\n                        Res.string.queue_automatic_stop_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.stopQueueOnEmpty\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setStopQueueOnEmpty(newValue)\n                            },\n                        ),\n                        describe = {\n                            if (it) Res.string.enabled.asStringSource()\n                            else Res.string.disabled.asStringSource()\n                        },\n                    ),\n                )\n            ),\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.queue_scheduler.asStringSource()),\n                nestedVisible = enabledSchedulerFlow,\n                mainConfigurable = BooleanConfigurable(\n                    Res.string.queue_enable_scheduler.asStringSource(),\n                    description = \"\".asStringSource(),\n                    describe = { \"\".asStringSource() },\n                    backedBy = createMutableStateFlowFromStateFlow(\n                        flow = enabledSchedulerFlow,\n                        scope = scope,\n                        updater = { newValue ->\n                            downloadQueue.setScheduledTimes {\n                                copy(\n                                    enabledStartTime = newValue,\n                                    enabledEndTime = newValue,\n                                )\n                            }\n                        }\n                    ),\n                ),\n                nestedConfigurable = listOf(\n                    DayOfWeekConfigurable(\n                        Res.string.queue_active_days.asStringSource(),\n                        Res.string.queue_active_days_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.daysOfWeek\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(daysOfWeek = newValue)\n                                }\n                            },\n                        ),\n                        validate = {\n                            it.isNotEmpty()\n                        },\n                        describe = { \"\".asStringSource() },\n                    ),\n                    BooleanConfigurable(\n                        Res.string.queue_scheduler_enable_auto_start_time.asStringSource(),\n                        description = \"\".asStringSource(),\n                        describe = { \"\".asStringSource() },\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = enabledStartTimeFlow,\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(enabledStartTime = newValue)\n                                }\n                            },\n                        ),\n                    ),\n                    TimeConfigurable(\n                        Res.string.queue_scheduler_auto_start_time.asStringSource(),\n                        \"\".asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.startTime\n                            },\n                            updater = {\n                                downloadQueue.setScheduledTimes {\n                                    copy(startTime = it)\n                                }\n                            },\n                        ),\n                        describe = { hourAndMinutesToString(it).asStringSource() },\n                        visible = enabledStartTimeFlow,\n                    ),\n                    BooleanConfigurable(\n                        Res.string.queue_scheduler_enable_auto_stop_time.asStringSource(),\n                        description = \"\".asStringSource(),\n                        describe = { \"\".asStringSource() },\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = enabledEndTimeFlow,\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(enabledEndTime = newValue)\n                                }\n                            },\n                        ),\n                    ),\n                    TimeConfigurable(\n                        Res.string.queue_scheduler_auto_stop_time.asStringSource(),\n                        \"\".asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.endTime\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(endTime = newValue)\n                                }\n                            },\n                        ),\n                        describe = { hourAndMinutesToString(it).asStringSource() },\n                        visible = enabledEndTimeFlow,\n                    ),\n                )\n            ),\n        )\n    }\n}\n\nprivate fun hourAndMinutesToString(it: LocalTime): String {\n    val hour = it.hour.toString().padStart(2, '0')\n    val min = it.minute.toString().padStart(2, '0')\n    return \"$hour:$min\"\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/queue/QueuesSheet.kt",
    "content": "package com.abdownloadmanager.android.pages.queue\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun QueueConfigSheet(\n    queuesConfigurationComponent: QueueConfigurationComponent?,\n    onDismiss: () -> Unit,\n) {\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(\n        queuesConfigurationComponent\n    ) {\n        if (queuesConfigurationComponent != null) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    state.OnFullyDismissed(onDismiss)\n    ResponsiveDialog(state, onDismiss = state::hide) {\n        queuesConfigurationComponent?.let {\n            QueueConfig(\n                name = it.downloadQueue.queueModel.collectAsState().value.name,\n                groups = it.configurations,\n                onDismissRequest = state::hide,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ResponsiveDialogScope.QueueConfig(\n    name: String,\n    groups: List<ConfigurableGroup>,\n    onDismissRequest: () -> Unit,\n) {\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    val queues = myStringResource(Res.string.queues)\n                    SheetTitle(\"${queues}: $name\")\n                }\n            )\n        }\n    ) {\n        Column(\n            Modifier\n                .verticalScroll(rememberScrollState())\n        ) {\n            for (group in groups) {\n                RenderConfigurableGroup(\n                    modifier = Modifier,\n                    group = group,\n                    itemPadding = PaddingValues(8.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/settings/AndroidSettings.kt",
    "content": "package com.abdownloadmanager.android.pages.settings\n\nimport com.abdownloadmanager.android.pages.onboarding.permissions.ABDMPermissions\nimport com.abdownloadmanager.android.storage.AppSettingsStorage\nimport com.abdownloadmanager.android.ui.configurable.android.item.PermissionConfigurable\nimport com.abdownloadmanager.android.util.pagemanager.PermissionsPageManager\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.NavigatableConfigurable\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\n\n\nobject AndroidSettings {\n    fun permissionSettings(\n        permissionsPageManager: PermissionsPageManager\n    ): NavigatableConfigurable {\n        return NavigatableConfigurable(\n            title = Res.string.permissions.asStringSource(),\n            description = \"\".asStringSource(),\n            onRequestNavigate = {\n                permissionsPageManager.openPermissionsPage(false)\n            },\n        )\n    }\n\n    fun ignoreBatteryOptimizations(): PermissionConfigurable {\n        val permission = ABDMPermissions.BatteryOptimizationPermission\n        return PermissionConfigurable(\n            title = permission.title,\n            description = permission.description,\n            backedBy = MutableStateFlow(permission),\n        )\n    }\n\n    fun browserIconInLauncher(\n        appSettingsStorage: AppSettingsStorage\n    ): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_browser_in_launcher.asStringSource(),\n            description = Res.string.settings_browser_in_launcher_description.asStringSource(),\n            backedBy = appSettingsStorage.browserIconInLauncher,\n            describe = {\n                if (it) {\n                    Res.string.enabled\n                } else {\n                    Res.string.disabled\n                }.asStringSource()\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/settings/AndroidSettingsComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.settings\n\nimport com.abdownloadmanager.android.storage.AppSettingsStorage\nimport com.abdownloadmanager.android.util.pagemanager.PermissionsPageManager\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.settings.BaseSettingsComponent\nimport com.abdownloadmanager.shared.settings.CommonSettings\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.getValue\n\nclass AndroidSettingsComponent(\n    ctx: ComponentContext,\n    perHostSettingsPageManager: PerHostSettingsPageManager,\n    permissionsPageManager: PermissionsPageManager,\n) : BaseSettingsComponent(\n    ctx\n), KoinComponent {\n    private val appSettings by inject<AppSettingsStorage>()\n    //    private val pageStorage by inject<PageStatesStorage>()\n    private val appRepository by inject<BaseAppRepository>()\n    private val proxyManager by inject<ProxyManager>()\n    private val themeManager by inject<ThemeManager>()\n    private val languageManager by inject<LanguageManager>()\n    override val configurables: StateFlow<List<ConfigurableGroup>> = MutableStateFlow(\n        listOf(\n            ConfigurableGroup(\n                mainConfigurable = CommonSettings.themeConfig(themeManager, scope),\n                nestedVisible = themeManager.currentThemeInfo.mapStateFlow {\n                    it.id == ThemeManager.systemThemeInfo.id\n                },\n                nestedConfigurable = listOfNotNull(\n                    CommonSettings.defaultDarkThemeConfig(themeManager, scope),\n                    CommonSettings.defaultLightThemeConfig(themeManager, scope),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.languageConfig(languageManager, scope),\n//                            DesktopSettings.fontConfig(fontManager, scope),\n                    CommonSettings.uiScaleConfig(appSettings),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOfNotNull(\n//                            DesktopSettings.useNativeMenuBarConfig(appSettings),\n//                            DesktopSettings.mergeTopBarWithTitleBarConfig(appSettings),\n//                    CommonSettings.showIconLabels(appSettings),\n                    CommonSettings.useRelativeDateTime(appSettings),\n                    CommonSettings.playSoundNotification(appSettings),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.autoStartConfig(appSettings),\n//                            DesktopSettings.useSystemTray(appSettings),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.sizeUnit(appRepository, scope),\n                    CommonSettings.speedUnit(appRepository, scope),\n                    CommonSettings.useAverageSpeedConfig(appRepository),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.autoShowDownloadProgressWindow(appSettings),\n                    CommonSettings.showDownloadFinishWindow(appSettings),\n                )\n            ),\n            // download engine\n\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.defaultDownloadFolderConfig(appSettings),\n                    CommonSettings.useCategoryByDefault(appSettings),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.speedLimitConfig(appRepository),\n                    CommonSettings.threadCountConfig(appRepository),\n                    CommonSettings.maxConcurrentDownloads(appRepository),\n                    CommonSettings.maxDownloadRetryCount(appRepository),\n                    CommonSettings.dynamicPartDownloadConfig(appRepository),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.perHostSettings(perHostSettingsPageManager),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.proxyConfig(proxyManager),\n                    CommonSettings.userAgent(appSettings),\n                    CommonSettings.ignoreSSLCertificates(appSettings),\n                    CommonSettings.useServerLastModified(appRepository),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    CommonSettings.trackDeletedFilesOnDisk(appRepository),\n                    CommonSettings.appendExtensionToIncompleteDownloads(appRepository),\n                    CommonSettings.deletePartialFileOnDownloadCancellation(appSettings),\n                    CommonSettings.useSparseFileAllocation(appRepository),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    AndroidSettings.browserIconInLauncher(appSettings),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    AndroidSettings.permissionSettings(permissionsPageManager),\n                    AndroidSettings.ignoreBatteryOptimizations(),\n                )\n            ),\n\n            // browser integration\n            // disabled for now\n//            ConfigurableGroup(\n//                nestedConfigurable = listOf(\n//                    CommonSettings.browserIntegrationEnabled(appRepository),\n//                    CommonSettings.browserIntegrationPort(appRepository)\n//                )\n//            )\n        )\n    )\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/settings/SettingsPage.kt",
    "content": "package com.abdownloadmanager.android.pages.settings\n\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\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.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.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.page.FooterFade\nimport com.abdownloadmanager.android.ui.page.PageUi\nimport com.abdownloadmanager.android.ui.page.PageHeader\nimport com.abdownloadmanager.android.ui.page.PageTitle\nimport com.abdownloadmanager.android.ui.page.createAlphaForHeader\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n\n@Composable\nfun SettingsPage(\n    settingsComponent: AndroidSettingsComponent,\n) {\n//    WindowIcon(MyIcons.settings)\n//    WindowIcon(MyIcons.appIcon)\n    val scrollState = rememberScrollState()\n    var pageContentPaddingValues by remember {\n        mutableStateOf(PaddingValues())\n    }\n    val topPadding = pageContentPaddingValues.calculateTopPadding()\n    val bottomPadding = pageContentPaddingValues.calculateBottomPadding()\n    val density = LocalDensity.current\n    PageUi(\n        header = {\n            val backDispatcher = LocalOnBackPressedDispatcherOwner.current\n            PageHeader(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .background(\n                        myColors.background.copy(\n                            createAlphaForHeader(\n                                scrollState.value.toFloat(),\n                                density.run { topPadding.toPx() },\n                            ) * 0.75f\n                        )\n                    )\n                    .statusBarsPadding(),\n                leadingIcon = {\n                    TransparentIconActionButton(\n                        icon = MyIcons.back,\n                        contentDescription = Res.string.back.asStringSource(),\n                        onClick = {\n                            backDispatcher?.onBackPressedDispatcher?.onBackPressed()\n                        }\n                    )\n                },\n                headerTitle = {\n                    PageTitle(myStringResource(Res.string.settings))\n                },\n            )\n        },\n        footer = {\n            Spacer(Modifier.navigationBarsPadding())\n        }\n    ) { params ->\n        pageContentPaddingValues = params.paddingValues\n        Box {\n            VerticalScrollableContent(\n                scrollState,\n                Modifier.fillMaxSize()\n            ) {\n                Column(\n                    Modifier\n                        .fillMaxSize()\n                        .verticalScroll(scrollState)\n                        .navigationBarsPadding()\n                        .padding(bottom = 8.dp)\n                        .padding(\n                            horizontal = 8.dp,\n                        ),\n                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                ) {\n                    Spacer(Modifier.height(topPadding))\n                    val configurableGroups by settingsComponent.configurables.collectAsState()\n                    for (cfgGroup in configurableGroups) {\n                        RenderConfigurableGroup(\n                            cfgGroup,\n                            Modifier,\n                            itemPadding = PaddingValues(\n                                vertical = 8.dp,\n                                horizontal = 16.dp\n                            )\n                        )\n                    }\n                }\n            }\n            FooterFade(bottomPadding)\n        }\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/singledownload/CompletedDownloadPage.kt",
    "content": "package com.abdownloadmanager.android.pages.singledownload\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.unit.dp\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun CompletedDownloadPage(\n    component: AndroidSingleDownloadComponent,\n    completedDownloadItemState: CompletedDownloadItemState,\n) {\n    Column {\n        Row(\n            Modifier\n                .padding(\n                    horizontal = 16.dp,\n                    vertical = 8.dp\n                )\n        ) {\n            RenderFileIconAndSize(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                component = component,\n                itemState = completedDownloadItemState,\n            )\n            Spacer(Modifier.width(16.dp))\n            RenderName(\n                Modifier.weight(1f),\n                completedDownloadItemState.name,\n            )\n        }\n        Actions(Modifier, component)\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier,\n    component: AndroidSingleDownloadComponent,\n) {\n    val iDownloadItemState by component.itemStateFlow.collectAsState()\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            ActionButton(\n                myStringResource(Res.string.open),\n                modifier = Modifier.weight(1f),\n                onClick = {\n                    component.openFile()\n                },\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderName(\n    modifier: Modifier,\n    name: String,\n) {\n    Column(\n        modifier = modifier\n    ) {\n        WithContentColor(\n            myColors.success\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                MyIcon(\n                    MyIcons.check, null,\n                    Modifier.size(24.dp)\n                )\n                Spacer(Modifier.width(4.dp))\n                Text(\n                    myStringResource(Res.string.download_page_download_completed),\n                    fontWeight = FontWeight.Bold,\n                    fontSize = myTextSizes.lg,\n                )\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        Text(\n            text = name,\n            maxLines = 1,\n            modifier = Modifier.basicMarquee(\n                iterations = Int.MAX_VALUE\n            )\n        )\n    }\n}\n\n@Composable\nprivate fun RenderFileIconAndSize(\n    modifier: Modifier,\n    component: AndroidSingleDownloadComponent,\n    itemState: CompletedDownloadItemState,\n) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        MyIcon(\n            icon = component.fileIconProvider.rememberIcon(itemState.name),\n            contentDescription = null,\n            modifier = Modifier.size(24.dp),\n        )\n        Spacer(Modifier.height(4.dp))\n        Text(\n            text = convertPositiveSizeToHumanReadable(\n                itemState.contentLength,\n                LocalSizeUnit.current,\n            ).rememberString(),\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/singledownload/DesktopSingleDownloadPageComponent.kt",
    "content": "package com.abdownloadmanager.android.pages.singledownload\n\nimport com.abdownloadmanager.android.storage.AndroidExtraDownloadItemSettings\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.singledownloadpage.BaseSingleDownloadComponent\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.util.*\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlin.getValue\n\nclass AndroidSingleDownloadComponent(\n    ctx: ComponentContext,\n    downloadItemOpener: DownloadItemOpener,\n    onDismiss: () -> Unit,\n    downloadId: Long,\n    extraDownloadSettingsStorage: ExtraDownloadSettingsStorage<AndroidExtraDownloadItemSettings>,\n    downloadSystem: DownloadSystem,\n    appSettings: BaseAppSettingsStorage,\n    appRepository: BaseAppRepository,\n    applicationScope: CoroutineScope,\n    fileIconProvider: FileIconProvider,\n    val comesFromExternalApplication: Boolean,\n) : BaseSingleDownloadComponent<AndroidExtraDownloadItemSettings>(\n    ctx = ctx,\n    downloadItemOpener = downloadItemOpener,\n    onDismiss = onDismiss,\n    downloadId = downloadId,\n    extraDownloadSettingsStorage = extraDownloadSettingsStorage,\n    downloadSystem = downloadSystem,\n    appSettings = appSettings,\n    appRepository = appRepository,\n    applicationScope = applicationScope,\n    fileIconProvider = fileIconProvider,\n) {\n    override val defaultShowPartInfo: Boolean = false\n//    private val singleDownloadPageStateToPersist by lazy {\n//        get<PageStatesStorage>().singleDownloadPageState\n//    }\n//    override fun setShowPartInfo(value: Boolean) {\n//        super.setShowPartInfo(value)\n//        singleDownloadPageStateToPersist.update {\n//            it.copy {\n//                SingleDownloadPageStateToPersist.showPartInfo.set(value)\n//            }\n//        }\n//    }\n\n    sealed interface Effects : BaseSingleDownloadComponent.Effects.Platform {\n    }\n\n    val onCompletion by lazy {\n        listOf(\n            BooleanConfigurable(\n                title = Res.string.download_item_settings_show_download_completion_dialog.asStringSource(),\n                description = Res.string.download_item_settings_show_download_completion_dialog_description.asStringSource(),\n                backedBy = itemShouldShowCompletionDialog.mapTwoWayStateFlow(\n                    map = {\n                        it ?: globalShowCompletionDialog.value\n                    },\n                    unMap = { it }\n                ),\n                describe = {\n                    when (it) {\n                        true -> Res.string.enabled\n                        false -> Res.string.disabled\n                    }.asStringSource()\n                },\n            ),\n        )\n    }\n\n    data class Config(\n        override val id: Long\n    ) : BaseSingleDownloadComponent.Config\n}\n\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/singledownload/ProgressDownloadPage.kt",
    "content": "package com.abdownloadmanager.android.pages.singledownload\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\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.Color\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.singledownloadpage.SingleDownloadPagePropertyItem\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.ui.useIsInDebugMode\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.*\nimport ir.amirab.downloader.part.PartDownloadStatus\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\nenum class SingleDownloadPageSections(\n    val title: StringSource,\n    val icon: IconSource,\n) {\n    Info(\n        Res.string.info.asStringSource(),\n        MyIcons.info\n    ),\n    Settings(\n        Res.string.speed.asStringSource(),\n        MyIcons.fast,\n    ),\n    OnCompletion(\n        Res.string.on_completion.asStringSource(),\n        MyIcons.flag\n    ),\n}\n\nprivate val tabs = SingleDownloadPageSections.entries.toList()\n\n@Composable\nfun ProgressDownloadPage(\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n    itemState: ProcessingDownloadItemState\n) {\n    var selectedTab by remember { mutableStateOf(SingleDownloadPageSections.Info) }\n    val showPartInfo by singleDownloadComponent.showPartInfo.collectAsState()\n    val setShowPartInfo = singleDownloadComponent::setShowPartInfo\n    val horizontalPadding = 16.dp\n\n    Column {\n        Column(\n            Modifier\n                .clip(myShapes.defaultRounded)\n                .padding(1.dp),\n        ) {\n            val scrollState = rememberScrollState()\n            //info / settings ...\n            val tabContentModifier = Modifier\n            VerticalScrollableContent(\n                scrollState,\n            ) {\n                Box(\n                    Modifier\n                        .animateContentSize()\n//                        .height(150.dp)\n                        .verticalScroll(scrollState)\n                ) {\n                    when (selectedTab) {\n                        SingleDownloadPageSections.Info -> RenderInfo(\n                            tabContentModifier,\n                            horizontalPadding,\n                            singleDownloadComponent,\n                        )\n\n                        SingleDownloadPageSections.Settings -> RenderSettings(\n                            modifier = tabContentModifier,\n                            horizontalPadding = horizontalPadding,\n                            singleDownloadComponent = singleDownloadComponent,\n                        )\n\n                        SingleDownloadPageSections.OnCompletion -> RenderOnCompletion(\n                            modifier = tabContentModifier,\n                            horizontalPadding = horizontalPadding,\n                            singleDownloadComponent = singleDownloadComponent,\n                        )\n                    }\n                }\n            }\n        }\n        //tabs\n        MyTabRow {\n            for (tab in tabs) {\n                MyTab(\n                    selected = tab == selectedTab,\n                    onClick = {\n                        selectedTab = tab\n                    },\n                    icon = tab.icon,\n                    title = tab.title,\n                    selectionBackground = Color.Transparent\n                )\n            }\n        }\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Column(\n            Modifier\n                .background(myColors.surface / 0.5f)\n        ) {\n            Column(\n                Modifier\n                    .padding(horizontal = horizontalPadding)\n            ) {\n                Spacer(Modifier.size(8.dp))\n                RenderProgressBar(itemState)\n                Spacer(Modifier.size(8.dp))\n                RenderParts(\n                    itemState.parts,\n                    Modifier\n                        .height(4.dp)\n                        .clip(myShapes.defaultRounded)\n                        .background(myColors.onBackground / 15)\n                )\n                Spacer(Modifier.size(mySpacings.largeSpace))\n                RenderActions(itemState, singleDownloadComponent, showPartInfo, setShowPartInfo)\n                Spacer(Modifier.size(mySpacings.largeSpace))\n            }\n            AnimatedVisibility(showPartInfo) {\n                RenderPartInfo(\n                    modifier = Modifier.height(240.dp),\n                    itemState = itemState,\n                    horizontalPadding = horizontalPadding,\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderSettings(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n) {\n    Column(modifier) {\n        for (configurable in singleDownloadComponent.settings) {\n            RenderConfigurable(\n                configurable,\n                ConfigurableUiProps(\n                    modifier = Modifier,\n                    itemPaddingValues = PaddingValues(\n                        horizontal = horizontalPadding,\n                    )\n                )\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderOnCompletion(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n) {\n    Column(modifier) {\n        for (configurable in singleDownloadComponent.onCompletion) {\n            RenderConfigurable(\n                configurable,\n                ConfigurableUiProps(\n                    modifier = Modifier,\n                    itemPaddingValues = PaddingValues(\n                        horizontal = horizontalPadding,\n                    )\n                )\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderProgressBar(itemState: IDownloadItemState) {\n    val progress = when (itemState) {\n        is CompletedDownloadItemState -> 100\n        is ProcessingDownloadItemState -> when (val status = itemState.status) {\n            is DownloadJobStatus.PreparingFile -> status.percent\n            else -> itemState.percent\n        }\n    }?.let {\n        it / 100f\n    }\n\n    val status = itemState.statusOrFinished()\n    val background = when (status) {\n        is DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.Canceled -> if (ExceptionUtils.isNormalCancellation(status.e)) {\n            myColors.warningGradient\n        } else {\n            myColors.errorGradient\n        }\n\n        DownloadJobStatus.IDLE -> myColors.warningGradient\n        is DownloadJobStatus.Retrying -> myColors.errorGradient\n        DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.PreparingFile -> myColors.infoGradient\n        DownloadJobStatus.Resuming,\n        DownloadJobStatus.Downloading,\n            -> myColors.primaryGradient\n    }\n\n    Box(\n        Modifier\n            .fillMaxWidth()\n            .clip(myShapes.defaultRounded)\n            .height(14.dp)\n            .background(myColors.onBackground / 15)\n    ) {\n        progress?.let { progress ->\n            Box(\n                Modifier\n                    .background(background)\n                    .fillMaxHeight()\n                    .fillMaxWidth(\n                        animateFloatAsState(\n                            progress,\n                            tween(100, easing = LinearEasing)\n                        ).value\n                    )\n            ) {\n                if (progress == 1f) {\n                    MyIcon(\n                        MyIcons.check,\n                        null,\n                        Modifier\n                            .padding(1.dp)\n                            .clip(CircleShape)\n                            .background(myColors.onBackground)\n                            .padding(1.dp)\n                            .fillMaxHeight()\n                            .align(Alignment.CenterEnd),\n                        tint = myColors.background,\n                    )\n                }\n            }\n        }\n        if (progress == null && status is DownloadJobStatus.IsActive) {\n            val anim = rememberInfiniteTransition()\n            val l = 2000\n            val endPos by anim.animateFloat(\n                0f,\n                1f,\n                infiniteRepeatable(tween(l), RepeatMode.Restart)\n            )\n            val width by anim.animateFloat(\n                6f, 16f, infiniteRepeatable(\n                    keyframes {\n                        durationMillis = l\n                        0f atFraction 0f\n                        0.75f atFraction 0.25f\n                        0f atFraction 1f\n                    },\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n            Box(\n                Modifier\n                    .fillMaxHeight()\n                    .fillMaxWidth(endPos)\n            ) {\n                Box(\n                    Modifier\n                        .background(background)\n                        .fillMaxHeight()\n                        .align(Alignment.CenterEnd)\n                        .fillMaxWidth(width)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderPartInfo(\n    modifier: Modifier,\n    itemState: ProcessingDownloadItemState,\n    horizontalPadding: Dp,\n) {\n    Column(modifier) {\n        Column(\n            Modifier.weight(1f)\n        ) {\n            Box(\n                Modifier.weight(1f)\n            ) {\n                val (onlyActiveParts, setOnlyActiveParts) = rememberSaveable {\n                    mutableStateOf(true)\n                }\n                val listToShow = remember(itemState, onlyActiveParts) {\n                    itemState.parts\n                        .let { parts ->\n                            if (onlyActiveParts) {\n                                parts.filter {\n                                    when (it.status) {\n                                        is PartDownloadStatus.Canceled -> true\n                                        PartDownloadStatus.Completed -> false\n                                        PartDownloadStatus.IDLE -> false\n                                        PartDownloadStatus.ReceivingData -> true\n                                        PartDownloadStatus.Connecting -> true\n                                    }\n                                }\n                            } else {\n                                parts\n                            }\n                        }\n                        .withIndex()\n                        .toList()\n                }\n                LazyColumn(\n                    Modifier.fillMaxSize(),\n                    state = rememberLazyListState()\n                ) {\n                    items(listToShow, key = { it.value.id }) { item ->\n                        Box(\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = horizontalPadding)\n                                .padding(vertical = 4.dp)\n                        ) {\n                            RenderSinglePart(\n                                index = item.index + 1,\n                                part = item.value,\n                                size = listToShow.size\n                            )\n                        }\n                    }\n                }\n                if (useIsInDebugMode()) {\n                    Row(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .padding(bottom = 8.dp, end = 8.dp)\n                            .clickable { setOnlyActiveParts(!onlyActiveParts) },\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\"Only Actives\")\n                        Spacer(Modifier.width(4.dp))\n                        CheckBox(onlyActiveParts, { setOnlyActiveParts(it) })\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderSinglePart(\n    index: Int,\n    part: UiPart,\n    size: Int,\n) {\n    val sizeStringLength = size.toString().length\n    Row {\n        Text(\n            index.toString().padStart(sizeStringLength, '0'),\n            color = LocalContentColor.current / 0.5f,\n            modifier = Modifier,\n        )\n        Spacer(Modifier.width(mySpacings.mediumSpace))\n        Text(\n            prettifyStatus(part.status).rememberString(),\n            color = LocalContentColor.current / 0.75f,\n            modifier = Modifier.weight(1f),\n        )\n        val progress = convertPositiveSizeToHumanReadable(\n            part.howMuchProceed,\n            LocalSizeUnit.current\n        ).rememberString()\n        val total = part.length?.let { length ->\n            convertPositiveSizeToHumanReadable(length, LocalSizeUnit.current).rememberString()\n        } ?: myStringResource(Res.string.unknown)\n\n        Text(\n            \"$progress / $total\",\n            color = LocalContentColor.current / 0.75f,\n            modifier = Modifier\n        )\n    }\n}\n\nprivate fun prettifyStatus(status: PartDownloadStatus): StringSource {\n    return when (status) {\n        is PartDownloadStatus.Canceled -> Res.string.disconnected\n        PartDownloadStatus.IDLE -> Res.string.idle\n        PartDownloadStatus.Completed -> Res.string.finished\n        PartDownloadStatus.ReceivingData -> Res.string.receiving_data\n        PartDownloadStatus.Connecting -> Res.string.connecting\n    }.asStringSource()\n}\n\n\n@Composable\nprivate fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {\n    val title = propertyItem.name\n    val value = propertyItem.value\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier.fillMaxWidth()\n    ) {\n        WithContentAlpha(0.75f) {\n            Text(\n                text = \"${title.rememberString()}:\",\n                modifier = Modifier.weight(0.3f),\n                maxLines = 1,\n                fontSize = myTextSizes.base\n            )\n        }\n        WithContentAlpha(1f) {\n            Text(\n                text = value.rememberString(),\n                modifier = Modifier\n                    .basicMarquee(\n                        iterations = Int.MAX_VALUE\n                    )\n                    .weight(0.7f),\n                maxLines = 1,\n                fontSize = myTextSizes.base,\n                color = when (propertyItem.valueState) {\n                    SingleDownloadPagePropertyItem.ValueType.Normal -> LocalContentColor.current\n                    SingleDownloadPagePropertyItem.ValueType.Error -> myColors.error\n                    SingleDownloadPagePropertyItem.ValueType.Success -> myColors.success\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderInfo(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n) {\n    Column(\n        modifier\n            .padding(horizontal = horizontalPadding)\n            .padding(top = 8.dp)\n    ) {\n        for (propertyItem in singleDownloadComponent.extraDownloadProgressInfo.collectAsState().value) {\n            Spacer(Modifier.height(2.dp))\n            RenderPropertyItem(propertyItem)\n        }\n    }\n}\n\n@Composable\nprivate fun RenderActions(\n    itemState: ProcessingDownloadItemState,\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n    showingPartInfo: Boolean,\n    onRequestShowPartInfo: (show: Boolean) -> Unit,\n) {\n    Row {\n        PartInfoButton(showingPartInfo, onRequestShowPartInfo)\n        Spacer(Modifier.width(8.dp))\n        ToggleButton(\n            itemState = itemState,\n            toggle = singleDownloadComponent::toggle,\n            pause = singleDownloadComponent::pause,\n            modifier = Modifier.weight(1f),\n        )\n        Spacer(Modifier.width(8.dp))\n        CancelButton(\n            cancel = singleDownloadComponent::cancel,\n            icon = if (singleDownloadComponent.deletePartialFileOnDownloadCancellation.collectAsState().value) {\n                MyIcons.stop\n            } else {\n                null\n            },\n            modifier = Modifier,\n        )\n    }\n}\n\n@Composable\nprivate fun PartInfoButton(\n    showing: Boolean,\n    onClick: (Boolean) -> Unit,\n) {\n    val partsInfoTitle = Res.string.parts_info.asStringSource()\n    Tooltip(partsInfoTitle) {\n        IconActionButton(\n            onClick = {\n                onClick(!showing)\n            },\n            contentDescription = partsInfoTitle,\n            icon = if (showing) {\n                MyIcons.up\n            } else {\n                MyIcons.down\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun SingleDownloadPageButton(\n    onClick: () -> Unit,\n    text: String,\n    color: Color = LocalContentColor.current,\n    icon: IconSource? = null,\n    modifier: Modifier,\n) {\n    ActionButton(\n        modifier = modifier,\n        text = text,\n        start = {\n            icon?.let {\n                Row {\n                    MyIcon(it, null, Modifier.size(16.dp))\n                    Spacer(Modifier.width(4.dp))\n                }\n            }\n        },\n        contentPadding = PaddingValues(vertical = 6.dp, horizontal = 12.dp),\n        contentColor = color,\n        onClick = onClick,\n    )\n}\n\n@Composable\nprivate fun CancelButton(\n    cancel: () -> Unit,\n    icon: IconSource?,\n    modifier: Modifier,\n) {\n    SingleDownloadPageButton(\n        {\n            cancel()\n        },\n        icon = icon,\n        text = myStringResource(Res.string.cancel),\n        modifier = modifier,\n    )\n}\n\n@Composable\nprivate fun ToggleButton(\n    itemState: ProcessingDownloadItemState,\n    toggle: () -> Unit,\n    pause: () -> Unit,\n    modifier: Modifier,\n) {\n    var showPromptOnNonePresumablePause by remember(itemState.status is DownloadJobStatus.IsActive) {\n        mutableStateOf(false)\n    }\n\n    val isResumeSupported = itemState.supportResume == true\n    val (icon, text) = when {\n        itemState.canBeResumed() -> {\n            MyIcons.resume to Res.string.resume\n        }\n\n        itemState.canBePaused() -> {\n            MyIcons.pause to Res.string.pause\n        }\n\n        else -> return\n    }\n\n    Box(modifier) {\n        SingleDownloadPageButton(\n            {\n                if (isResumeSupported) {\n                    toggle()\n                } else {\n                    if (itemState.status is DownloadJobStatus.IsActive) {\n                        showPromptOnNonePresumablePause = true\n                    } else {\n                        toggle()\n                    }\n                }\n            },\n            icon = icon,\n            text = myStringResource(text),\n            color = if (isResumeSupported) {\n                LocalContentColor.current\n            } else {\n                if (itemState.status is DownloadJobStatus.IsActive) {\n                    myColors.error\n                } else {\n                    LocalContentColor.current\n                }\n            },\n            modifier = Modifier.fillMaxWidth(),\n        )\n        if (showPromptOnNonePresumablePause) {\n            val shape = myShapes.defaultRounded\n            val closePopup = {\n                showPromptOnNonePresumablePause = false\n            }\n            Popup(\n                popupPositionProvider = rememberMyComponentRectPositionProvider(\n                    offset = DpOffset.Zero,\n                    anchor = Alignment.TopEnd,\n                    alignment = Alignment.TopStart,\n                ),\n                onDismissRequest = closePopup\n            ) {\n                Column(\n                    Modifier\n                        .clip(shape)\n                        .border(2.dp, myColors.onBackground / 10, shape)\n                        .background(\n                            Brush.linearGradient(\n                                listOf(\n                                    myColors.surface,\n                                    myColors.background,\n                                )\n                            )\n                        )\n                        .padding(16.dp)\n                        .widthIn(max = 140.dp)\n                ) {\n                    Text(buildAnnotatedString {\n                        withStyle(SpanStyle(color = myColors.warning)) {\n                            append(\"${myStringResource(Res.string.warning)}:\\n\")\n                        }\n                        append(myStringResource(Res.string.unsupported_resume_warning))\n                    })\n                    Spacer(Modifier.height(8.dp))\n                    ActionButton(\n                        myStringResource(Res.string.stop_anyway),\n                        onClick = {\n                            closePopup()\n                            pause()\n                        },\n                        contentColor = myColors.error\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderParts(parts: List<UiPart>, modifier: Modifier) {\n    Row(\n        modifier\n            .fillMaxWidth()\n    ) {\n        if (parts.isNotEmpty()) {\n            val sortedParts = remember(parts) {\n                parts.sortedBy {\n                    it.id\n                }\n            }\n            for (p in sortedParts) {\n                val partSpace = p.partSpace\n                if (partSpace <= 0f) continue\n                key(p.id) {\n                    RenderPart(\n                        p,\n                        Modifier\n                            .fillMaxHeight()\n                            .weight(partSpace)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderPart(part: UiPart, modifier: Modifier) {\n    val partProgress = part.percent?.let {\n        it / 100f\n    } ?: 0f\n\n    val foregroundColor = when (part.status) {\n        is PartDownloadStatus.Canceled -> myColors.error\n        PartDownloadStatus.Completed -> myColors.info\n        PartDownloadStatus.IDLE -> myColors.info / 25\n        PartDownloadStatus.ReceivingData -> myColors.success\n        PartDownloadStatus.Connecting -> myColors.warning\n    }\n    Row(modifier) {\n        Box(\n            Modifier\n                .fillMaxSize()\n        ) {\n            Box(\n                Modifier\n                    .align(Alignment.CenterStart)\n                    .fillMaxWidth(partProgress)\n                    .fillMaxHeight()\n                    .background(foregroundColor)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/singledownload/ShowDownloadDialogs.kt",
    "content": "package com.abdownloadmanager.android.pages.singledownload\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.runtime.*\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.singledownloadpage.createStatusString\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.delay\n\n@Composable\nprivate fun getDownloadTitle(itemState: IDownloadItemState): String {\n    return buildString {\n        if (itemState is ProcessingDownloadItemState && itemState.percent != null) {\n            append(\"${itemState.percent}%\")\n            append(\" \")\n        }\n        append(createStatusString(itemState).rememberString())\n    }\n}\n\n\n@Composable\nfun ShowDownloadDialog(\n    singleDownloadComponent: AndroidSingleDownloadComponent,\n    onRequestShowInDownloads: () -> Unit,\n) {\n    val itemState by singleDownloadComponent.itemStateFlow.collectAsState()\n    val dialogState = rememberResponsiveDialogState(false)\n    dialogState.OnFullyDismissed {\n        singleDownloadComponent.close()\n    }\n    LaunchedEffect(Unit) {\n        // animate open after activity becomes fully open\n        // is there a better way?\n        delay(10)\n        dialogState.show()\n    }\n    val closeDialog = dialogState::hide\n    ResponsiveDialog(\n        dialogState, closeDialog\n    ) {\n        itemState?.let { downloadItemState ->\n            SheetUI(header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(getDownloadTitle(downloadItemState))\n                    },\n                    headerActions = {\n                        if (singleDownloadComponent.comesFromExternalApplication) {\n                            TransparentIconActionButton(\n                                MyIcons.externalLink,\n                                contentDescription = Res.string.show_downloads.asStringSource(),\n                                onClick = onRequestShowInDownloads,\n                            )\n                        }\n                        TransparentIconActionButton(\n                            MyIcons.close,\n                            contentDescription = Res.string.close.asStringSource(),\n                            onClick = closeDialog\n                        )\n                    }\n                )\n            }) {\n                AnimatedContent(\n                    targetState = downloadItemState,\n                    contentKey = {\n                        when (it) {\n                            is CompletedDownloadItemState -> 0\n                            is ProcessingDownloadItemState -> 1\n                        }\n                    }\n                ) { downloadItemState ->\n                    when (downloadItemState) {\n                        is CompletedDownloadItemState -> {\n                            CompletedDownloadPage(\n                                singleDownloadComponent,\n                                downloadItemState,\n                            )\n                        }\n\n                        is ProcessingDownloadItemState -> {\n                            ProgressDownloadPage(\n                                singleDownloadComponent,\n                                downloadItemState,\n                            )\n                        }\n                    }\n                }\n\n            }\n        }\n    }\n\n}\n\n\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/singledownload/SingleDownloadPageActivity.kt",
    "content": "package com.abdownloadmanager.android.pages.singledownload\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.abdownloadmanager.android.storage.AndroidExtraDownloadItemSettings\nimport com.abdownloadmanager.android.ui.MainActivity\nimport com.abdownloadmanager.android.util.AndroidDownloadItemOpener\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.android.util.activity.HandleActivityEffects\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport org.koin.core.component.inject\n\nclass SingleDownloadPageActivity : ABDMActivity() {\n    private val downloadSystem: DownloadSystem by inject()\n    private val downloadItemOpener: AndroidDownloadItemOpener by inject()\n    private val iconProvider: FileIconProvider by inject()\n    private val extraDownloadSettingsStorage: ExtraDownloadSettingsStorage<AndroidExtraDownloadItemSettings> by inject()\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val downloadId = getDownloadId(intent)\n        val isComingFromOutside = isComingFromExternalApplication(intent)\n        val myRetainedComponent = myRetainedComponent {\n            val closeAddDownloadDialog = {\n                this@myRetainedComponent.finishActivityAction()\n            }\n            AndroidSingleDownloadComponent(\n                ctx = it,\n                onDismiss = closeAddDownloadDialog,\n                downloadId = downloadId,\n                extraDownloadSettingsStorage = extraDownloadSettingsStorage,\n                downloadSystem = downloadSystem,\n                downloadItemOpener = downloadItemOpener,\n                appSettings = appSettingsStorage,\n                appRepository = appRepository,\n                fileIconProvider = iconProvider,\n                applicationScope = applicationScope,\n                comesFromExternalApplication = isComingFromOutside,\n            )\n        }\n        val singleDownloadComponent = myRetainedComponent.component\n        setABDMContent {\n            myRetainedComponent.HandleActivityEffects()\n            ShowDownloadDialog(\n                singleDownloadComponent = singleDownloadComponent,\n                onRequestShowInDownloads = {\n                    startActivity(\n                        MainActivity.createRevelDownloadIntent(\n                            context = this,\n                            singleDownloadComponent.downloadId,\n                        )\n                    )\n                    finish()\n                },\n            )\n        }\n    }\n\n    private fun getDownloadId(intent: Intent): Long {\n        return intent.getLongExtra(DOWNLOAD_ID, -1)\n    }\n    private fun isComingFromExternalApplication(intent: Intent): Boolean {\n        return intent.getBooleanExtra(COMING_FROM_OUTSIDE, true)\n    }\n\n    companion object {\n        const val DOWNLOAD_ID = \"downloadId\"\n\n        /**\n         * if we are inside app then there is no need to add app icon shortcut\n         */\n        const val COMING_FROM_OUTSIDE = \"comeFromOutside\"\n        fun createIntent(\n            context: Context,\n            downloadId: Long,\n            comingFromOutside: Boolean,\n        ): Intent {\n            val intent = Intent(\n                context,\n                SingleDownloadPageActivity::class.java,\n            )\n            intent.putExtra(DOWNLOAD_ID, downloadId)\n            intent.putExtra(COMING_FROM_OUTSIDE, comingFromOutside)\n            return intent\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/updater/NewUpdatePage.kt",
    "content": "package com.abdownloadmanager.android.pages.updater\n\nimport androidx.compose.animation.animateColor\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.BlurredEdgeTreatment\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.ui.theme.myMarkdownColors\nimport com.abdownloadmanager.shared.ui.theme.myMarkdownTypography\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport io.github.z4kn4fein.semver.Version\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport com.mikepenz.markdown.compose.Markdown\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun ResponsiveDialogScope.NewUpdatePage(\n    newVersionInfo: UpdateInfo,\n    currentVersion: Version,\n    update: () -> Unit,\n    cancel: () -> Unit,\n) {\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(\n                        myStringResource(Res.string.update_updater),\n                        MyIcons.refresh,\n                    )\n                }\n            )\n        }\n    ) {\n        val contentHorizontalPadding = 16.dp\n        Box {\n            BackgroundEffects()\n            Column(\n                Modifier\n            ) {\n                Column(\n                    Modifier\n                        .padding(\n                            top = 8.dp\n                        )\n                        .weight(1f, false)\n                ) {\n                    Column(\n                        Modifier\n                            .padding(horizontal = contentHorizontalPadding)\n                    ) {\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            Text(\n                                text = myStringResource(Res.string.update_available),\n                                fontSize = myTextSizes.xl,\n                                fontWeight = FontWeight.Bold\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(\n                                text = myStringResource(\n                                    Res.string.version_n, Res.string.version_n_createArgs(\n                                        newVersionInfo.version.toString()\n                                    )\n                                ),\n                                fontSize = myTextSizes.xl,\n                                fontWeight = FontWeight.Bold,\n                                color = myColors.success,\n                            )\n                        }\n                        Spacer(Modifier.height(8.dp))\n                        Text(\n                            text = myStringResource(Res.string.update_available_suggest_to_to_update),\n                            fontSize = myTextSizes.base,\n                        )\n                        Spacer(Modifier.height(8.dp))\n                    }\n                    RenderChangeLog(\n                        Modifier\n                            .fillMaxWidth()\n                            .weight(1f, false),\n                        newVersionInfo.changeLog,\n                        horizontalPadding = contentHorizontalPadding,\n                    )\n                }\n                Actions(\n                    Modifier.fillMaxWidth(),\n                    update,\n                    cancel\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BoxScope.BackgroundEffects() {\n    Box(\n        Modifier\n            .align(Alignment.TopCenter)\n            .offset(y = (-148).dp)\n            .fillMaxWidth(0.5f)\n            .height(200.dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.primary / 0.15f\n            )\n    )\n    Box(\n        Modifier\n            .align(Alignment.BottomEnd)\n            .size(180.dp)\n            .offset(x = 32.dp, y = (-32).dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.secondary / 0.15f\n            )\n    )\n}\n\n@Composable\nfun Actions(modifier: Modifier, update: () -> Unit, cancel: () -> Unit) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 16.dp),\n            horizontalArrangement = Arrangement.End\n        ) {\n            UpdateButton(Modifier.weight(1f), update)\n            Spacer(Modifier.width(8.dp))\n            CancelButton(Modifier.weight(1f), cancel)\n        }\n    }\n}\n\n@Composable\nfun UpdateButton(\n    modifier: Modifier,\n    update: () -> Unit,\n) {\n    val backgroundColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 30\n        }\n    )\n    val borderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors\n    )\n    val disabledBorderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 50\n        }\n    )\n    ActionButton(\n        text = myStringResource(Res.string.update),\n        modifier = modifier,\n        onClick = update,\n        backgroundColor = backgroundColor,\n        disabledBackgroundColor = backgroundColor,\n        borderColor = borderColor,\n        disabledBorderColor = disabledBorderColor,\n    )\n}\n\n@Composable\nfun CancelButton(\n    modifier: Modifier,\n    cancel: () -> Unit,\n) {\n    ActionButton(\n        text = myStringResource(Res.string.cancel),\n        modifier = modifier,\n        onClick = cancel,\n    )\n}\n\n@Composable\nprivate fun RenderChangeLog(\n    modifier: Modifier,\n    changeLog: String,\n    horizontalPadding: Dp,\n) {\n    val trimmedChangelog = remember(changeLog) {\n        changeLog\n            .lines()\n            .filterNot { it.isBlank() }\n            .joinToString(\"\\n\")\n    }\n    Column(modifier) {\n        Text(\n            text = myStringResource(Res.string.update_release_notes),\n            modifier = Modifier.padding(horizontal = horizontalPadding),\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.lg,\n        )\n        Spacer(Modifier.height(8.dp))\n        Column(\n            Modifier.background(myColors.surface / 75)\n        ) {\n            val transition = rememberInfiniteTransition()\n            val topBorderColors = listOf(\n                myColors.primary to myColors.secondaryVariant,\n                myColors.secondary to myColors.primaryVariant,\n                myColors.primaryVariant to myColors.secondary,\n                myColors.secondaryVariant to myColors.primary,\n            )\n            val animatedTopBorderColors = topBorderColors.map {\n                transition.animateColor(\n                    it.first, it.second, infiniteRepeatable(\n                        animation = tween(durationMillis = 3000, easing = LinearEasing),\n                        repeatMode = RepeatMode.Reverse\n                    )\n                )\n            }\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .height(2.dp)\n                    .background(\n                        Brush.horizontalGradient(\n                            animatedTopBorderColors.map { it.value }\n                        )\n                    )\n            )\n            val scrollState = rememberScrollState()\n            VerticalScrollableContent(\n                scrollState,\n                modifier,\n            ) {\n                Markdown(\n                    modifier = Modifier\n                        .weight(1f)\n                        .verticalScroll(scrollState)\n                        .padding(\n                            horizontal = horizontalPadding,\n                            vertical = 8.dp\n                        ),\n                    content = trimmedChangelog,\n                    colors = myMarkdownColors(),\n                    typography = myMarkdownTypography()\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderKeyValue(\n    key: String,\n    value: String,\n) {\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        WithContentAlpha(0.50f) {\n            Text(\n                key,\n                fontSize = myTextSizes.base,\n                maxLines = 1,\n            )\n        }\n        Spacer(Modifier.width(8.dp))\n        Text(\n            value,\n            fontSize = myTextSizes.base,\n            maxLines = 1,\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/pages/updater/UpdaterDialog.kt",
    "content": "package com.abdownloadmanager.android.pages.updater\n\nimport androidx.compose.runtime.*\nimport com.abdownloadmanager.shared.pages.updater.RenderUpdateNotifications\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\n\n@Composable\nfun UpdaterSheet(\n    updaterComponent: UpdateComponent,\n) {\n    ShowUpdaterDialog(updaterComponent)\n}\n\n@Composable\nprivate fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {\n    val showUpdate = updaterComponent.showNewUpdate.collectAsState().value\n    val newVersion = updaterComponent.newVersionData.collectAsState().value\n    val closeUpdatePage = {\n        updaterComponent.requestClose()\n    }\n    RenderUpdateNotifications(updaterComponent)\n    val isOpened = showUpdate && newVersion != null\n    val state = rememberResponsiveDialogState(false)\n    LaunchedEffect(isOpened) {\n        if (isOpened) {\n            state.show()\n        } else {\n            state.hide()\n        }\n    }\n    state.OnFullyDismissed(closeUpdatePage)\n    ResponsiveDialog(\n        state, state::hide\n    ) {\n        newVersion?.let {\n            NewUpdatePage(\n                newVersionInfo = newVersion,\n                currentVersion = updaterComponent.currentVersion,\n                cancel = closeUpdatePage,\n                update = {\n                    updaterComponent.performUpdate()\n                    closeUpdatePage()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/receiver/StartOnBootBroadcastReceiver.kt",
    "content": "package com.abdownloadmanager.android.receiver\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass StartOnBootBroadcastReceiver : BroadcastReceiver(), KoinComponent {\n    private val appManager: ABDMAppManager by inject()\n    private val appSettingStorage: BaseAppSettingsStorage by inject()\n    override fun onReceive(context: Context, intent: Intent) {\n        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {\n            if (appSettingStorage.autoStartOnBoot.value) {\n                appManager.bootDownloadSystemAndService()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/repository/AppRepository.kt",
    "content": "package com.abdownloadmanager.android.repository\n\nimport com.abdownloadmanager.android.pages.browser.BrowserActivity\nimport com.abdownloadmanager.android.storage.AppSettingsStorage\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.autoremove.RemovedDownloadsFromDiskTracker\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport ir.amirab.downloader.DownloadSettings\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass AppRepository(\n    scope: CoroutineScope,\n    appSettings: AppSettingsStorage,\n    proxyManager: ProxyManager,\n    downloadSystem: DownloadSystem,\n    downloadSettings: DownloadSettings,\n    removedDownloadsFromDiskTracker: RemovedDownloadsFromDiskTracker,\n    categoryManager: CategoryManager,\n) : BaseAppRepository(\n    scope = scope,\n    appSettings = appSettings,\n    proxyManager = proxyManager,\n    downloadSystem = downloadSystem,\n    downloadSettings = downloadSettings,\n    removedDownloadsFromDiskTracker = removedDownloadsFromDiskTracker,\n    categoryManager = categoryManager,\n) {\n    init {\n        appSettings.browserIconInLauncher\n            .debounce(500)\n            .distinctUntilChanged()\n            .onEach { enabled ->\n                BrowserActivity.Companion.Launcher.setEnabled(enabled)\n            }.launchIn(scope)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/service/DownloadSystemService.kt",
    "content": "package com.abdownloadmanager.android.service\n\nimport android.app.Service\nimport android.content.Intent\nimport android.util.Log\nimport androidx.core.app.ServiceCompat\nimport com.abdownloadmanager.android.util.ABDMServiceNotificationManager\nimport com.abdownloadmanager.android.util.AndroidConstants\nimport com.abdownloadmanager.android.util.AndroidUi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.first\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass DownloadSystemService : Service(), KoinComponent {\n    val abdmServiceNotificationManager: ABDMServiceNotificationManager by inject()\n    override fun onCreate() {\n        _isServiceRunningFlow.value = true\n        AndroidUi.boot()\n        abdmServiceNotificationManager.initNotificationChannel()\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        Log.i(\"DownloadSystemService\", \"onStartCommand: at the beginning\")\n        startForeground(\n            AndroidConstants.SERVICE_NOTIFICATION_ID,\n            abdmServiceNotificationManager.createMainNotification()\n        )\n        abdmServiceNotificationManager.startUpdatingNotifications()\n        Log.i(\"DownloadSystemService\", \"onStartCommand: service goes to foreground\")\n        return START_STICKY\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        abdmServiceNotificationManager.stopUpdatingNotifications()\n        ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)\n        _isServiceRunningFlow.value = false\n    }\n\n    override fun onBind(intent: Intent?) = null\n\n    companion object {\n        private val _isServiceRunningFlow = MutableStateFlow(false)\n        val isServiceRunningFlow = _isServiceRunningFlow.asStateFlow()\n        fun isServiceRunning(): Boolean {\n            return isServiceRunningFlow.value\n        }\n\n        suspend fun awaitStart() {\n            if (isServiceRunning()) {\n                return\n            }\n            isServiceRunningFlow.first { it }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/service/KeepAliveServiceReason.kt",
    "content": "package com.abdownloadmanager.android.service\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.MyDateAndTimeFormats\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.format\nimport kotlinx.datetime.toLocalDateTime\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\nsealed interface KeepAliveServiceReason {\n    fun getKeyChanges(): Any\n    fun getReasonString(): String\n    data class ActiveDownloads(val count: Int) : KeepAliveServiceReason {\n        override fun getKeyChanges() = count\n        override fun getReasonString(): String {\n            return Res.string.downloading.asStringSource().getString() + \": $count\"\n        }\n    }\n\n    data class ActiveQueue(val queueModels: List<QueueModel>) : KeepAliveServiceReason {\n        override fun getKeyChanges() = queueModels.size\n        override fun getReasonString(): String {\n            val qNames = queueModels.joinToString(\", \") { it.name }\n            return \"Q: $qNames ⏳\"\n        }\n    }\n\n    data class ScheduledQueues(val queueModels: List<QueueModel>) : KeepAliveServiceReason {\n        override fun getKeyChanges() = queueModels.map { it.scheduledTimes }\n\n        @OptIn(ExperimentalTime::class)\n        override fun getReasonString(): String {\n            val q = queueModels.map {\n                it to it.scheduledTimes.getNearestTimeToStart()\n            }.minByOrNull() { it.second } ?: return \"\"\n            val startTime = q.second\n            val instant = Instant.fromEpochMilliseconds(startTime)\n            val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())\n            val fDateTime = dateTime.format(MyDateAndTimeFormats.fullDateTimeWithoutYearAndSeconds)\n            return \"Q: ${q.first.name} - $fDateTime\"\n        }\n    }\n\n    data object AppIsInForeground : KeepAliveServiceReason {\n        override fun getKeyChanges() = Unit\n\n        override fun getReasonString(): String {\n            return Res.string.idle.asStringSource().getString()\n        }\n    }\n\n    @Composable\n    fun rememberReasonString(): String {\n        return remember(getKeyChanges()) {\n            getReasonString()\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/AndroidExtraDownloadItemSettings.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport com.abdownloadmanager.shared.storage.IExtraDownloadItemSettings\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AndroidExtraDownloadItemSettings(\n    override val id: Long,\n    // turnOffWifi: Boolean\n) : IExtraDownloadItemSettings {\n\n    companion object : IExtraDownloadItemSettings.DataClassDefinitions<AndroidExtraDownloadItemSettings> {\n        override fun createDefault(id: Long) = AndroidExtraDownloadItemSettings(id = id)\n        override val serializer: KSerializer<AndroidExtraDownloadItemSettings> =\n            AndroidExtraDownloadItemSettings.serializer()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/AndroidExtraQueueSettings.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettings\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AndroidExtraQueueSettings(\n    override val id: Long,\n) : IExtraQueueSettings {\n\n    companion object : IExtraQueueSettings.DataClassDefinitions<AndroidExtraQueueSettings> {\n        override fun createDefault(id: Long) = AndroidExtraQueueSettings(id)\n        override val serializer: KSerializer<AndroidExtraQueueSettings> = AndroidExtraQueueSettings.serializer()\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/AndroidOnBoardingStorage.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport androidx.datastore.core.DataStore\nimport arrow.optics.optics\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\nimport kotlinx.serialization.Serializable\n\n@optics\n@Serializable\ndata class OnBoardingData(\n    val initialSetupPassed: Boolean = false,\n    val permissionsPassedAtLeastOnce: Boolean = false,\n) {\n    companion object\n}\n\nclass AndroidOnBoardingStorage(\n    dataStore: DataStore<OnBoardingData>,\n) : ConfigBaseSettingsByJson<OnBoardingData>(dataStore) {\n    val onBoardingFlow = data\n\n    val initialSetupPassed = from(OnBoardingData.initialSetupPassed)\n    val permissionsPassedAtLeastOnce = from(OnBoardingData.permissionsPassedAtLeastOnce)\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/AppSettingsStorage.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport androidx.datastore.core.DataStore\nimport arrow.optics.Lens\nimport arrow.optics.optics\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.IAppSettingsModel\nimport com.abdownloadmanager.shared.storage.SupportedSizeUnits\nimport com.abdownloadmanager.shared.util.downloadlocation.PlatformDownloadLocationProvider\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByMapConfig\nimport com.abdownloadmanager.shared.util.ui.theme.DEFAULT_UI_SCALE\nimport ir.amirab.util.config.*\nimport ir.amirab.util.enumValueOrNull\nimport kotlinx.serialization.Serializable\nimport org.koin.core.component.KoinComponent\n\n@optics([arrow.optics.OpticsTarget.LENS])\n@Serializable\ndata class AppSettingsModel(\n    override val theme: String = \"dark\",\n    override val defaultDarkTheme: String = \"dark\",\n    override val defaultLightTheme: String = \"light\",\n    override val language: String? = null,\n    override val font: String? = null,\n    override val uiScale: Float? = null,\n    override val showIconLabels: Boolean = true,\n    override val useRelativeDateTime: Boolean = true,\n    override val threadCount: Int = 8,\n    override val maxConcurrentDownloads: Int = 0,\n    override val maxDownloadRetryCount: Int = 3,\n    override val dynamicPartCreation: Boolean = true,\n    override val useServerLastModifiedTime: Boolean = false,\n    override val appendExtensionToIncompleteDownloads: Boolean = false,\n    override val useSparseFileAllocation: Boolean = true,\n    override val useAverageSpeed: Boolean = true,\n    override val showDownloadProgressDialog: Boolean = true,\n    override val showDownloadCompletionDialog: Boolean = true,\n    override val speedLimit: Long = 0,\n    override val autoStartOnBoot: Boolean = true,\n    override val notificationSound: Boolean = true,\n    override val defaultDownloadFolder: String = PlatformDownloadLocationProvider\n        .instance.getDownloadLocation()\n        .resolve(\"ABDM\")\n        .canonicalFile.absolutePath,\n    override val browserIntegrationEnabled: Boolean = true,\n    override val browserIntegrationPort: Int = 15151,\n    override val trackDeletedFilesOnDisk: Boolean = false,\n    override val deletePartialFileOnDownloadCancellation: Boolean = false,\n    override val sizeUnit: SupportedSizeUnits = SupportedSizeUnits.BinaryBytes,\n    override val speedUnit: SupportedSizeUnits = SupportedSizeUnits.BinaryBytes,\n    override val ignoreSSLCertificates: Boolean = false,\n    override val useCategoryByDefault: Boolean = true,\n    override val userAgent: String = \"\",\n    val browserIconInLauncher: Boolean = false,\n) : IAppSettingsModel {\n    companion object {\n        val default: AppSettingsModel get() = AppSettingsModel()\n    }\n\n    object ConfigLens : Lens<MapConfig, AppSettingsModel>, KoinComponent {\n        object Keys {\n            val theme = stringKeyOf(\"theme\")\n            val defaultDarkTheme = stringKeyOf(\"defaultDarkTheme\")\n            val defaultLightTheme = stringKeyOf(\"defaultLightTheme\")\n            val language = stringKeyOf(\"language\")\n            val font = stringKeyOf(\"font\")\n            val uiScale = floatKeyOf(\"uiScale\")\n            val mergeTopBarWithTitleBar = booleanKeyOf(\"mergeTopBarWithTitleBar\")\n            val useNativeMenuBar = booleanKeyOf(\"useNativeMenuBar\")\n            val showIconLabels = booleanKeyOf(\"showIconLabels\")\n            val useRelativeDateTime = booleanKeyOf(\"useRelativeDateTime\")\n            val useSystemTray = booleanKeyOf(\"useSystemTray\")\n            val threadCount = intKeyOf(\"threadCount\")\n            val maxConcurrentDownloads = intKeyOf(\"maxConcurrentDownloads\")\n            val maxDownloadRetryCount = intKeyOf(\"maxDownloadRetryCount\")\n            val dynamicPartCreation = booleanKeyOf(\"dynamicPartCreation\")\n            val useServerLastModifiedTime = booleanKeyOf(\"useServerLastModifiedTime\")\n            val appendExtensionToIncompleteDownloads = booleanKeyOf(\"appendExtensionToIncompleteDownloads\")\n            val useSparseFileAllocation = booleanKeyOf(\"useSparseFileAllocation\")\n            val useAverageSpeed = booleanKeyOf(\"useAverageSpeed\")\n            val showDownloadProgressDialog = booleanKeyOf(\"showDownloadProgressDialog\")\n            val showDownloadCompletionDialog = booleanKeyOf(\"showDownloadCompletionDialog\")\n            val speedLimit = longKeyOf(\"speedLimit\")\n            val autoStartOnBoot = booleanKeyOf(\"autoStartOnBoot\")\n            val notificationSound = booleanKeyOf(\"notificationSound\")\n            val defaultDownloadFolder = stringKeyOf(\"defaultDownloadFolder\")\n            val browserIntegrationEnabled = booleanKeyOf(\"browserIntegrationEnabled\")\n            val browserIntegrationPort = intKeyOf(\"browserIntegrationPort\")\n            val trackDeletedFilesOnDisk = booleanKeyOf(\"trackDeletedFilesOnDisk\")\n            val deletePartialFileOnDownloadCancellation = booleanKeyOf(\"deletePartialFileOnDownloadCancellation\")\n            val sizeUnit = stringKeyOf(\"sizeUnit\")\n            val speedUnit = stringKeyOf(\"speedUnit\")\n            val ignoreSSLCertificates = booleanKeyOf(\"ignoreSSLCertificates\")\n            val useCategoryByDefault = booleanKeyOf(\"useCategoryByDefault\")\n            val userAgent = stringKeyOf(\"userAgent\")\n            val browserIconInLauncher = booleanKeyOf(\"browserIconInLauncher\")\n        }\n\n\n        override fun get(source: MapConfig): AppSettingsModel {\n            val default by lazy { AppSettingsModel.default }\n            // for nullable types we don't get default value\n            return AppSettingsModel(\n                theme = source.get(Keys.theme) ?: default.theme,\n                defaultDarkTheme = source.get(Keys.defaultDarkTheme) ?: default.defaultDarkTheme,\n                defaultLightTheme = source.get(Keys.defaultLightTheme) ?: default.defaultLightTheme,\n                language = source.get(Keys.language),\n                font = source.get(Keys.font),\n                uiScale = source.get(Keys.uiScale),\n                showIconLabels = source.get(Keys.showIconLabels) ?: default.showIconLabels,\n                useRelativeDateTime = source.get(Keys.useRelativeDateTime) ?: default.useRelativeDateTime,\n                threadCount = source.get(Keys.threadCount) ?: default.threadCount,\n                maxConcurrentDownloads = source.get(Keys.maxConcurrentDownloads) ?: default.maxConcurrentDownloads,\n                maxDownloadRetryCount = source.get(Keys.maxDownloadRetryCount) ?: default.maxDownloadRetryCount,\n                dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,\n                useServerLastModifiedTime = source.get(Keys.useServerLastModifiedTime)\n                    ?: default.useServerLastModifiedTime,\n                appendExtensionToIncompleteDownloads = source.get(Keys.appendExtensionToIncompleteDownloads)\n                    ?: default.appendExtensionToIncompleteDownloads,\n                useSparseFileAllocation = source.get(Keys.useSparseFileAllocation) ?: default.useSparseFileAllocation,\n                useAverageSpeed = source.get(Keys.useAverageSpeed) ?: default.useAverageSpeed,\n                showDownloadProgressDialog = source.get(Keys.showDownloadProgressDialog)\n                    ?: default.showDownloadProgressDialog,\n                showDownloadCompletionDialog = source.get(Keys.showDownloadCompletionDialog)\n                    ?: default.showDownloadCompletionDialog,\n                speedLimit = source.get(Keys.speedLimit) ?: default.speedLimit,\n                autoStartOnBoot = source.get(Keys.autoStartOnBoot) ?: default.autoStartOnBoot,\n                notificationSound = source.get(Keys.notificationSound) ?: default.notificationSound,\n                defaultDownloadFolder = source.get(Keys.defaultDownloadFolder) ?: default.defaultDownloadFolder,\n                browserIntegrationEnabled = source.get(Keys.browserIntegrationEnabled)\n                    ?: default.browserIntegrationEnabled,\n                browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort,\n                trackDeletedFilesOnDisk = source.get(Keys.trackDeletedFilesOnDisk) ?: default.trackDeletedFilesOnDisk,\n                deletePartialFileOnDownloadCancellation = source.get(Keys.deletePartialFileOnDownloadCancellation)\n                    ?: default.deletePartialFileOnDownloadCancellation,\n                sizeUnit = source.get(Keys.sizeUnit)?.enumValueOrNull<SupportedSizeUnits>() ?: default.sizeUnit,\n                speedUnit = source.get(Keys.speedUnit)?.enumValueOrNull<SupportedSizeUnits>() ?: default.speedUnit,\n                ignoreSSLCertificates = source.get(Keys.ignoreSSLCertificates) ?: default.ignoreSSLCertificates,\n                useCategoryByDefault = source.get(Keys.useCategoryByDefault) ?: default.useCategoryByDefault,\n                userAgent = source.get(Keys.userAgent) ?: default.userAgent,\n                browserIconInLauncher = source.get(Keys.browserIconInLauncher) ?: default.browserIconInLauncher,\n            )\n        }\n\n        override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {\n            return source.apply {\n                put(Keys.theme, focus.theme)\n                put(Keys.defaultDarkTheme, focus.defaultDarkTheme)\n                put(Keys.defaultLightTheme, focus.defaultLightTheme)\n                putNullable(Keys.language, focus.language)\n                putNullable(Keys.font, focus.font)\n                putNullable(Keys.uiScale, focus.uiScale)\n                put(Keys.showIconLabels, focus.showIconLabels)\n                put(Keys.useRelativeDateTime, focus.useRelativeDateTime)\n                put(Keys.threadCount, focus.threadCount)\n                put(Keys.maxConcurrentDownloads, focus.maxConcurrentDownloads)\n                put(Keys.maxDownloadRetryCount, focus.maxDownloadRetryCount)\n                put(Keys.dynamicPartCreation, focus.dynamicPartCreation)\n                put(Keys.useServerLastModifiedTime, focus.useServerLastModifiedTime)\n                put(Keys.appendExtensionToIncompleteDownloads, focus.appendExtensionToIncompleteDownloads)\n                put(Keys.useSparseFileAllocation, focus.useSparseFileAllocation)\n                put(Keys.useAverageSpeed, focus.useAverageSpeed)\n                put(Keys.showDownloadProgressDialog, focus.showDownloadProgressDialog)\n                put(Keys.showDownloadCompletionDialog, focus.showDownloadCompletionDialog)\n                put(Keys.speedLimit, focus.speedLimit)\n                put(Keys.autoStartOnBoot, focus.autoStartOnBoot)\n                put(Keys.notificationSound, focus.notificationSound)\n                put(Keys.defaultDownloadFolder, focus.defaultDownloadFolder)\n                put(Keys.browserIntegrationEnabled, focus.browserIntegrationEnabled)\n                put(Keys.browserIntegrationPort, focus.browserIntegrationPort)\n                put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk)\n                put(Keys.deletePartialFileOnDownloadCancellation, focus.deletePartialFileOnDownloadCancellation)\n                put(Keys.sizeUnit, focus.sizeUnit.name)\n                put(Keys.speedUnit, focus.speedUnit.name)\n                put(Keys.ignoreSSLCertificates, focus.ignoreSSLCertificates)\n                put(Keys.useCategoryByDefault, focus.useCategoryByDefault)\n                put(Keys.userAgent, focus.userAgent)\n                put(Keys.browserIconInLauncher, focus.browserIconInLauncher)\n            }\n        }\n    }\n}\n\nprivate val fontLens: Lens<AppSettingsModel, String?>\n    get() = Lens(\n        get = {\n            it.font\n        },\n        set = { s, f ->\n            s.copy(font = f)\n        }\n    )\n\n// use null for default scale!\nprivate val uiScaleLens: Lens<AppSettingsModel, Float>\n    get() = Lens(\n        get = {\n            it.uiScale ?: DEFAULT_UI_SCALE\n        },\n        set = { s, f ->\n            s.copy(uiScale = f.takeIf { it != DEFAULT_UI_SCALE })\n        }\n    )\nprivate val languageLens: Lens<AppSettingsModel, String?>\n    get() = Lens(\n        get = {\n            it.language\n        },\n        set = { s, f ->\n            s.copy(language = f)\n        }\n    )\n\nclass AppSettingsStorage(\n    settings: DataStore<MapConfig>,\n) : BaseAppSettingsStorage,\n    ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {\n    override val theme = from(AppSettingsModel.theme)\n    override val defaultDarkTheme = from(AppSettingsModel.defaultDarkTheme)\n    override val defaultLightTheme = from(AppSettingsModel.defaultLightTheme)\n\n    override val selectedLanguage = from(languageLens)\n    override val font = from(fontLens)\n    override val uiScale = from(uiScaleLens)\n    override val showIconLabels = from(AppSettingsModel.showIconLabels)\n    override val useRelativeDateTime = from(AppSettingsModel.useRelativeDateTime)\n    override val threadCount = from(AppSettingsModel.threadCount)\n    override val maxConcurrentDownloads = from(AppSettingsModel.maxConcurrentDownloads)\n    override val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)\n    override val useServerLastModifiedTime = from(AppSettingsModel.useServerLastModifiedTime)\n    override val appendExtensionToIncompleteDownloads = from(AppSettingsModel.appendExtensionToIncompleteDownloads)\n    override val useSparseFileAllocation = from(AppSettingsModel.useSparseFileAllocation)\n    override val useAverageSpeed = from(AppSettingsModel.useAverageSpeed)\n    override val maxDownloadRetryCount = from(AppSettingsModel.maxDownloadRetryCount)\n    override val showDownloadProgressDialog = from(AppSettingsModel.showDownloadProgressDialog)\n    override val showDownloadCompletionDialog = from(AppSettingsModel.showDownloadCompletionDialog)\n    override val speedLimit = from(AppSettingsModel.speedLimit)\n    override val autoStartOnBoot = from(AppSettingsModel.autoStartOnBoot)\n    override val notificationSound = from(AppSettingsModel.notificationSound)\n    override val defaultDownloadFolder = from(AppSettingsModel.defaultDownloadFolder)\n    override val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled)\n    override val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort)\n    override val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk)\n    override val deletePartialFileOnDownloadCancellation =\n        from(AppSettingsModel.deletePartialFileOnDownloadCancellation)\n    override val sizeUnit = from(AppSettingsModel.sizeUnit)\n    override val speedUnit = from(AppSettingsModel.speedUnit)\n    override val ignoreSSLCertificates = from(AppSettingsModel.ignoreSSLCertificates)\n    override val useCategoryByDefault = from(AppSettingsModel.useCategoryByDefault)\n    override val userAgent = from(AppSettingsModel.userAgent)\n\n    val browserIconInLauncher = from(AppSettingsModel.browserIconInLauncher)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/BrowserBookmarksStorage.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport androidx.compose.runtime.Immutable\nimport androidx.datastore.core.DataStore\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Immutable\ndata class BrowserBookmark(\n    val url: String,\n    val title: String,\n)\n\nclass BrowserBookmarksStorage(\n    dataStore: DataStore<List<BrowserBookmark>>,\n) : ConfigBaseSettingsByJson<List<BrowserBookmark>>(dataStore) {\n    val bookmarksFlow = data\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/storage/HomePageStorage.kt",
    "content": "package com.abdownloadmanager.android.storage\n\nimport androidx.datastore.core.DataStore\nimport com.abdownloadmanager.android.pages.home.HomePageStateToPersist\nimport com.abdownloadmanager.android.pages.home.sortBy\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\n\nclass HomePageStorage(\n    dataStore: DataStore<HomePageStateToPersist>,\n) : ConfigBaseSettingsByJson<HomePageStateToPersist>(\n    dataStore = dataStore,\n) {\n    val sortBy = from(HomePageStateToPersist.sortBy)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/ABDownloadManagerApplicationContent.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport com.abdownloadmanager.android.ui.configurable.comon.CommonConfigurableRenderersForAndroid\nimport com.abdownloadmanager.android.ui.configurable.comon.ConfigurableRenderersForAndroid\nimport com.abdownloadmanager.android.util.AppInfo\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.ui.ProvideCommonSettings\nimport com.abdownloadmanager.shared.ui.ProvideSizeUnits\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRendererRegistry\nimport com.abdownloadmanager.shared.ui.theme.ABDownloaderTheme\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.ui.widget.ProvideLanguageManager\nimport com.abdownloadmanager.shared.ui.widget.ProvideNotificationManager\nimport com.abdownloadmanager.shared.util.PopUpContainer\nimport com.abdownloadmanager.shared.util.ResponsiveBox\nimport com.abdownloadmanager.shared.util.ui.ProvideDebugInfo\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport kotlin.collections.component1\nimport kotlin.collections.component2\n\n@Composable\nfun ABDownloadManagerApplicationContent(\n    languageManager: LanguageManager,\n    themeManager: ThemeManager,\n    appSettingsStorage: BaseAppSettingsStorage,\n    iconResolver: IIconResolver,\n    appRepository: BaseAppRepository,\n    notificationManager: NotificationManager,\n    content: @Composable () -> Unit,\n) {\n    val configurableRendererRegistry = remember {\n        ConfigurableRendererRegistry {\n            listOf(\n                CommonConfigurableRenderersForAndroid,\n                ConfigurableRenderersForAndroid\n            ).forEach {\n                it.getAllRenderers().forEach { (key, renderer) ->\n                    this.register(key, renderer)\n                }\n            }\n        }\n    }\n    ProvideDebugInfo(AppInfo.isInDebugMode) {\n        ProvideLanguageManager(languageManager) {\n            ProvideCommonSettings(\n                appSettings = appSettingsStorage,\n                iconProvider = iconResolver,\n                configurableRendererRegistry = configurableRendererRegistry,\n            ) {\n                ProvideNotificationManager(notificationManager) {\n                    val myColors by themeManager.currentThemeColor.collectAsState()\n                    val uiScale by appSettingsStorage.uiScale.collectAsState()\n                    ABDownloaderTheme(\n                        myColors = myColors,\n                        fontFamily = null,\n                        uiScale = uiScale,\n                    ) {\n                        ResponsiveBox {\n                            ProvideSizeUnits(\n                                appRepository\n                            ) {\n                                PopUpContainer {\n                                    content()\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/MainActivity.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.util.activity.ABDMActivity\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.arkivanov.decompose.retainedComponent\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.inject\n\nclass MainActivity : ABDMActivity() {\n\n    private val downloadItemOpener: DownloadItemOpener by inject()\n    private val downloadSystem: DownloadSystem by inject()\n    private val categoryManager: CategoryManager by inject()\n    private val queueManager: QueueManager by inject()\n    private val defaultCategories: DefaultCategories by inject()\n    private val fileIconProvider: FileIconProvider by inject()\n    private val downloaderInUiRegistry: DownloaderInUiRegistry by inject()\n    private val json: Json by inject()\n    private val updateManager: UpdateManager by inject()\n    private val permissionManager: PermissionManager by inject()\n    val mainComponent by lazy {\n        retainedComponent {\n            // make sure to not pass any activity to retained component\n            MainComponent(\n                ctx = it,\n                context = applicationContext,\n                downloadItemOpener = downloadItemOpener,\n                downloadSystem = downloadSystem,\n                categoryManager = categoryManager,\n                queueManager = queueManager,\n                defaultCategories = defaultCategories,\n                fileIconProvider = fileIconProvider,\n                json = json,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n                perHostSettingsManager = perHostSettingsManager,\n                applicationScope = applicationScope,\n                appRepository = appRepository,\n                updateManager = updateManager,\n                permissionManager = permissionManager,\n                languageManager = languageManager,\n                themeManager = themeManager,\n                abdmAppManager = abdmAppManager,\n                onBoardingStorage = onBoardingStorage,\n                homePageStorage = homePageStorage,\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setABDMContent {\n            MainContent(\n                mainComponent = mainComponent,\n            )\n        }\n    }\n\n    override fun handleIntent(intent: Intent) {\n        if (intent.action == ACTION_REVEAL_DOWNLOAD_IN_LIST) {\n            val downloadId = intent.getLongExtra(DOWNLOAD_ID_KEY, -1)\n                .takeIf { it >= 0 } ?: return\n            mainComponent.revealDownload(downloadId)\n        }\n    }\n\n    companion object {\n        private const val DOWNLOAD_ID_KEY = \"downloadId\"\n        private const val ACTION_REVEAL_DOWNLOAD_IN_LIST = \"revealDownloadList\"\n        fun createRevelDownloadIntent(\n            context: Context,\n            downloadId: Long,\n        ): Intent {\n            return Intent(context, MainActivity::class.java).apply {\n                action = ACTION_REVEAL_DOWNLOAD_IN_LIST\n                putExtra(DOWNLOAD_ID_KEY, downloadId)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/MainComponent.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport android.content.Context\nimport android.content.Intent\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.android.pages.add.multiple.AddMultiDownloadActivity\nimport com.abdownloadmanager.android.pages.add.single.AddSingleDownloadActivity\nimport com.abdownloadmanager.android.pages.batchdownload.AndroidBatchDownloadComponent\nimport com.abdownloadmanager.android.pages.browser.BrowserActivity\nimport com.abdownloadmanager.android.pages.checksum.AndroidFileChecksumComponent\nimport com.abdownloadmanager.android.pages.editdownload.AndroidEditDownloadComponent\nimport com.abdownloadmanager.android.pages.home.HomeComponent\nimport com.abdownloadmanager.android.pages.onboarding.initialsetup.InitialSetupComponent\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionComponent\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.pages.perhostsettings.AndroidPerHostSettingsComponent\nimport com.abdownloadmanager.android.pages.queue.QueueConfigurationComponent\nimport com.abdownloadmanager.android.pages.settings.AndroidSettingsComponent\nimport com.abdownloadmanager.android.pages.singledownload.SingleDownloadPageActivity\nimport com.abdownloadmanager.android.storage.AndroidOnBoardingStorage\nimport com.abdownloadmanager.android.storage.HomePageStorage\nimport com.abdownloadmanager.android.ui.Screen.*\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.pagemanager.IBrowserPageManager\nimport com.abdownloadmanager.android.util.pagemanager.PermissionsPageManager\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pagemanager.AboutPageManager\nimport com.abdownloadmanager.shared.pagemanager.AddDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.BatchDownloadPageManager\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.OpenSourceLibrariesPageManager\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pagemanager.SettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.TranslatorsPageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.ui.widget.NotificationModel\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.childContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.activate\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.dismiss\nimport com.arkivanov.decompose.router.stack.StackNavigation\nimport com.arkivanov.decompose.router.stack.childStack\nimport com.arkivanov.decompose.router.stack.navigate\nimport com.arkivanov.decompose.router.stack.pushToFront\nimport ir.amirab.downloader.monitor.isDownloadActiveFlow\nimport ir.amirab.downloader.queue.DefaultQueueInfo\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.builtins.serializer\nimport kotlinx.serialization.json.Json\n\nsealed interface Screen {\n    data class Home(\n        val component: HomeComponent,\n    ) : Screen\n\n    data class Settings(\n        val component: AndroidSettingsComponent,\n    ) : Screen\n\n    data object About : Screen\n\n    data object OpenSourceThirdPartyLibraries : Screen\n\n    data object Translators : Screen\n\n    data class PerHostSettings(\n        val component: AndroidPerHostSettingsComponent,\n    ) : Screen\n\n    data class FileChecksum(\n        val component: AndroidFileChecksumComponent,\n    ) : Screen\n\n    data class InitialSetup(\n        val component: InitialSetupComponent,\n    ) : Screen\n\n    data class Permissions(\n        val component: PermissionComponent,\n    ) : Screen\n}\n\n@Serializable\nsealed interface ScreenConfig {\n    @Serializable\n    data object Home : ScreenConfig\n\n    @Serializable\n    data object Settings : ScreenConfig\n\n    @Serializable\n    data object About : ScreenConfig\n\n    @Serializable\n    data object OpenSourceThirdPartyLibraries : ScreenConfig\n\n    @Serializable\n    data object Translators : ScreenConfig\n\n    @Serializable\n    data class PerHostSettings(\n        val config: AndroidPerHostSettingsComponent.Config\n    ) : ScreenConfig\n\n    @Serializable\n    data class FileChecksum(\n        val config: AndroidFileChecksumComponent.Config\n    ) : ScreenConfig\n\n    @Serializable\n    data object InitialSetup : ScreenConfig\n\n    @Serializable\n    data class Permissions(\n        val openHomeAfterFinish: Boolean,\n    ) : ScreenConfig\n}\n\nclass MainComponent(\n    ctx: ComponentContext,\n    private val context: Context,\n    private val downloadItemOpener: DownloadItemOpener,\n    private val downloadSystem: DownloadSystem,\n    private val categoryManager: CategoryManager,\n    private val queueManager: QueueManager,\n    private val defaultCategories: DefaultCategories,\n    private val fileIconProvider: FileIconProvider,\n    private val downloaderInUiRegistry: DownloaderInUiRegistry,\n    private val perHostSettingsManager: PerHostSettingsManager,\n    private val applicationScope: CoroutineScope,\n    private val appRepository: BaseAppRepository,\n    private val updateManager: UpdateManager,\n    private val permissionManager: PermissionManager,\n    private val languageManager: LanguageManager,\n    private val themeManager: ThemeManager,\n    val abdmAppManager: ABDMAppManager,\n    val onBoardingStorage: AndroidOnBoardingStorage,\n    val homePageStorage: HomePageStorage,\n    private val json: Json,\n) : BaseComponent(ctx),\n    DownloadDialogManager,\n    EditDownloadDialogManager,\n    AddDownloadDialogManager,\n    FileChecksumDialogManager,\n    QueuePageManager,\n    CategoryDialogManager,\n    NotificationSender,\n    SettingsPageManager,\n    TranslatorsPageManager,\n    OpenSourceLibrariesPageManager,\n    AboutPageManager,\n    BatchDownloadPageManager,\n    PerHostSettingsPageManager,\n    PermissionsPageManager,\n    IBrowserPageManager,\n    ContainsEffects<MainComponent.MainAppEffects> by supportEffects() {\n    val categoryComponentNavigation = SlotNavigation<Long>()\n    val categorySlot = childSlot(\n        source = categoryComponentNavigation,\n        key = \"categoryEdit\",\n        childFactory = { config, ctx ->\n            CategoryComponent(\n                ctx = ctx,\n                id = config,\n                close = ::closeCategoryDialog,\n                submit = { submittedCategory ->\n                    if (submittedCategory.id < 0) {\n                        categoryManager.addCustomCategory(submittedCategory)\n                    } else {\n                        categoryManager.updateCategory(\n                            submittedCategory.id\n                        ) {\n                            submittedCategory.copy(\n                                items = it.items\n                            )\n                        }\n                    }\n                    closeCategoryDialog()\n                },\n            )\n        },\n        serializer = Long.serializer(),\n    ).subscribeAsStateFlow()\n    val queueConfigComponentNavigation = SlotNavigation<Long>()\n    val queueConfigSlot = childSlot(\n        source = queueConfigComponentNavigation,\n        key = \"queueConfigs\",\n        childFactory = { config, ctx ->\n            QueueConfigurationComponent(\n                ctx = ctx,\n                id = config,\n                queueManager = queueManager,\n            )\n        },\n        serializer = Long.serializer(),\n    ).subscribeAsStateFlow()\n\n    val batchDownloadNavigation = SlotNavigation<Unit>()\n    val batchDownloadSlot = childSlot(\n        source = batchDownloadNavigation,\n        key = \"batchDownload\",\n        childFactory = { config, ctx ->\n            AndroidBatchDownloadComponent(\n                ctx = ctx,\n                onClose = ::closeBatchDownload,\n                importLinks = { links ->\n                    openAddDownloadDialog(\n                        links.mapNotNull { link ->\n                            downloaderInUiRegistry\n                                .bestMatchForThisLink(link)\n                                ?.createMinimumCredentials(link)\n                                ?.let { credentials ->\n                                    AddDownloadCredentialsInUiProps(\n                                        credentials = credentials,\n                                    )\n                                }\n                        }\n                    )\n                }\n            )\n        },\n        serializer = null,\n    ).subscribeAsStateFlow()\n    val editDownloadNavigation = SlotNavigation<Long>()\n    val editDownloadSlot = childSlot(\n        source = editDownloadNavigation,\n        key = \"editDownload\",\n        childFactory = { editDownloadConfig: Long, componentContext: ComponentContext ->\n            AndroidEditDownloadComponent(\n                ctx = componentContext,\n                onRequestClose = {\n                    closeEditDownloadDialog()\n                },\n                onEdited = { updater, downloadJobExtraConfig ->\n                    scope.launch {\n                        downloadSystem.editDownload(\n                            id = editDownloadConfig,\n                            applyUpdate = updater,\n                            downloadJobExtraConfig = downloadJobExtraConfig\n                        )\n                        closeEditDownloadDialog()\n                    }\n                },\n                downloadId = editDownloadConfig,\n                acceptEdit = downloadSystem.downloadMonitor\n                    .isDownloadActiveFlow(editDownloadConfig)\n                    .mapStateFlow { !it },\n                downloadSystem = downloadSystem,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n                iconProvider = fileIconProvider,\n            )\n        },\n        serializer = null,\n    ).subscribeAsStateFlow()\n\n    val updaterComponent = UpdateComponent(\n        childContext(\"updater\"),\n        this,\n        updateManager,\n    )\n\n    val stackNavigation = StackNavigation<ScreenConfig>()\n    val stack = childStack(\n        stackNavigation,\n        key = \"mainStack\",\n        serializer = ScreenConfig.serializer(),\n        initialStack = {\n            val initialConfigPassed = onBoardingStorage.initialSetupPassed.value\n            val firstPage = if (!initialConfigPassed) {\n                ScreenConfig.InitialSetup\n            } else {\n                if (shouldGoToPermissionsPage()) {\n                    ScreenConfig.Permissions(openHomeAfterFinish = true)\n                } else {\n                    ScreenConfig.Home\n                }\n            }\n            listOf(firstPage)\n        },\n        handleBackButton = true,\n        childFactory = { cfg, ctx ->\n            when (cfg) {\n                ScreenConfig.Home -> {\n                    Home(\n                        HomeComponent(\n                            componentContext = ctx,\n                            downloadItemOpener = downloadItemOpener,\n                            downloadDialogManager = this,\n                            editDownloadDialogManager = this,\n                            addDownloadDialogManager = this,\n                            fileChecksumDialogManager = this,\n                            queuePageManager = this,\n                            categoryDialogManager = this,\n                            notificationSender = this,\n                            downloadSystem = downloadSystem,\n                            categoryManager = categoryManager,\n                            queueManager = queueManager,\n                            defaultCategories = defaultCategories,\n                            fileIconProvider = fileIconProvider,\n                            openSourceLibrariesPageManager = this,\n                            translatorsPageManager = this,\n                            aboutPageManager = this,\n                            batchDownloadPageManager = this,\n                            settingsPageManager = this,\n                            perHostSettingsPageManager = this,\n                            downloaderInUiRegistry = downloaderInUiRegistry,\n                            updateComponent = updaterComponent,\n                            homePageStorage = homePageStorage,\n                            browserPageManager = this,\n                        )\n                    )\n                }\n\n                ScreenConfig.Settings -> {\n                    Settings(\n                        AndroidSettingsComponent(\n                            ctx = ctx,\n                            perHostSettingsPageManager = this,\n                            permissionsPageManager = this,\n                        )\n                    )\n                }\n\n                ScreenConfig.About -> {\n                    About\n                }\n\n                ScreenConfig.OpenSourceThirdPartyLibraries -> {\n                    OpenSourceThirdPartyLibraries\n                }\n\n                ScreenConfig.Translators -> {\n                    Translators\n                }\n\n                is ScreenConfig.PerHostSettings -> {\n                    PerHostSettings(\n                        AndroidPerHostSettingsComponent(\n                            ctx = ctx,\n                            perHostSettingsManager = perHostSettingsManager,\n                            appRepository = appRepository,\n                            appScope = applicationScope,\n                            closeRequested = ::closePerHostSettings\n                        )\n                    )\n                }\n\n                is ScreenConfig.FileChecksum -> {\n                    FileChecksum(\n                        AndroidFileChecksumComponent(\n                            ctx = ctx,\n                            id = cfg.config.id,\n                            itemIds = cfg.config.itemIds,\n                            closeComponent = {\n                                closeFileChecksumPage(cfg.config.id)\n                            },\n                            downloadSystem = downloadSystem,\n                            iconProvider = fileIconProvider,\n                        )\n                    )\n                }\n\n                ScreenConfig.InitialSetup -> {\n                    Screen.InitialSetup(\n                        InitialSetupComponent(\n                            ctx = ctx,\n                            languageManager = languageManager,\n                            themeManager = themeManager,\n                            onFinish = {\n                                afterInitialFinish()\n                            }\n                        )\n                    )\n                }\n\n                is ScreenConfig.Permissions -> {\n                    Screen.Permissions(\n                        PermissionComponent(\n                            componentContext = ctx,\n                            permissionManager = permissionManager,\n                            onReady = {\n                                onPermissionsReady(cfg.openHomeAfterFinish)\n                            },\n                            onDismiss = {\n                                closePermissionsPage()\n                            }\n                        )\n                    )\n                }\n            }\n        },\n    ).subscribeAsStateFlow()\n\n    private fun onPermissionsReady(openHomeAfterFinish: Boolean) {\n        if (openHomeAfterFinish) {\n            onBoardingStorage.permissionsPassedAtLeastOnce.value = true\n            scope.launch {\n                abdmAppManager.startDownloadSystem()\n                abdmAppManager.startOurService()\n                initiallyGoToHome()\n            }\n        } else {\n            closePermissionsPage()\n        }\n    }\n\n    private fun shouldGoToPermissionsPage(): Boolean {\n        val permissionsPassedAtLeastOnce = onBoardingStorage.permissionsPassedAtLeastOnce.value\n        if (!permissionsPassedAtLeastOnce) {\n            return true\n        }\n        return !permissionManager.isReady()\n    }\n\n    private fun afterInitialFinish() {\n        onBoardingStorage.initialSetupPassed.value = true\n        if (shouldGoToPermissionsPage()) {\n            openPermissionsPage(true)\n        } else {\n            initiallyGoToHome()\n        }\n    }\n\n\n    private fun initiallyGoToHome() {\n        scope.launch {\n            stackNavigation.navigate {\n                listOf(ScreenConfig.Home)\n            }\n        }\n    }\n\n    override fun openDownloadDialog(id: Long) {\n        sendEffect(\n            MainAppEffects.StartActivity(\n                SingleDownloadPageActivity.createIntent(\n                    context = context,\n                    downloadId = id,\n                    comingFromOutside = false\n                )\n            )\n        )\n    }\n\n    override fun closeDownloadDialog() {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun openEditDownloadDialog(id: Long) {\n        scope.launch {\n            editDownloadNavigation.activate(id)\n        }\n    }\n\n    override fun closeEditDownloadDialog() {\n        scope.launch {\n            editDownloadNavigation.dismiss()\n        }\n    }\n\n    override fun closeAddDownloadDialog() {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun openAddDownloadDialog(\n        links: List<AddDownloadCredentialsInUiProps>,\n        importOptions: ImportOptions\n    ) {\n        scope.launch {\n            when (links.size) {\n                0 -> return@launch\n                1 -> {\n                    val intent = AddSingleDownloadActivity.createIntent(\n                        context = context,\n                        singleAddConfig = AddDownloadConfig.SingleAddConfig(\n                            newDownload = links.first(),\n                            importOptions = importOptions,\n                        ),\n                        json = json,\n                    )\n                    sendEffect(MainAppEffects.StartActivity(intent))\n                }\n\n                else -> {\n                    val intent = AddMultiDownloadActivity.createIntent(\n                        context = context,\n                        multipleAddConfig = AddDownloadConfig.MultipleAddConfig(\n                            newDownloads = links,\n                            importOptions = importOptions,\n                        ),\n                        json = json,\n                    )\n                    sendEffect(MainAppEffects.StartActivity(intent))\n                }\n            }\n        }\n    }\n\n    override fun openFileChecksumPage(ids: List<Long>) {\n        scope.launch {\n            stackNavigation.pushToFront(\n                ScreenConfig.FileChecksum(\n                    AndroidFileChecksumComponent.Config(\n                        itemIds = ids,\n                    )\n                )\n            )\n        }\n    }\n\n    override fun closeFileChecksumPage(dialogId: String) {\n        scope.launch {\n            stackNavigation.navigate {\n                it.filterNot { config ->\n                    config is ScreenConfig.FileChecksum\n                }\n            }\n        }\n    }\n\n    override fun openQueues(openQueueId: Long?) {\n        scope.launch {\n            queueConfigComponentNavigation.activate(openQueueId ?: DefaultQueueInfo.ID)\n        }\n    }\n\n    override fun closeQueues() {\n        scope.launch {\n            queueConfigComponentNavigation.dismiss()\n        }\n    }\n\n    override fun openCategoryDialog(categoryId: Long) {\n        scope.launch {\n            categoryComponentNavigation.activate(categoryId)\n        }\n    }\n\n    override fun closeCategoryDialog() {\n        scope.launch {\n            categoryComponentNavigation.dismiss()\n        }\n    }\n\n    override fun sendDialogNotification(\n        title: StringSource,\n        description: StringSource,\n        type: MessageDialogType\n    ) {\n        sendNotification(\n            tag = title,\n            title = title,\n            description = description,\n            type = when (type) {\n                MessageDialogType.Error -> NotificationType.Error\n                MessageDialogType.Info -> NotificationType.Info\n                MessageDialogType.Success -> NotificationType.Success\n                MessageDialogType.Warning -> NotificationType.Warning\n            },\n        )\n    }\n\n    override fun openSettings() {\n        scope.launch {\n            stackNavigation.pushToFront(ScreenConfig.Settings)\n        }\n    }\n\n    override fun closeSettings() {\n        scope.launch {\n            stackNavigation.navigate {\n                it.filterNot { config ->\n                    config is ScreenConfig.Settings\n                }\n            }\n        }\n    }\n\n    override fun sendNotification(\n        tag: Any,\n        title: StringSource,\n        description: StringSource,\n        type: NotificationType\n    ) {\n        sendEffect(\n            MainAppEffects.SimpleNotificationNotification(\n                NotificationModel(\n                    tag = tag,\n                    initialTitle = title,\n                    initialDescription = description,\n                    initialNotificationType = type,\n                )\n            )\n        )\n    }\n\n    override fun openTranslatorsPage() {\n        scope.launch {\n            stackNavigation.pushToFront(ScreenConfig.Translators)\n        }\n    }\n\n    override fun closeTranslatorsPage() {\n        scope.launch {\n            stackNavigation.navigate {\n                it.filterNot { config -> config is ScreenConfig.Translators }\n            }\n        }\n    }\n\n    override fun openOpenSourceLibrariesPage() {\n        scope.launch {\n            stackNavigation.pushToFront(ScreenConfig.OpenSourceThirdPartyLibraries)\n        }\n    }\n\n    override fun openAboutPage() {\n        scope.launch {\n            stackNavigation.pushToFront(ScreenConfig.About)\n        }\n    }\n\n    override fun openBatchDownloadPage() {\n        scope.launch {\n            batchDownloadNavigation.activate(Unit)\n        }\n    }\n\n    override fun closeBatchDownload() {\n        scope.launch {\n            batchDownloadNavigation.dismiss()\n        }\n    }\n\n    override fun openPerHostSettings(openedHost: String?) {\n        scope.launch {\n            stackNavigation.pushToFront(\n                ScreenConfig.PerHostSettings(\n                    AndroidPerHostSettingsComponent.Config(openedHost)\n                )\n            )\n        }\n    }\n\n    override fun closePerHostSettings() {\n        scope.launch {\n            stackNavigation.navigate {\n                it.filterNot { config ->\n                    config is ScreenConfig.PerHostSettings\n                }\n            }\n        }\n    }\n\n    override fun openPermissionsPage(\n        openHomeAfterFinish: Boolean\n    ) {\n        scope.launch {\n            stackNavigation.pushToFront(\n                ScreenConfig.Permissions(openHomeAfterFinish)\n            )\n        }\n    }\n\n    override fun closePermissionsPage() {\n        scope.launch {\n            stackNavigation.navigate {\n                val newList = it.filterNot { config ->\n                    config is ScreenConfig.Permissions\n                }\n                newList.ifEmpty {\n                    listOf(ScreenConfig.InitialSetup)\n                }\n            }\n        }\n    }\n\n\n    private val _showAddQueue = MutableStateFlow(false)\n    val showAddQueue = _showAddQueue.asStateFlow()\n    fun setShowAddQueue(value: Boolean) {\n        _showAddQueue.value = value\n    }\n\n    fun createQueueWithName(name: String) {\n        scope.launch { queueManager.addQueue(name) }\n        setShowAddQueue(false)\n    }\n\n    override fun closeNewQueueDialog() {\n        setShowAddQueue(false)\n    }\n\n    override fun openNewQueueDialog() {\n        setShowAddQueue(true)\n    }\n\n    fun revealDownload(downloadId: Long) {\n        stackNavigation.pushToFront(\n            ScreenConfig.Home,\n        ) {\n            if (downloadId < 0) {\n                return@pushToFront\n            }\n            stack.value.items\n                .lastOrNull()\n                ?.let {\n                    (it.instance as? Screen.Home)?.component?.revealItem(downloadId)\n                }\n        }\n    }\n\n    override fun openBrowser(url: String?) {\n        val intent = BrowserActivity.createIntent(\n            context = context,\n            url = url,\n        )\n        sendEffect(MainAppEffects.StartActivity(intent))\n    }\n\n    sealed interface MainAppEffects {\n        data class StartActivity(val intent: Intent) : MainAppEffects\n        data class SimpleNotificationNotification(val notificationModel: NotificationModel) : MainAppEffects\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/MainContent.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.about.AboutPage\nimport com.abdownloadmanager.android.pages.batchdownload.BatchDownloadSheet\nimport com.abdownloadmanager.android.pages.category.CategorySheet\nimport com.abdownloadmanager.android.pages.checksum.FileChecksumPage\nimport com.abdownloadmanager.android.pages.home.HomePage\nimport com.abdownloadmanager.android.pages.settings.SettingsPage\nimport com.abdownloadmanager.android.pages.credits.thirdpartylibraries.ThirdPartyLibrariesPage\nimport com.abdownloadmanager.android.pages.credits.translators.TranslatorsPage\nimport com.abdownloadmanager.android.pages.editdownload.EditDownloadSheet\nimport com.abdownloadmanager.android.pages.newqueue.NewQueueSheet\nimport com.abdownloadmanager.android.pages.onboarding.initialsetup.InitialSetupPage\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionsPage\nimport com.abdownloadmanager.android.pages.perhostsettings.PerHostSettingsPage\nimport com.abdownloadmanager.android.pages.queue.QueueConfigSheet\nimport com.abdownloadmanager.android.pages.updater.UpdaterSheet\nimport com.abdownloadmanager.android.util.compose.rememberIsUiVisible\nimport com.abdownloadmanager.shared.ui.widget.NotificationArea\nimport com.abdownloadmanager.shared.ui.widget.useNotification\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.ScreenSurface\nimport com.arkivanov.decompose.extensions.compose.stack.Children\nimport com.arkivanov.decompose.extensions.compose.stack.animation.fade\nimport com.arkivanov.decompose.extensions.compose.stack.animation.plus\nimport com.arkivanov.decompose.extensions.compose.stack.animation.scale\nimport com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withTimeout\n\n\n@Composable\nfun MainContent(\n    mainComponent: MainComponent,\n) {\n    val activity = LocalActivity.current\n    val notificationManager = useNotification()\n    val scope = rememberCoroutineScope()\n    ScreenSurface(\n        modifier = Modifier.fillMaxSize(),\n        background = myColors.background,\n        contentColor = myColors.onBackground\n    ) {\n        HandleEffects(mainComponent) { effect ->\n            when (effect) {\n                is MainComponent.MainAppEffects.StartActivity -> {\n                    activity?.startActivity(effect.intent)\n                }\n\n                is MainComponent.MainAppEffects.SimpleNotificationNotification -> {\n                    scope.launch {\n                        withTimeout(5000) {\n                            notificationManager.showNotification(effect.notificationModel)\n                        }\n                    }\n                }\n            }\n        }\n        Children(\n            mainComponent.stack.collectAsState().value,\n            modifier = Modifier.imePadding(),\n            animation = stackAnimation { scale() + fade() },\n        ) {\n            when (val screen = it.instance) {\n                is Screen.Home -> {\n                    HomePage(screen.component)\n                }\n\n                is Screen.Settings -> {\n                    SettingsPage(screen.component)\n                }\n\n                Screen.About -> {\n                    AboutPage(\n                        onRequestShowOpenSourceLibraries = {\n                            mainComponent.openOpenSourceLibrariesPage()\n                        },\n                        onRequestShowTranslators = {\n                            mainComponent.openTranslatorsPage()\n                        }\n                    )\n                }\n\n                Screen.OpenSourceThirdPartyLibraries -> {\n                    ThirdPartyLibrariesPage()\n                }\n\n                Screen.Translators -> {\n                    TranslatorsPage(\n                        onBack = {\n                            mainComponent.closeTranslatorsPage()\n                        }\n                    )\n                }\n\n                is Screen.PerHostSettings -> {\n                    PerHostSettingsPage(component = screen.component)\n                }\n\n                is Screen.FileChecksum -> {\n                    FileChecksumPage(component = screen.component)\n                }\n\n                is Screen.InitialSetup -> {\n                    InitialSetupPage(component = screen.component)\n                }\n\n                is Screen.Permissions -> {\n                    PermissionsPage(component = screen.component)\n                }\n            }\n        }\n        CategorySheet(\n            mainComponent.categorySlot.rememberChild(),\n            mainComponent::closeCategoryDialog\n        )\n        QueueConfigSheet(\n            mainComponent.queueConfigSlot.rememberChild(),\n            mainComponent::closeQueues\n        )\n        NewQueueSheet(\n            onQueueCreate = mainComponent::createQueueWithName,\n            isOpened = mainComponent.showAddQueue.collectAsState().value,\n            onCloseRequest = { mainComponent.setShowAddQueue(false) },\n        )\n        BatchDownloadSheet(\n            component = mainComponent.batchDownloadSlot.rememberChild(),\n            onDismiss = mainComponent::closeBatchDownload\n        )\n        EditDownloadSheet(\n            component = mainComponent.editDownloadSlot.rememberChild(),\n            onDismiss = mainComponent::closeEditDownloadDialog,\n        )\n        UpdaterSheet(\n            updaterComponent = mainComponent.updaterComponent,\n        )\n        val isUiVisible = rememberIsUiVisible()\n        LaunchedEffect(isUiVisible) {\n            mainComponent.abdmAppManager.setNotificationsHandledInUi(isUiVisible)\n        }\n        // is this really necessary?\n        DisposableEffect(Unit) {\n            onDispose {\n                mainComponent.abdmAppManager.setNotificationsHandledInUi(false)\n            }\n        }\n        if (isUiVisible) {\n            NotificationArea(\n                Modifier\n                    .fillMaxWidth()\n                    .align(Alignment.BottomEnd)\n                    .padding(bottom = 96.dp)\n                    .padding(horizontal = 24.dp)\n                    .navigationBarsPadding()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/SelectionControls.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\n\nobject SelectionControlsScope\n\n@Composable\nfun RenderControlSelections(\n    onRequestSelectAll: () -> Unit,\n    onRequestSelectInside: () -> Unit,\n    onRequestInvertSelection: () -> Unit,\n    selectionCount: Int,\n    total: Int,\n    otherActions: @Composable SelectionControlsScope.() -> Unit\n) {\n    with(SelectionControlsScope) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            RenderSelectAll(\n                onClick = onRequestSelectAll,\n                Modifier,\n            )\n            RenderSelectInside(\n                onClick = onRequestSelectInside,\n                Modifier,\n            )\n            RenderInvertSelection(\n                onClick = onRequestInvertSelection,\n                Modifier,\n            )\n            Text(\n                \"$selectionCount / $total\",\n                Modifier.weight(1f),\n                textAlign = TextAlign.Center,\n                fontWeight = FontWeight.Bold,\n            )\n            otherActions()\n        }\n    }\n}\n\n@Composable\ncontext(_: SelectionControlsScope)\nprivate fun RenderSelectAll(\n    onClick: () -> Unit,\n    modifier: Modifier,\n) {\n    SelectionControlButton(\n        icon = MyIcons.selectAll,\n        contentDescription = Res.string.select_all.asStringSource(),\n        modifier = modifier,\n        enabled = true,\n        toggledOff = false,\n        onClick = {\n            onClick()\n        },\n        padding = PaddingValues(12.dp),\n    )\n}\n\n@Composable\ncontext(_: SelectionControlsScope)\nprivate fun RenderSelectInside(\n    onClick: () -> Unit,\n    modifier: Modifier,\n) {\n    SelectionControlButton(\n        icon = MyIcons.selectInside,\n        contentDescription = Res.string.select_inside.asStringSource(),\n        modifier = modifier,\n        enabled = true,\n        toggledOff = false,\n        onClick = {\n            onClick()\n        },\n        padding = PaddingValues(12.dp),\n    )\n}\n\n@Composable\ncontext(_: SelectionControlsScope)\nprivate fun RenderInvertSelection(\n    onClick: () -> Unit,\n    modifier: Modifier,\n) {\n    SelectionControlButton(\n        icon = MyIcons.selectInvert,\n        contentDescription = Res.string.select_invert.asStringSource(),\n        modifier = modifier,\n        enabled = true,\n        toggledOff = false,\n        onClick = {\n            onClick()\n        },\n    )\n}\n\n@Composable\ncontext(_: SelectionControlsScope)\nfun SelectionControlButton(\n    icon: IconSource,\n    contentDescription: StringSource,\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    padding: PaddingValues = PaddingValues(12.dp),\n    toggledOff: Boolean = false,\n    enabled: Boolean = true,\n    shape: Shape = RectangleShape,\n) {\n    val size: Dp = 24.dp\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    Box(\n        modifier\n            .ifThen(!enabled || toggledOff) {\n                alpha(0.5f)\n            }\n            .ifThen(isFocused) {\n                border(\n                    1.dp,\n                    myColors.focusedBorderColor,\n                    shape\n                )\n            }\n            .clip(shape)\n            .clickable(\n                enabled = enabled,\n                indication = LocalIndication.current,\n                interactionSource = interactionSource,\n                onClick = onClick,\n            )\n            .padding(padding)\n    ) {\n        MyIcon(\n            icon,\n            contentDescription.rememberString(),\n            Modifier\n                .size(size)\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/SheetUI.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.areNavigationBarsVisible\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.consumeWindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.layout.systemBarsPadding\nimport androidx.compose.foundation.layout.width\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 com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\n\n@Composable\nfun ResponsiveDialogScope.SheetUI(\n    header: @Composable () -> Unit,\n    content: @Composable () -> Unit,\n) {\n    WithContentColor(myColors.onSurface) {\n        Column(\n            Modifier\n                .padding(\n                    WindowInsets.navigationBars\n                        .only(WindowInsetsSides.Horizontal)\n                        .asPaddingValues()\n                )\n                .fillMaxWidth()\n                .statusBarsPadding()\n                .clip(\n                    myShapes.createSheetWithCustomEdges(\n                        topStart = isTopStartFree,\n                        bottomStart = isBottomStartFree,\n                        topEnd = isTopEndFree,\n                        bottomEnd = isBottomEndFree,\n                    )\n                )\n                .background(myColors.surface)\n                .let { modifier ->\n                    val verticalNavigationBarPaddingValues = WindowInsets.navigationBars\n                        .only(WindowInsetsSides.Vertical)\n                        .asPaddingValues()\n                    modifier\n                        .padding(verticalNavigationBarPaddingValues)\n                        .consumeWindowInsets(verticalNavigationBarPaddingValues)\n                }\n                .imePadding()\n                .padding(mySpacings.smallSpace)\n        ) {\n            header()\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            Box(\n                Modifier\n                    .fillMaxWidth()\n            ) {\n                content()\n            }\n        }\n    }\n}\n\n@Composable\nfun SheetHeader(\n    headerTitle: @Composable () -> Unit = {},\n    headerActions: @Composable RowScope.() -> Unit = {},\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(vertical = mySpacings.mediumSpace)\n            .padding(horizontal = mySpacings.mediumSpace),\n    ) {\n        Box(Modifier.weight(1f)) {\n            headerTitle()\n        }\n        Spacer(Modifier.width(mySpacings.smallSpace))\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            headerActions()\n        }\n    }\n}\n\n@Composable\nfun SheetTitle(\n    title: String,\n    icon: IconSource? = null\n) {\n    Row(\n        Modifier.padding(start = mySpacings.mediumSpace, top = mySpacings.mediumSpace)\n    ) {\n        icon?.let {\n            MyIcon(icon, null)\n            Spacer(Modifier.width(mySpacings.mediumSpace))\n        }\n        Text(\n            text = title,\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.xl,\n            modifier = Modifier,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\n@Composable\nfun SheetTitleWithDescription(\n    title: String,\n    description: String,\n) {\n    Column {\n        Text(\n            text = title,\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.xl,\n            modifier = Modifier.padding(start = mySpacings.mediumSpace, top = mySpacings.mediumSpace)\n        )\n        Text(\n            text = description,\n            modifier = Modifier\n                .padding(start = mySpacings.mediumSpace, top = mySpacings.mediumSpace),\n            color = LocalContentColor.current / 0.75f\n        )\n\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/AndroidConfigurableUtils.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.Help\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.modifiers.autoMirror\nimport ir.amirab.util.ifThen\n\n\n@Composable\nfun ConfigTemplate(\n    modifier: Modifier,\n    title: @Composable ColumnScope.() -> Unit,\n    value: @Composable ColumnScope.() -> Unit,\n    nestedContent: @Composable ColumnScope.() -> Unit = {},\n) {\n    Column(\n        modifier\n    ) {\n        Row(\n            Modifier\n                .height(IntrinsicSize.Max),\n            horizontalArrangement = Arrangement.Center,\n        ) {\n            Column(\n                Modifier.weight(1f, true),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.Start,\n            ) {\n                title()\n            }\n            Column(\n                Modifier.fillMaxHeight(),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.End,\n            ) {\n                value()\n            }\n        }\n        Column(\n            Modifier.fillMaxWidth()\n        ) {\n            nestedContent()\n        }\n    }\n}\n\n@Composable\nfun <T> TitleAndDescription(\n    cfg: Configurable<T>,\n    describe: Boolean = true,\n    modifier: Modifier = Modifier,\n    contentPadding: PaddingValues = PaddingValues(8.dp),\n) {\n    val value = cfg.backedBy.collectAsState().value\n    val describedStringSource = remember(value) {\n        cfg.describe(value)\n    }\n    val describeContent = describedStringSource.rememberString()\n    TitleAndDescription(\n        cfg = cfg,\n        describe = describe,\n        modifier = modifier,\n        describeContent = describeContent,\n        contentPadding = contentPadding,\n    )\n}\n\n@Composable\nfun <T> TitleAndDescription(\n    cfg: Configurable<T>,\n    describe: Boolean = true,\n    describeContent: String,\n    describeWrapper: @Composable (@Composable () -> Unit) -> Unit = { it() },\n    modifier: Modifier = Modifier,\n    contentPadding: PaddingValues = PaddingValues(8.dp),\n) {\n    val enabled = isConfigEnabled()\n    Column(\n        modifier\n            .padding(contentPadding)\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n    ) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(\n                cfg.title.rememberString(),\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier.weight(1f, false)\n            )\n            if (cfg.description.rememberString().isNotBlank()) {\n                Spacer(Modifier.size(4.dp))\n                Help(\n                    Modifier.align(Alignment.Top),\n                    cfg\n                )\n            }\n        }\n        if (describe) {\n            if (describeContent.isNotBlank()) {\n                Spacer(Modifier.size(4.dp))\n                describeWrapper {\n                    WithContentAlpha(0.75f) {\n                        AnimatedContent(\n                            targetState = describeContent,\n                            transitionSpec = {\n                                scaleIn() + fadeIn() togetherWith scaleOut() + fadeOut()\n                            }\n                        ) { content ->\n                            Text(\n                                content,\n                                fontSize = myTextSizes.base,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun NextIcon() {\n    MyIcon(\n        MyIcons.next,\n        null,\n        Modifier\n            .size(16.dp)\n            .autoMirror()\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/ConfigurableSheet.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport ir.amirab.util.compose.StringSource\n\n@Composable\nfun ConfigurableSheet(\n    title: StringSource,\n    isOpened: Boolean,\n    onDismiss: () -> Unit,\n    headerActions: @Composable RowScope.() -> Unit = {},\n    content: @Composable () -> Unit,\n) {\n    val dialogState = rememberResponsiveDialogState(isOpened)\n    LaunchedEffect(isOpened) {\n        when (isOpened) {\n            true -> dialogState.show()\n            false -> dialogState.hide()\n        }\n    }\n    dialogState.OnFullyDismissed {\n        onDismiss()\n    }\n    ResponsiveDialog(\n        state = dialogState,\n        onDismiss = dialogState::hide,\n    ) {\n        SheetUI(\n            header = {\n                SheetHeader(\n                    headerTitle = {\n                        SheetTitle(\n                            title.rememberString()\n                        )\n                    },\n                    headerActions = headerActions,\n                )\n            }\n        ) {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/SheetInput.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable\n\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.width\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Immutable\ndata class InputParams<T>(\n    val editingValue: T,\n    val setEditingValue: (T) -> Unit,\n    val modifier: Modifier,\n    val keyboardActions: KeyboardActions,\n)\n\n@Composable\nfun <T> SheetInput(\n    configurable: Configurable<T>,\n    isOpened: Boolean,\n    onDismiss: () -> Unit,\n    onConfirm: (T) -> Unit,\n    inputContent: @Composable (InputParams<T>) -> Unit,\n) {\n    SheetInput(\n        title = configurable.title,\n        validate = configurable.validate,\n        isOpened = isOpened,\n        onDismiss = onDismiss,\n        onConfirm = onConfirm,\n        inputContent = inputContent,\n        initialValue = { configurable.stateFlow.value },\n    )\n}\n\n@Composable\nfun <T> SheetInput(\n    title: StringSource,\n    validate: (T) -> Boolean,\n    isOpened: Boolean,\n    initialValue: () -> T,\n    onDismiss: () -> Unit,\n    onConfirm: (T) -> Unit,\n    inputContent: @Composable (InputParams<T>) -> Unit,\n) {\n    ConfigurableSheet(\n        title = title,\n        onDismiss = onDismiss,\n        isOpened = isOpened,\n        headerActions = {\n            TransparentIconActionButton(\n                MyIcons.close,\n                contentDescription = Res.string.close.asStringSource(),\n                onClick = onDismiss\n            )\n        }\n    ) {\n        Column(\n            Modifier.padding(horizontal = mySpacings.mediumSpace)\n        ) {\n            var editingValue by remember(initialValue) {\n                mutableStateOf(initialValue())\n            }\n            val isInputValid = remember(validate, editingValue) {\n                validate(editingValue)\n            }\n            val fr = remember { FocusRequester() }\n            LaunchedEffect(Unit) {\n                fr.requestFocus()\n            }\n            inputContent(\n                InputParams(\n                    editingValue = editingValue,\n                    setEditingValue = {\n                        editingValue = it\n                    },\n                    modifier = Modifier\n                        .focusRequester(fr),\n                    keyboardActions = KeyboardActions(\n                        onDone = {\n                            if (isInputValid) {\n                                onConfirm(editingValue)\n                            }\n                        },\n                    )\n                )\n            )\n\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n            Row {\n                ActionButton(\n                    text = myStringResource(Res.string.cancel),\n                    onClick = onDismiss,\n                    modifier = Modifier.weight(1f),\n                )\n                Spacer(Modifier.width(mySpacings.mediumSpace))\n                ActionButton(\n                    text = myStringResource(Res.string.ok),\n                    onClick = {\n                        onConfirm(editingValue)\n                    },\n                    modifier = Modifier.weight(1f),\n                    enabled = isInputValid,\n                )\n            }\n            Spacer(Modifier.height(mySpacings.mediumSpace))\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/SheetSpinner.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\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.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateMapOf\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.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInParent\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.defaultValueToString\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.VerticalScrollableContent\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\nimport kotlin.collections.set\n\n\n@Composable\nfun <T> RenderSpinnerInSheet(\n    title: StringSource,\n    isOpened: Boolean,\n    onDismiss: () -> Unit,\n    possibleValues: List<T>,\n    value: T,\n    onSelect: (T) -> Unit,\n    valueToString: (T) -> List<String> = ::defaultValueToString,\n//    minWidth:Dp,\n    render: @Composable (T) -> Unit,\n) {\n    val verticalPadding = 4.dp\n    val horizontalPadding = 16.dp\n\n    val shape = myShapes.defaultRounded\n    val borderWidth = 1.dp\n    ConfigurableSheet(\n        title = title,\n        isOpened = isOpened,\n        onDismiss = onDismiss,\n        headerActions = {\n            TransparentIconActionButton(\n                MyIcons.close,\n                contentDescription = Res.string.close.asStringSource(),\n                onClick = onDismiss\n            )\n        }\n    ) {\n        val focusRequester = remember { FocusRequester() }\n        LaunchedEffect(Unit) {\n            focusRequester.requestFocus()\n        }\n        val possibleValuePositions = remember(possibleValues) {\n            mutableStateMapOf<Int, Float>()\n        }\n        var itemToBeIndicated: Int by remember {\n            mutableStateOf(-1)\n        }\n        val scrollState = rememberScrollState()\n        VerticalScrollableContent(scrollState) {\n            Column(\n                Modifier\n                    .focusRequester(focusRequester)\n                    .clip(shape)\n                    .verticalScroll(scrollState)\n            ) {\n                WithContentColor(myColors.onSurface) {\n                    for ((index, p) in possibleValues.withIndex()) {\n                        key(p) {\n                            val isIndicating = itemToBeIndicated == index\n                            Row(\n                                modifier = Modifier\n                                    .onGloballyPositioned {\n                                        possibleValuePositions[index] = it.positionInParent().y\n                                    }\n                                    .ifThen(isIndicating) {\n                                        background(\n                                            myColors.onBackground / 0.05f\n                                        )\n                                    }\n                                    .clickable(onClick = {\n                                        onSelect(p)\n                                    })\n                                    .heightIn(mySpacings.thumbSize)\n                                    .padding(horizontal = horizontalPadding),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                val selected = p == value\n                                WithContentAlpha(if (selected) 1f else 0.75f) {\n                                    Box(\n                                        Modifier\n                                            .weight(1f)\n                                            .padding(vertical = verticalPadding)\n                                    ) {\n                                        render(p)\n                                    }\n                                }\n                                Spacer(\n                                    Modifier.width(borderWidth)\n                                )\n                                if (selected) {\n                                    MyIcon(\n                                        MyIcons.check, null, Modifier\n                                            .padding(4.dp)\n                                            .size(16.dp)\n                                    )\n                                }\n                            }\n                            Spacer(\n                                Modifier\n                                    .fillMaxWidth()\n                                    .height(1.dp)\n                                    .background(\n                                        Brush.horizontalGradient(\n                                            listOf(\n                                                myColors.onSurface / 0.05f,\n                                                myColors.onSurface / 0.1f,\n                                                myColors.onSurface / 0.05f,\n                                            )\n                                        )\n                                    )\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/android/AndroidConfigurableRenderers.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.android\n\nimport com.abdownloadmanager.android.ui.configurable.android.item.PermissionConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ContainsConfigurableRenderers\n\ndata class AndroidConfigurableRenderers(\n    val permissionConfigurableRenderers: ConfigurableRenderer<PermissionConfigurable>,\n) : ContainsConfigurableRenderers {\n    override fun getAllRenderers(): Map<Configurable.Key, ConfigurableRenderer<*>> {\n        return mapOf(\n            PermissionConfigurable.Key to permissionConfigurableRenderers,\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/android/item/PermissionConfigurable.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.android.item\n\nimport com.abdownloadmanager.android.pages.onboarding.permissions.AppPermission\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass PermissionConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<AppPermission>,\n    describe: () -> StringSource = { \"\".asStringSource() },\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<AppPermission>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = {\n        describe()\n    },\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/android/renderer/PermissionConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.android.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport com.abdownloadmanager.android.pages.onboarding.permissions.AppPermissionState\nimport com.abdownloadmanager.android.pages.onboarding.permissions.rememberAppPermissionState\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.android.ui.configurable.android.item.PermissionConfigurable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.compose.asStringSource\n\nobject PermissionConfigurableRenderer : ConfigurableRenderer<PermissionConfigurable> {\n    @Composable\n    override fun RenderConfigurable(\n        configurable: PermissionConfigurable,\n        configurableUiProps: ConfigurableUiProps\n    ) {\n        val permission by configurable.stateFlow.collectAsState()\n        val permissionState = rememberAppPermissionState(permission) { result ->\n\n        }\n\n        RenderPermissionConfigurable(\n            cfg = configurable,\n            configurableUiProps = configurableUiProps,\n            permissionState = permissionState\n        )\n    }\n\n    @Composable\n    fun RenderPermissionConfigurable(\n        cfg: PermissionConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n        permissionState: AppPermissionState,\n    ) {\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    permissionState.launchRequest()\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(\n                    cfg = cfg,\n                    describe = true,\n                    describeContent = if (permissionState.isGranted) {\n                        Res.string.permission_granted\n                    } else {\n                        Res.string.permission_not_granted\n                    }.asStringSource().rememberString(),\n                    describeWrapper = { content ->\n                        val contentColor =\n                            if (permissionState.isGranted) myColors.success else myColors.warning\n                        CompositionLocalProvider(\n                            LocalContentColor provides contentColor\n                        ) {\n                            content()\n                        }\n                    }\n                )\n            },\n            value = {\n                NextIcon()\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/CommonConfigurableRenderersForAndroid.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon\n\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.BooleanConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.DayOfWeekConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.EnumConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.FileChecksumConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.FloatConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.FolderConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.IntConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.LongConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.NavigatableConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.ProxyConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.SpeedLimitConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.StringConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.ThemeConfigurableRenderer\nimport com.abdownloadmanager.android.ui.configurable.comon.renderer.TimeConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.CommonConfigurableRenderers\n\nval CommonConfigurableRenderersForAndroid = CommonConfigurableRenderers(\n    booleanConfigurableRenderer = BooleanConfigurableRenderer,\n    dayOfWeekConfigurableRenderer = DayOfWeekConfigurableRenderer,\n    fileChecksumConfigurableRenderer = FileChecksumConfigurableRenderer,\n    floatConfigurableRenderer = FloatConfigurableRenderer,\n    folderConfigurableRenderer = FolderConfigurableRenderer,\n    intConfigurableRenderer = IntConfigurableRenderer,\n    longConfigurableRenderer = LongConfigurableRenderer,\n    perHostSettingsConfigurableRenderer = NavigatableConfigurableRenderer,\n    enumConfigurableRenderer = EnumConfigurableRenderer,\n    speedConfigurableRenderer = SpeedLimitConfigurableRenderer,\n    stringConfigurableRenderer = StringConfigurableRenderer,\n    themeConfigurableRenderer = ThemeConfigurableRenderer,\n    timeConfigurableRenderer = TimeConfigurableRenderer,\n    proxyConfigurableRenderer = ProxyConfigurableRenderer,\n)\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/ConfigurableRenderersForAndroid.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon\n\nimport com.abdownloadmanager.android.ui.configurable.android.AndroidConfigurableRenderers\nimport com.abdownloadmanager.android.ui.configurable.android.renderer.PermissionConfigurableRenderer\n\nval ConfigurableRenderersForAndroid = AndroidConfigurableRenderers(\n    permissionConfigurableRenderers = PermissionConfigurableRenderer\n)\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/BooleanConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Switch\n\nobject BooleanConfigurableRenderer : ConfigurableRenderer<BooleanConfigurable> {\n    @Composable\n    override fun RenderConfigurable(\n        configurable: BooleanConfigurable,\n        configurableUiProps: ConfigurableUiProps\n    ) {\n        RenderBooleanConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderBooleanConfig(\n        cfg: BooleanConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n    ) {\n        val checked = cfg.stateFlow.collectAsState().value\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    setValue(!checked)\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    BooleanConfigurable.RenderMode.Checkbox -> {\n                        CheckBox(\n                            value = checked,\n                            enabled = enabled,\n                            onValueChange = {\n                                setValue(it)\n                            }\n                        )\n                    }\n\n                    BooleanConfigurable.RenderMode.Switch -> {\n                        Switch(\n                            checked = checked,\n                            enabled = enabled,\n                            onCheckedChange = {\n                                setValue(it)\n                            }\n                        )\n                    }\n                }\n            })\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/DayOfWeekConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\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.heightIn\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.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.DayOfWeekConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\nimport kotlinx.datetime.DayOfWeek\n\nobject DayOfWeekConfigurableRenderer : ConfigurableRenderer<DayOfWeekConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: DayOfWeekConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderDayOfWeekConfigurable(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val allDays = DayOfWeek.entries.toSet()\n        val enabled = isConfigEnabled()\n        fun isSelected(dayOfWeek: DayOfWeek): Boolean {\n            return dayOfWeek in value\n        }\n\n        fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) {\n            if (!enabled) return\n            if (select) {\n                setValue(\n                    value.plus(dayOfWeek).sorted().toSet()\n                )\n            } else {\n                setValue(\n                    value.minus(dayOfWeek).sorted().toSet()\n                )\n            }\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {},\n            nestedContent = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Row(\n                        Modifier.ifThen(!enabled) {\n                            alpha(0.5f)\n                        }\n                    ) {\n                        FlowRow(Modifier.fillMaxWidth()) {\n                            allDays.forEach { dayOfWeek ->\n                                RenderDayOfWeek(\n                                    modifier = Modifier,\n                                    enabled = enabled,\n                                    dayOfWeek = dayOfWeek,\n                                    selected = isSelected(dayOfWeek),\n                                    onSelect = { s, isSelected ->\n                                        selectDay(dayOfWeek, isSelected)\n                                    }\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        )\n    }\n\n    @Composable\n    fun RenderDayOfWeek(\n        modifier: Modifier,\n        dayOfWeek: DayOfWeek,\n        selected: Boolean,\n        onSelect: (DayOfWeek, Boolean) -> Unit,\n        enabled: Boolean = true,\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = modifier\n                .heightIn(mySpacings.thumbSize)\n                .padding(2.dp)\n                .clip(myShapes.defaultRounded)\n                .ifThen(selected) {\n                    background(myColors.onBackground / 10)\n                }\n                .clickable(enabled = enabled) {\n                    onSelect(dayOfWeek, !selected)\n                }\n                .padding(vertical = 4.dp)\n                .padding(horizontal = 8.dp)\n\n        ) {\n            MyIcon(\n                MyIcons.check,\n                null,\n                Modifier\n                    .size(16.dp)\n                    .alpha(if (selected) 1f else 0f),\n            )\n            Spacer(Modifier.width(4.dp))\n            Text(\n                text = dayOfWeek.asStringSource().rememberString(),\n                modifier = Modifier.alpha(\n                    if (selected) 1f\n                    else 0.5f\n                ),\n                softWrap = false,\n                fontSize = myTextSizes.base,\n            )\n        }\n    }\n\n    private fun DayOfWeek.asStringSource() = when (this) {\n        DayOfWeek.MONDAY -> Res.string.monday\n        DayOfWeek.TUESDAY -> Res.string.tuesday\n        DayOfWeek.WEDNESDAY -> Res.string.wednesday\n        DayOfWeek.THURSDAY -> Res.string.thursday\n        DayOfWeek.FRIDAY -> Res.string.friday\n        DayOfWeek.SATURDAY -> Res.string.saturday\n        DayOfWeek.SUNDAY -> Res.string.sunday\n    }.asStringSource()\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/EnumConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.RenderSpinnerInSheet\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.EnumConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject EnumConfigurableRenderer : ConfigurableRenderer<EnumConfigurable<Any>> {\n    @Composable\n    override fun RenderConfigurable(configurable: EnumConfigurable<Any>, configurableUiProps: ConfigurableUiProps) {\n        RenderEnumConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun <T> RenderEnumConfig(cfg: EnumConfigurable<T>, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val index = remember(cfg.possibleValues, value) {\n            cfg.possibleValues.indexOf(value)\n        }\n        val enabled = isConfigEnabled()\n\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    isOpened = true\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                Column {\n                    TitleAndDescription(cfg, true)\n                }\n            },\n            value = {\n                NextIcon()\n            }\n        )\n        RenderSpinnerInSheet(\n            title = cfg.title,\n            onDismiss = onDismiss,\n            isOpened = isOpened,\n            possibleValues = cfg.possibleValues,\n            value = value,\n            onSelect = {\n                setValue(it)\n                onDismiss()\n            },\n            valueToString = cfg.valueToString,\n            render = {\n                Text(cfg.describe(it).rememberString())\n            })\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/FileChecksumConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.FileChecksumAlgorithm\nimport ir.amirab.util.compose.resources.myStringResource\n\nobject FileChecksumConfigurableRenderer : ConfigurableRenderer<FileChecksumConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FileChecksumConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFileChecksumConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFileChecksumConfig(cfg: FileChecksumConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val enabled = isConfigEnabled()\n        val hasFileChecksum = value != null\n        ConfigTemplate(\n            configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    TitleAndDescription(cfg, true)\n                }\n            },\n            nestedContent = {\n                Column(Modifier.align(Alignment.End)) {\n                    AnimatedVisibility(\n                        hasFileChecksum,\n                    ) {\n                        value?.let { value ->\n                            Row(\n                                Modifier.padding(vertical = 8.dp),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                RenderSpinner(\n                                    possibleValues = FileChecksumAlgorithm\n                                        .all()\n                                        .map { it.algorithm },\n                                    value = value.algorithm,\n                                    modifier = Modifier.Companion,\n                                    enabled = enabled,\n                                    onSelect = {\n                                        setValue(value.copy(algorithm = it))\n                                    }\n                                ) {\n                                    Text(it)\n                                }\n                                Text(\":\", Modifier.padding(horizontal = 4.dp))\n                                MyTextField(\n                                    text = value.value,\n                                    onTextChange = {\n                                        setValue(value.copy(value = it))\n                                    },\n                                    shape = RectangleShape,\n                                    textPadding = PaddingValues(4.dp),\n                                    enabled = enabled,\n                                    modifier = Modifier.weight(1f),\n                                    placeholder = myStringResource(Res.string.file_checksum),\n                                )\n                            }\n                        }\n                    }\n                }\n            },\n            value = {\n                CheckBox(\n                    value = hasFileChecksum,\n                    enabled = enabled,\n                    onValueChange = {\n                        if (it) {\n                            setValue(\n                                FileChecksum(\n                                    FileChecksumAlgorithm.default().algorithm,\n                                    \"\",\n                                )\n                            )\n                        } else {\n                            setValue(null)\n                        }\n                    })\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/FloatConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.text.KeyboardOptions\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.Modifier\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.FloatConfigurable\nimport com.abdownloadmanager.shared.ui.widget.FloatTextField\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject FloatConfigurableRenderer : ConfigurableRenderer<FloatConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FloatConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFloatConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFloatConfig(cfg: FloatConfigurable, configurableUiProps: ConfigurableUiProps) {\n//        val value by cfg.stateFlow.collectAsState()\n//        val setValue = cfg::set\n//        val enabled = isConfigEnabled()\n\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable { isOpened = true }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    FloatConfigurable.RenderMode.TextField -> {\n                        NextIcon()\n                        RenderTextFieldFloatInput(cfg = cfg, isOpened = isOpened, onDismiss = onDismiss)\n                    }\n                }\n            }\n        )\n    }\n\n    @Composable\n    fun RenderTextFieldFloatInput(\n        cfg: FloatConfigurable,\n        isOpened: Boolean,\n        onDismiss: () -> Unit,\n    ) {\n        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n\n        SheetInput(\n            configurable = cfg,\n            isOpened = isOpened,\n            onDismiss = onDismiss,\n            inputContent = { params ->\n                FloatTextField(\n                    value = params.editingValue,\n                    onValueChange = { v ->\n                        params.setEditingValue(v)\n                    },\n                    interactionSource = interactionSource,\n                    range = cfg.range,\n                    modifier = params.modifier.fillMaxWidth(),\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Decimal,\n                        imeAction = ImeAction.Done,\n                    ),\n                    textPadding = PaddingValues(8.dp),\n                    keyboardActions = params.keyboardActions,\n                    placeholder = \"\",\n                )\n            },\n            onConfirm = {\n                cfg.set(it)\n                onDismiss()\n            },\n        )\n\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/FolderConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.pages.directorypicker.rememberAndroidDirectoryPickerLauncher\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.FolderConfigurable\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport java.io.File\n\nobject FolderConfigurableRenderer : ConfigurableRenderer<FolderConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FolderConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFolderConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFolderConfig(cfg: FolderConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val pickFolderLauncher = rememberAndroidDirectoryPickerLauncher(\n            title = cfg.title,\n            initialDirectory = remember(value) {\n                runCatching {\n                    File(value).canonicalPath\n                }.getOrNull()\n            },\n        ) { directory ->\n            directory?.let(setValue)\n        }\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable { pickFolderLauncher.launch() }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                NextIcon()\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/IntConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.FloatConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.widget.FloatTextField\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject IntConfigurableRenderer : ConfigurableRenderer<IntConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: IntConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderIntegerConfig(configurable, configurableUiProps)\n    }\n\n\n    private operator fun IntRange.get(index: Int): Int {\n        return (start + index).also {\n            if (it > last) {\n                throw IndexOutOfBoundsException(\"$it bigger that $last\")\n            }\n        }\n\n    }\n\n    @Composable\n    private fun RenderIntegerConfig(cfg: IntConfigurable, configurableUiProps: ConfigurableUiProps) {\n//        val value by cfg.stateFlow.collectAsState()\n//        val setValue = cfg::set\n//        val enabled = isConfigEnabled()\n\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable { isOpened = true }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    IntConfigurable.RenderMode.TextField -> {\n                        NextIcon()\n                        RenderTextFieldIntInput(cfg = cfg, isOpened = isOpened, onDismiss = onDismiss)\n                    }\n                }\n            })\n    }\n\n    @Composable\n    fun RenderTextFieldIntInput(\n        cfg: IntConfigurable,\n        isOpened: Boolean,\n        onDismiss: () -> Unit,\n    ) {\n        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n\n        SheetInput(\n            configurable = cfg,\n            isOpened = isOpened,\n            onDismiss = onDismiss,\n            inputContent = { params ->\n                IntTextField(\n                    value = params.editingValue,\n                    onValueChange = { v ->\n                        params.setEditingValue(v)\n                    },\n                    interactionSource = interactionSource,\n                    range = cfg.range,\n                    modifier = params.modifier.fillMaxWidth(),\n                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                    keyboardActions = params.keyboardActions,\n                    textPadding = PaddingValues(8.dp),\n                    placeholder = \"\",\n                )\n            },\n            onConfirm = {\n                cfg.set(it)\n                onDismiss()\n            },\n        )\n\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/LongConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.LongConfigurable\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.ui.widget.LongTextField\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject LongConfigurableRenderer : ConfigurableRenderer<LongConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: LongConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderLongConfig(configurable, configurableUiProps)\n    }\n\n\n    private operator fun LongRange.get(index: Int): Long {\n        return (start + index).also {\n            if (it > last) {\n                throw IndexOutOfBoundsException(\"$it bigger that $last\")\n            }\n        }\n    }\n\n    @Composable\n    private fun RenderLongConfig(cfg: LongConfigurable, configurableUiProps: ConfigurableUiProps) {\n//        val value by cfg.stateFlow.collectAsState()\n//        val setValue = cfg::set\n//        val enabled = isConfigEnabled()\n\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable { isOpened = true }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    LongConfigurable.RenderMode.TextField -> {\n                        NextIcon()\n\n                        RenderTextFieldIntInput(\n                            cfg = cfg, isOpened = isOpened, onDismiss = onDismiss\n                        )\n                    }\n                }\n            })\n    }\n\n    @Composable\n    fun RenderTextFieldIntInput(\n        cfg: LongConfigurable,\n        isOpened: Boolean,\n        onDismiss: () -> Unit,\n    ) {\n        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n        SheetInput(\n            configurable = cfg,\n            isOpened = isOpened,\n            onDismiss = onDismiss,\n            inputContent = { params ->\n                LongTextField(\n                    value = params.editingValue,\n                    onValueChange = { v ->\n                        params.setEditingValue(v)\n                    },\n                    interactionSource = interactionSource,\n                    range = cfg.range,\n                    modifier = params.modifier.fillMaxWidth(),\n                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                    keyboardActions = params.keyboardActions,\n                    textPadding = PaddingValues(8.dp),\n                    placeholder = \"\",\n                )\n            },\n            onConfirm = {\n                cfg.set(it)\n                onDismiss()\n            },\n        )\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/NavigatableConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.NavigatableConfigurable\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject NavigatableConfigurableRenderer : ConfigurableRenderer<NavigatableConfigurable> {\n    @Composable\n    override fun RenderConfigurable(\n        configurable: NavigatableConfigurable,\n        configurableUiProps: ConfigurableUiProps\n    ) {\n        RenderPerHostSettingsConfigurable(\n            cfg = configurable,\n            configurableUiProps = configurableUiProps,\n            onRequestOpenConfigWindow = configurable.onRequestNavigate,\n        )\n    }\n\n    @Composable\n    private fun RenderPerHostSettingsConfigurable(\n        cfg: NavigatableConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n        onRequestOpenConfigWindow: () -> Unit\n    ) {\n//    val value by cfg.stateFlow.collectAsState()\n//    val setValue = cfg::set\n//    val enabled = isConfigEnabled()\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    onRequestOpenConfigWindow()\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                NextIcon()\n            },\n        )\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/ProxyConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\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.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\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.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.text.KeyboardOptions\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\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.graphics.Brush\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.ConfigurableSheet\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.ui.widget.Help\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.ui.widget.Multiselect\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.RadioButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\nimport com.abdownloadmanager.shared.util.proxy.ProxyMode\nimport com.abdownloadmanager.shared.util.proxy.ProxyRules\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.connection.proxy.Proxy\nimport ir.amirab.downloader.connection.proxy.ProxyType\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\nobject ProxyConfigurableRenderer : ConfigurableRenderer<ProxyConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: ProxyConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderProxyConfig(configurable, configurableUiProps)\n    }\n\n\n    @Composable\n    fun RenderProxyConfig(cfg: ProxyConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        var proxyConfigState by remember {\n            mutableStateOf(null as ProxyEditState?)\n        }\n        val dismiss = {\n            proxyConfigState = null\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable(\n                    onClick = {\n                        proxyConfigState = ProxyEditState(\n                            proxyData = value,\n                            setProxyData = {\n                                setValue(it)\n                                dismiss()\n                            }\n                        )\n                    }\n                )\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                NextIcon()\n            }\n        )\n        proxyConfigState?.let {\n            ProxyEditDialog(it, onDismiss = dismiss)\n        }\n    }\n\n    @Stable\n    private class ProxyEditState(\n        private val proxyData: ProxyData,\n        private val setProxyData: (ProxyData) -> Unit,\n    ) {\n        var proxyMode = mutableStateOf(proxyData.proxyMode)\n\n        //pac\n        var pacURL = mutableStateOf(proxyData.pac.uri)\n\n        //manual\n        var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type)\n\n        var proxyHost = mutableStateOf(proxyData.proxyWithRules.proxy.host)\n        var proxyPort = mutableStateOf(proxyData.proxyWithRules.proxy.port)\n\n        var useAuth = mutableStateOf(proxyData.proxyWithRules.proxy.username != null)\n        var proxyUsername = mutableStateOf(proxyData.proxyWithRules.proxy.username.orEmpty())\n        var proxyPassword = mutableStateOf(proxyData.proxyWithRules.proxy.password.orEmpty())\n\n        var excludeURLPatterns = mutableStateOf(proxyData.proxyWithRules.rules.excludeURLPatterns.joinToString(\" \"))\n\n        val canSave: Boolean by derivedStateOf {\n            when (proxyMode.value) {\n                ProxyMode.Direct -> true\n                ProxyMode.Manual -> {\n                    val hostValid = proxyHost.value.isNotBlank()\n                    hostValid\n                }\n                // at the moment these two not supported on android\n                ProxyMode.UseSystem -> false\n                ProxyMode.Pac -> false\n//                ProxyMode.Pac -> {\n//                    HttpUrlUtils.isValidUrl(pacURL.value)\n//                }\n            }\n\n        }\n\n        fun save() {\n            val useAuth = useAuth.value\n            if (!canSave) {\n                return\n            }\n            setProxyData(\n                proxyData.copy(\n                    proxyMode = proxyMode.value,\n                    pac = proxyData.pac.copy(pacURL.value),\n                    proxyWithRules = proxyData.proxyWithRules.copy(\n                        proxy = Proxy(\n                            type = proxyType.value,\n                            host = proxyHost.value.trim(),\n                            port = proxyPort.value,\n                            username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth },\n                            password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth },\n                        ),\n                        rules = ProxyRules(\n                            excludeURLPatterns = excludeURLPatterns.value\n                                .split(\" \")\n                                .map { it.trim() }\n                                .filterNot { it.isEmpty() },\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    @Composable\n    fun RenderChangeProxyConfig() {\n        NextIcon()\n    }\n\n\n    @Composable\n    private fun ProxyEditDialog(\n        state: ProxyEditState?,\n        onDismiss: () -> Unit,\n    ) {\n        val headerTitle = Res.string.proxy_change_title.asStringSource()\n        ConfigurableSheet(\n            title = headerTitle,\n            onDismiss = onDismiss,\n            isOpened = state != null,\n            content = {\n                state?.let { state ->\n                    val (mode, setMode) = state.proxyMode\n\n                    val shape = myShapes.defaultRounded\n                    Column(\n                        Modifier\n                            .verticalScroll(rememberScrollState())\n                    ) {\n                        Accordion(\n                            wrapItem = { item, content ->\n                                val selected = item == mode\n                                Box(\n                                    Modifier.ifThen(selected) {\n                                        Modifier\n                                            .clip(shape)\n                                            .border(1.dp, myColors.onBackground / 0.15f, shape)\n                                            .background(myColors.background / 25)\n                                    }\n                                ) {\n                                    content()\n                                }\n                            },\n                            possibleValues = ProxyMode.usableValues(),\n                            selectedItem = mode,\n                            renderHeader = {\n                                val selected = it == mode\n                                Row(\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .clip(shape)\n                                        .clickable { setMode(it) }\n                                        .padding(8.dp)\n                                        .padding(\n                                            animateDpAsState(\n                                                if (selected) 8.dp else 4.dp\n                                            ).value\n                                        )\n                                ) {\n                                    RadioButton(\n                                        value = selected,\n                                        onValueChange = {},\n                                    )\n                                    Spacer(Modifier.width(8.dp))\n                                    Text(\n                                        text = it.asStringSource().rememberString(),\n                                        fontSize = if (selected) {\n                                            myTextSizes.lg\n                                        } else {\n                                            myTextSizes.base\n                                        },\n                                        fontWeight = if (selected) {\n                                            FontWeight.Bold\n                                        } else {\n                                            null\n                                        }\n                                    )\n                                }\n                            },\n                            renderContent = {\n                                val cm = Modifier\n                                    .fillMaxWidth()\n                                    .padding(\n                                        vertical = 12.dp,\n                                        horizontal = 16.dp\n                                    )\n                                when (it) {\n                                    ProxyMode.Direct -> {\n                                    }\n\n                                    ProxyMode.UseSystem -> {\n                                    }\n\n                                    ProxyMode.Manual -> {\n                                        Column(cm) {\n                                            RenderManualConfig(state)\n                                        }\n                                    }\n\n                                    ProxyMode.Pac -> {\n//                                            Column(cm) {\n//                                                RenderPACConfig(state)\n//                                            }\n                                    }\n                                }\n                            }\n                        )\n                        ProxyConfigSpacer()\n                        Row {\n                            val btnModifier = Modifier.weight(1f)\n                            ActionButton(\n                                myStringResource(Res.string.change),\n                                enabled = state.canSave,\n                                modifier = btnModifier,\n                                onClick = {\n                                    state.save()\n                                })\n                            Spacer(Modifier.width(mySpacings.mediumSpace))\n                            ActionButton(\n                                myStringResource(Res.string.cancel),\n                                modifier = btnModifier,\n                                onClick = {\n                                    onDismiss()\n                                })\n                        }\n                    }\n                }\n            }\n        )\n    }\n\n    @Composable\n    private fun RenderPACConfig(\n        state: ProxyEditState,\n    ) {\n        Column {\n            val (url, setPacUrl) = state.pacURL\n            DialogConfigItem(\n                modifier = Modifier.Companion,\n                title = {\n                    Text(myStringResource(Res.string.proxy_pac_url))\n                },\n                value = {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        MyTextField(\n                            text = url,\n                            onTextChange = setPacUrl,\n                            placeholder = \"http://path/to/file.pac\",\n                            modifier = Modifier.weight(1f),\n                        )\n                    }\n                }\n            )\n        }\n    }\n\n    @Composable\n    private fun RenderManualConfig(\n        state: ProxyEditState,\n    ) {\n        val (type, setType) = state.proxyType\n        val (host, setHost) = state.proxyHost\n        val (port, setPort) = state.proxyPort\n        val (useAuth, setUseAuth) = state.useAuth\n        val (username, setUsername) = state.proxyUsername\n        val (password, setPassword) = state.proxyPassword\n        val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Text(myStringResource(Res.string.proxy_type))\n            },\n            value = {\n                Multiselect(\n                    selections = ProxyType.entries.toList(),\n                    selectedItem = type,\n                    onSelectionChange = setType,\n                    modifier = Modifier.Companion,\n                    render = {\n                        Text(\n                            it.name,\n                            modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),\n                        )\n                    },\n                    selectedColor = LocalContentColor.current / 15,\n                    unselectedAlpha = 0.8f,\n                )\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Text(myStringResource(Res.string.address_and_port))\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = host,\n                        onTextChange = setHost,\n                        placeholder = \"127.0.0.1\",\n                        modifier = Modifier.weight(1f),\n                    )\n                    Text(\":\", Modifier.padding(horizontal = 8.dp))\n                    IntTextField(\n                        value = port,\n                        onValueChange = setPort,\n                        placeholder = myStringResource(Res.string.port),\n                        range = 1..65535,\n                        modifier = Modifier.width(120.dp),\n                        keyboardOptions = KeyboardOptions(),\n                        textPadding = PaddingValues(8.dp),\n                        shape = RoundedCornerShape(12.dp),\n                    )\n                }\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Row(\n                    modifier = Modifier.clickable {\n                        setUseAuth(!useAuth)\n                    }\n                ) {\n                    CheckBox(\n                        value = useAuth,\n                        onValueChange = setUseAuth,\n                        size = 16.dp\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Text(myStringResource(Res.string.use_authentication))\n                }\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = username,\n                        onTextChange = setUsername,\n                        placeholder = myStringResource(Res.string.username),\n                        modifier = Modifier.weight(1f),\n                        enabled = useAuth,\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    MyTextField(\n                        text = password,\n                        onTextChange = setPassword,\n                        placeholder = myStringResource(Res.string.password),\n                        modifier = Modifier.weight(1f),\n                        enabled = useAuth,\n                    )\n                }\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Row {\n                    Text(myStringResource(Res.string.proxy_do_not_use_proxy_for))\n                    Spacer(Modifier.width(8.dp))\n                    Help(\n                        myStringResource(Res.string.proxy_do_not_use_proxy_for_description)\n                    )\n                }\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = excludeURLPatterns,\n                        onTextChange = setExcludeURLPatterns,\n                        placeholder = \"example.com 192.168.1.*\",\n                        modifier = Modifier.Companion,\n                    )\n                }\n            }\n        )\n    }\n\n    @Composable\n    private fun SettingsDialog(\n        headerTitle: String,\n        onDismiss: () -> Unit,\n        content: @Composable () -> Unit,\n        actions: (@Composable RowScope.() -> Unit)? = null,\n    ) {\n        val shape = myShapes.defaultRounded\n        Column(\n            modifier = Modifier\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n                .padding(16.dp)\n                .width(450.dp),\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n            ) {\n                Text(\n                    headerTitle,\n                    fontSize = myTextSizes.lg,\n                    fontWeight = FontWeight.Bold,\n                )\n                MyIcon(\n                    MyIcons.windowClose,\n                    myStringResource(Res.string.close),\n                    Modifier\n                        .clip(CircleShape)\n                        .clickable { onDismiss() }\n                        .padding(12.dp)\n                        .size(12.dp),\n                )\n            }\n            Spacer(Modifier.height(8.dp))\n            Box(Modifier.weight(1f, false)) {\n                content()\n            }\n            actions?.let {\n                Spacer(Modifier.height(8.dp))\n                Row(\n                    Modifier.align(Alignment.End),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    actions()\n                }\n            }\n        }\n    }\n\n    @Composable\n    private fun ProxyConfigSpacer() {\n        Spacer(Modifier.height(8.dp))\n    }\n\n    @Composable\n    private fun DialogConfigItem(\n        modifier: Modifier,\n        title: @Composable ColumnScope.() -> Unit,\n        value: @Composable ColumnScope.() -> Unit,\n    ) {\n        Column(\n            modifier,\n        ) {\n            Column(\n                Modifier\n                    .height(IntrinsicSize.Max),\n            ) {\n                Column(\n                    verticalArrangement = Arrangement.Center,\n                    horizontalAlignment = Alignment.Start,\n                ) {\n                    title()\n                }\n                Spacer(Modifier.height(8.dp))\n                Column(\n                    verticalArrangement = Arrangement.Center,\n                    horizontalAlignment = Alignment.End,\n                ) {\n                    value()\n                }\n            }\n        }\n    }\n\n    private fun ProxyMode.asStringSource(): StringSource {\n        return when (this) {\n            ProxyMode.Direct -> Res.string.proxy_no\n            ProxyMode.UseSystem -> Res.string.proxy_system\n            ProxyMode.Manual -> Res.string.proxy_manual\n            ProxyMode.Pac -> Res.string.proxy_pac\n        }.asStringSource()\n    }\n\n    @Composable\n    private fun <T> Accordion(\n        possibleValues: List<T>,\n        selectedItem: T,\n        wrapItem: @Composable (T, @Composable () -> Unit) -> Unit = { _, content -> content() },\n        renderHeader: @Composable (T) -> Unit,\n        renderContent: @Composable (T) -> Unit,\n    ) {\n        Column {\n            possibleValues.forEach {\n                wrapItem(it) {\n                    ExpandableItem(\n                        isExpanded = selectedItem == it,\n                        header = {\n                            renderHeader(it)\n                        },\n                        body = {\n                            renderContent(it)\n                        },\n                    )\n                }\n            }\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/SpeedLimitConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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 com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.DoubleTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.LocalSpeedUnit\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.datasize.SizeConverter\nimport ir.amirab.util.datasize.SizeFactors\nimport ir.amirab.util.datasize.SizeUnit\nimport ir.amirab.util.datasize.SizeWithUnit\nimport ir.amirab.util.datasize.asConverterConfig\n\nobject SpeedLimitConfigurableRenderer : ConfigurableRenderer<SpeedLimitConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: SpeedLimitConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderSpeedConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val speedUnit = LocalSpeedUnit.current\n        val allowedFactors = listOf(\n            SizeFactors.FactorValue.Kilo,\n            SizeFactors.FactorValue.Mega,\n        )\n        val units = allowedFactors.map {\n            SizeUnit(\n                factorValue = it,\n                baseSize = speedUnit.baseSize,\n                factors = speedUnit.factors\n            )\n        }\n        val enabled = isConfigEnabled()\n        val hasLimitSpeed = value > 0L\n\n        var currentUnit by remember(hasLimitSpeed) {\n            mutableStateOf(\n                SizeConverter.bytesToSize(\n                    value,\n                    speedUnit.copy(acceptedFactors = allowedFactors)\n                ).unit\n            )\n        }\n        var currentValue by remember(value) {\n            val v = SizeConverter.bytesToSize(\n                value, currentUnit.asConverterConfig()\n            ).formatedValue().toDouble()\n            mutableStateOf(v)\n        }\n        LaunchedEffect(currentValue, currentUnit) {\n            setValue(\n                SizeConverter.sizeToBytes(\n                    SizeWithUnit(currentValue, currentUnit),\n                )\n            )\n        }\n        ConfigTemplate(\n            configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    TitleAndDescription(cfg, true)\n                }\n            },\n            nestedContent = {\n                Column(Modifier.align(Alignment.End)) {\n                    AnimatedVisibility(hasLimitSpeed) {\n                        Row(\n                            Modifier\n                                .padding(vertical = 8.dp)\n                                .width(250.dp)\n                        ) {\n                            DoubleTextField(\n                                value = currentValue,\n                                onValueChange = {\n                                    currentValue = it\n                                },\n                                enabled = enabled && hasLimitSpeed,\n                                range = 0.0..1_000.0,\n                                unit = 1.0,\n                                modifier = Modifier.weight(1f),\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            RenderSpinner(\n                                possibleValues = units,\n                                value = currentUnit,\n                                modifier = Modifier.Companion,\n                                enabled = enabled && hasLimitSpeed,\n                                onSelect = {\n                                    currentUnit = it\n                                }\n                            ) {\n                                val prettified = remember(it) {\n                                    \"$it/s\"\n                                }\n                                Text(prettified, Modifier.padding(horizontal = 4.dp))\n                            }\n                        }\n                    }\n                }\n            },\n            value = {\n                CheckBox(\n                    value = hasLimitSpeed,\n                    enabled = enabled,\n                    onValueChange = {\n                        if (it) {\n                            setValue(\n                                SizeConverter.sizeToBytes(\n                                    SizeWithUnit(\n                                        256.0, currentUnit\n                                    )\n                                )\n                            )\n                        } else {\n                            setValue(0)\n                        }\n                    })\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/StringConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\nobject StringConfigurableRenderer : ConfigurableRenderer<StringConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: StringConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderStringConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    fun RenderStringConfig(cfg: StringConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable { isOpened = true }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                NextIcon()\n            }\n        )\n        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n        SheetInput(\n            configurable = cfg,\n            isOpened = isOpened,\n            onDismiss = onDismiss,\n            inputContent = { params ->\n                MyTextField(\n                    modifier = params.modifier.fillMaxWidth(),\n                    text = params.editingValue,\n                    onTextChange = {\n                        params.setEditingValue(it)\n                    },\n                    shape = myShapes.defaultRounded,\n                    textPadding = PaddingValues(8.dp),\n                    placeholder = \"\",\n                    interactionSource = interactionSource,\n                    keyboardActions = params.keyboardActions,\n                )\n            },\n            onConfirm = {\n                cfg.set(it)\n                onDismiss()\n            },\n        )\n\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/ThemeConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\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.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.NextIcon\nimport com.abdownloadmanager.android.ui.configurable.RenderSpinnerInSheet\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.ThemeConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.ifThen\n\nobject ThemeConfigurableRenderer : ConfigurableRenderer<ThemeConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: ThemeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderThemeConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderThemeConfig(cfg: ThemeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        var isOpened by remember { mutableStateOf(false) }\n        val onDismiss = {\n            isOpened = false\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    isOpened = true\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.ifThen(!enabled) {\n                        alpha(0.5f)\n                    }\n                ) {\n                    Spacer(\n                        Modifier\n                            .clip(CircleShape)\n                            .border(\n                                1.dp,\n                                Brush.verticalGradient(myColors.primaryGradientColors),\n                                CircleShape\n                            )\n                            .padding(1.dp)\n                            .background(\n                                value.color,\n                            )\n                            .size(16.dp)\n                    )\n                    Spacer(Modifier.width(16.dp))\n                    NextIcon()\n                }\n            }\n        )\n        RenderSpinnerInSheet(\n            title = cfg.title,\n            onDismiss = onDismiss,\n            isOpened = isOpened,\n            possibleValues = cfg.possibleValues,\n            value = value,\n            onSelect = {\n                setValue(it)\n            },\n            valueToString = cfg.valueToString,\n            render = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.ifThen(!enabled) {\n                        alpha(0.5f)\n                    }\n                ) {\n                    Spacer(\n                        Modifier\n                            .clip(CircleShape)\n                            .border(\n                                1.dp,\n                                Brush.verticalGradient(myColors.primaryGradientColors),\n                                CircleShape\n                            )\n                            .padding(1.dp)\n                            .background(\n                                it.color,\n                            )\n                            .size(16.dp)\n                    )\n                    Spacer(Modifier.width(16.dp))\n                    Text(cfg.describe(it).rememberString())\n                }\n            })\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/configurable/comon/renderer/TimeConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.android.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.android.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.android.ui.configurable.SheetInput\nimport com.abdownloadmanager.android.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.TimeConfigurable\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport kotlinx.datetime.LocalTime\n\nobject TimeConfigurableRenderer : ConfigurableRenderer<TimeConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: TimeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderTimeConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    fun RenderTimeConfig(cfg: TimeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n\n        var isOpened by remember { mutableStateOf(false) }\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier\n                .clickable {\n                    isOpened = true\n                }\n                .padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                MyIcon(MyIcons.next, null, Modifier.size(16.dp))\n                SheetInput(\n                    configurable = cfg,\n                    isOpened = isOpened,\n                    onDismiss = { isOpened = false },\n                    onConfirm = {\n                        setValue(it)\n                        isOpened = false\n                    },\n                ) { inputParams ->\n                    var hour by remember(value) {\n                        mutableStateOf(value.hour)\n                    }\n                    var minute by remember(value) {\n                        mutableStateOf(value.minute)\n                    }\n                    LaunchedEffect(hour, minute) {\n                        inputParams.setEditingValue(\n                            LocalTime(\n                                hour = hour, minute = minute,\n                            )\n                        )\n                    }\n                    Row(\n                        modifier = inputParams.modifier,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        val textFieldModifier = Modifier\n                            .weight(1f)\n                        IntTextField(\n                            value = hour,\n                            onValueChange = {\n                                hour = it\n                            },\n                            range = 0..23,\n                            modifier = textFieldModifier,\n                            enabled = enabled,\n                            keyboardOptions = KeyboardOptions(\n                                keyboardType = KeyboardType.Companion.Decimal,\n                                imeAction = ImeAction.Next\n                            ),\n                            keyboardActions = KeyboardActions.Default,\n                            placeholder = \"hour\",\n                            prettify = { it.toString().padStart(2, '0') },\n                        )\n                        Text(\":\", Modifier.padding(horizontal = 4.dp))\n                        IntTextField(\n                            value = minute,\n                            onValueChange = {\n                                minute = it\n                            },\n                            range = 0..59,\n                            modifier = textFieldModifier,\n                            enabled = enabled,\n                            keyboardOptions = KeyboardOptions(\n                                keyboardType = KeyboardType.Decimal,\n                                imeAction = ImeAction.Done\n                            ),\n                            keyboardActions = inputParams.keyboardActions,\n                            placeholder = \"minute\",\n                            prettify = { it.toString().padStart(2, '0') },\n                        )\n                    }\n                }\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/menu/Menu.kt",
    "content": "package com.abdownloadmanager.android.ui.menu\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\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.heightIn\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.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.LocalMenuDisabledItemBehavior\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MenuDisabledItemBehavior\nimport com.abdownloadmanager.shared.util.LocalShortCutManager\nimport com.abdownloadmanager.shared.util.PlatformKeyStroke\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.ProvideTextStyle\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.modifiers.autoMirror\nimport ir.amirab.util.ifThen\n\n/**\n * render a menu\n */\n@Composable\nfun Menu(\n    menu: MenuItem.SubMenu,\n    onRequestClose: () -> Unit,\n    onNewMenuSelected: (MenuItem.SubMenu) -> Unit,\n    modifier: Modifier,\n) {\n    var openedItem: MenuItem.SubMenu? by remember {\n        mutableStateOf(null)\n    }\n\n    WithContentColor(myColors.onMenuColor) {\n        Column(\n            modifier\n        ) {\n            val items by menu.items.collectAsState()\n            for (menuItem in items) {\n                val interactionSource = remember { MutableInteractionSource() }\n                RenderMenuItem(\n                    menuItem = menuItem,\n//                    openedItem = openedItem,\n                    onRequestCLose = onRequestClose,\n                    isSelected = openedItem == menuItem,\n                    onRequestOpenItem = {\n                        onNewMenuSelected(it)\n                    },\n                    modifier = Modifier.hoverable(interactionSource)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ReactableItem(\n    item: MenuItem.ReadableItem,\n    onClick: () -> Unit,\n    isSelected: Boolean,\n    modifier: Modifier = Modifier,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    extraContent: @Composable () -> Unit = {},\n) {\n    val iconModifier = Modifier.size(menuIconSize)\n    val title by item.title.collectAsState()\n    val icon by item.icon.collectAsState()\n    val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    val isEnabled = (item as? MenuItem.HasEnable)\n        ?.isEnabled\n        ?.collectAsState()\n        ?.value ?: true\n    Row(\n        modifier\n            .ifThen(!isEnabled) { alpha(0.5f) }\n            .heightIn(mySpacings.thumbSize)\n            .hoverable(interactionSource)\n            .background(\n                when {\n                    (isHovered && isEnabled) || isSelected -> {\n                        myColors.surface\n                    }\n\n                    else -> {\n                        Color.Transparent\n                    }\n                }\n            )\n            .clickable(enabled = isEnabled) {\n                onClick()\n            }\n            .then(itemPadding)\n            .fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        icon.let { icon ->\n            if (icon != null) {\n                Spacer(Modifier.width(4.dp))\n                MyIcon(icon, null, iconModifier)\n                Spacer(Modifier.width(16.dp))\n            } else {\n                Spacer(iconModifier)\n            }\n        }\n        Text(\n            title.rememberString(),\n            Modifier.weight(1f),\n            fontSize = myTextSizes.base,\n            softWrap = false,\n            maxLines = 1,\n        )\n        Spacer(Modifier.width(16.dp))\n        extraContent()\n    }\n}\n\n@Composable\nprivate fun RenderMenuItem(\n    menuItem: MenuItem,\n    onRequestCLose: () -> Unit,\n    isSelected: Boolean,\n    modifier: Modifier = Modifier,\n    onRequestOpenItem: (MenuItem.SubMenu) -> Unit,\n) {\n//    val isEnabled by menuItem.isEnabled.collectAsState()\n    Row(\n        modifier\n            .fillMaxWidth()\n    ) {\n        when (menuItem) {\n            MenuItem.Separator -> {\n                RenderSeparator()\n            }\n\n            is MenuItem.SingleItem -> {\n                RenderSingleItem(\n                    item = menuItem,\n                    isSelected = isSelected,\n                    onRequestClose = onRequestCLose,\n                )\n            }\n\n            is MenuItem.SubMenu -> {\n                RenderSubMenuItem(\n                    menuItem = menuItem,\n                    isSelected = isSelected,\n//                    onRequestCLose = onRequestCLose,\n//                    openedItem = openedItem,\n                    onRequestOpenItem = onRequestOpenItem,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun RenderSeparator() {\n    Spacer(\n        Modifier\n            .fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onSurface / 5)\n    )\n}\n\n@Composable\nprivate fun RenderSubMenuItem(\n    menuItem: MenuItem.SubMenu,\n    isSelected: Boolean,\n//    openedItem: MenuItem.SubMenu?,\n    onRequestOpenItem: (MenuItem.SubMenu) -> Unit,\n//    onRequestCLose: () -> Unit,\n) {\n    ReactableItem(\n        item = menuItem,\n        onClick = {\n            onRequestOpenItem(menuItem)\n        },\n        isSelected = isSelected,\n        extraContent = {\n            MyIcon(\n                MyIcons.next,\n                null,\n                Modifier\n                    .size(16.dp)\n                    .autoMirror(),\n            )\n        })\n//    if (openedItem == menuItem) {\n//        SiblingDropDown(\n//            onDismissRequest = {\n//                onRequestOpenItem(null)\n//            }\n//        ) {\n//            SubMenu(menuItem, onRequestCLose)\n//        }\n//    }\n}\n\n@Composable\nprivate fun RenderSingleItem(\n    onRequestClose: () -> Unit,\n    isSelected: Boolean,\n    item: MenuItem.SingleItem,\n) {\n    val isEnabled by item.isEnabled.collectAsState()\n    if (!isEnabled && LocalMenuDisabledItemBehavior.current == MenuDisabledItemBehavior.Filter) {\n        return\n    }\n\n    val shortcutManager = LocalShortCutManager.current\n    val shortcutStroke = remember(shortcutManager, item) {\n        shortcutManager?.getShortCutOf(item)\n    }\n    val onClick = {\n        if (item.shouldDismissOnClick) {\n            onRequestClose()\n        }\n        item.onClick()\n    }\n    ReactableItem(\n        item = item,\n        onClick = onClick,\n        isSelected = isSelected,\n        extraContent = {\n            if (shortcutStroke != null) {\n                RenderShortcutStroke(shortcutStroke)\n            }\n        }\n    )\n\n}\n\n@Composable\nprivate fun RenderShortcutStroke(shortcutStroke: PlatformKeyStroke) {\n    val modifiers = remember(shortcutStroke) {\n        buildList {\n            addAll(shortcutStroke.getModifiers())\n            add(shortcutStroke.getKeyText())\n        }\n    }\n    ProvideTextStyle(\n        TextStyle(\n            fontSize = myTextSizes.xs,\n        )\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(1.dp)\n        ) {\n            val shape = RoundedCornerShape(10)\n            WithContentColor(myColors.onBackground) {\n                Text(\n                    modifiers.joinToString(\"+\"),\n                    Modifier\n                        .clip(shape)\n                        .background(myColors.onBackground / 5)\n                        .padding(2.dp)\n                )\n            }\n        }\n    }\n}\n\nval menuIconSize = 20.dp\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/menu/RenderMenuInSheet.kt",
    "content": "package com.abdownloadmanager.android.ui.menu\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport com.abdownloadmanager.android.ui.SheetHeader\nimport com.abdownloadmanager.android.ui.SheetTitle\nimport com.abdownloadmanager.android.ui.SheetUI\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.TransparentIconActionButton\nimport com.abdownloadmanager.shared.util.OnFullyDismissed\nimport com.abdownloadmanager.shared.util.ResponsiveDialog\nimport com.abdownloadmanager.shared.util.ResponsiveDialogScope\nimport com.abdownloadmanager.shared.util.rememberResponsiveDialogState\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nprivate fun ResponsiveDialogScope.RenderMenuInSheetUi(\n    menuStack: StackMenuState,\n    onDismissRequest: () -> Unit,\n) {\n    val currentMenu = menuStack.currentMenu\n    SheetUI(\n        header = {\n            SheetHeader(\n                headerTitle = {\n                    SheetTitle(\n                        title = currentMenu.title.collectAsState().value.rememberString(),\n                        icon = currentMenu.icon.collectAsState().value,\n                    )\n                },\n                headerActions = {\n                    if (menuStack.canGoBack) {\n                        TransparentIconActionButton(\n                            icon = MyIcons.back,\n                            contentDescription = Res.string.back.asStringSource(),\n                        ) {\n                            menuStack.pop()\n                        }\n                    }\n                    TransparentIconActionButton(\n                        MyIcons.close,\n                        Res.string.close.asStringSource()\n                    ) {\n                        onDismissRequest()\n                    }\n                }\n            )\n        }\n    ) {\n        BaseStackedMenu(\n            menuStack = menuStack,\n            onDismissRequest = onDismissRequest,\n            modifier = Modifier.fillMaxWidth(),\n        )\n    }\n}\n\n@Composable\nfun RenderMenuInSheet(\n    menu: MenuItem.SubMenu?,\n    onDismissRequest: () -> Unit,\n) {\n    val responsiveDialogState = rememberResponsiveDialogState(false)\n    LaunchedEffect(menu) {\n        if (menu != null) {\n            responsiveDialogState.show()\n        } else {\n            responsiveDialogState.hide()\n        }\n    }\n    responsiveDialogState.OnFullyDismissed {\n        onDismissRequest()\n    }\n    val hideDialog = responsiveDialogState::hide\n    menu?.let {\n        ResponsiveDialog(\n            responsiveDialogState,\n            hideDialog,\n        ) {\n            val menuStackState = rememberMenuStack(it)\n            RenderMenuInSheetUi(menuStackState, hideDialog)\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/menu/RenderMenuInSinglePage.kt",
    "content": "package com.abdownloadmanager.android.ui.menu\n\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\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.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.mutableStateListOf\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.shadow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.LocalMenuBoxClip\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.localizationmanager.WithLanguageDirection\nimport ir.amirab.util.compose.modifiers.autoMirror\n\n@Composable\nprivate fun RenderMenuInSinglePage(\n    menuStack: StackMenuState,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier,\n) {\n    val shape = LocalMenuBoxClip.current\n    val alpha = remember { Animatable(0f) }\n    LaunchedEffect(Unit) {\n        alpha.animateTo(1f)\n    }\n    WithLanguageDirection {\n        Column(\n            modifier\n                .shadow(4.dp, shape)\n                .clip(shape)\n                .widthIn(200.dp)\n                .border(1.dp, myColors.onSurface / 0.1f, shape)\n                .background(myColors.surface)\n                .padding(horizontal = 0.dp, vertical = 0.dp)\n        ) {\n            BaseStackedMenu(\n                menuStack,\n                onDismissRequest,\n            ) { currentMenu, render ->\n                val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher\n                render()\n                val currentTitle = currentMenu.title.collectAsState().value.rememberString()\n                if (currentTitle.isNotEmpty()) {\n                    RenderSeparator()\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier\n                            .clickable(\n                                enabled = menuStack.size > 1\n                            ) {\n                                onBackPressedDispatcher?.onBackPressed()\n                            }\n                            .fillMaxWidth()\n                            .heightIn(mySpacings.thumbSize)\n                            .padding(horizontal = 16.dp)\n                    ) {\n                        val iconModifier = Modifier\n                            .size(menuIconSize)\n                        if (menuStack.size > 1) {\n                            MyIcon(\n                                MyIcons.back,\n                                null,\n                                iconModifier.autoMirror(),\n                            )\n                            Spacer(Modifier.width(16.dp))\n                        }\n                        Text(\n                            currentTitle,\n                            Modifier.weight(1f),\n                            color = LocalContentColor.current / 0.75f,\n                        )\n                    }\n                }\n            }\n\n        }\n    }\n}\n\n@Composable\nfun RenderMenuInSinglePage(\n    menu: List<MenuItem>,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier,\n) {\n    RenderMenuInSinglePage(\n        menuStack = rememberMenuStack(menu),\n        modifier = modifier,\n        onDismissRequest = onDismissRequest,\n    )\n}\n\n@Composable\nfun RenderMenuInSinglePage(\n    menu: MenuItem.SubMenu,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier,\n) {\n    RenderMenuInSinglePage(\n        menuStack = rememberMenuStack(menu),\n        modifier = modifier,\n        onDismissRequest = onDismissRequest,\n    )\n}\n\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/menu/StackedMenu.kt",
    "content": "package com.abdownloadmanager.android.ui.menu\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.ui.Modifier\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun rememberMenuStack(\n    menu: MenuItem.SubMenu,\n): StackMenuState {\n    return remember(menu) {\n        StackMenuState(mutableStateListOf(menu))\n    }\n}\n\n@Composable\nfun rememberMenuStack(\n    menu: List<MenuItem>,\n): StackMenuState {\n    return remember(menu) {\n        StackMenuState(\n            mutableStateListOf(\n                MenuItem.SubMenu(\n                    title = \"\".asStringSource(),\n                    items = menu,\n                )\n            )\n        )\n    }\n}\n\n@Stable\nclass StackMenuState(val menu: SnapshotStateList<MenuItem.SubMenu>) {\n    val menuStack = menu\n    val currentMenu by derivedStateOf {\n        menuStack.last()\n    }\n    val size by derivedStateOf {\n        menu.size\n    }\n\n    fun push(newMenu: MenuItem.SubMenu) {\n        menu.add(newMenu)\n    }\n    val canGoBack by derivedStateOf {\n        menuStack.size > 1\n    }\n    fun pop(): Boolean {\n        if (menuStack.size == 1) {\n            return false\n        } else {\n            menuStack.removeAt(menuStack.lastIndex)\n            return true\n        }\n    }\n}\n\n@Composable\nfun BaseStackedMenu(\n    menuStack: StackMenuState,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier = Modifier,\n    menuWrapper: (@Composable (\n        subMenu: MenuItem.SubMenu,\n        renderMenu: @Composable () -> Unit\n    ) -> Unit) = @Composable { _, render -> render() }\n) {\n    BackHandler {\n        val menuStack = menuStack\n        if (!menuStack.pop()) {\n            onDismissRequest()\n        }\n    }\n    val currentMenu = menuStack.currentMenu\n    AnimatedContent(\n        currentMenu\n    ) { currentMenu ->\n        Column {\n            menuWrapper(currentMenu) {\n                Menu(\n                    menu = currentMenu,\n                    onNewMenuSelected = { newMenu ->\n                        menuStack.push(newMenu)\n                    },\n                    onRequestClose = {\n                        onDismissRequest()\n                    },\n                    modifier = modifier\n                        .verticalScroll(rememberScrollState()),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/myCombinedClickable.kt",
    "content": "package com.abdownloadmanager.android.ui\n\nimport androidx.compose.foundation.Indication\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.indication\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.pointerInput\n\nfun Modifier.myCombinedClickable(\n    onClick: ((offset: Offset) -> Unit)? = null,\n    onLongClick: ((offset: Offset) -> Unit)? = null,\n    onDoubleClick: ((offset: Offset) -> Unit)? = null,\n    interactionSource: MutableInteractionSource?,\n    indication: Indication?,\n): Modifier {\n    return pointerInput(\n        interactionSource,\n        onClick,\n        onLongClick,\n        onDoubleClick,\n    ) {\n        detectTapGestures(\n            onPress = { offset ->\n                interactionSource?.let { mutableInteractionSource ->\n                    val press = PressInteraction.Press(offset)\n                    mutableInteractionSource.emit(press)\n                    awaitRelease()\n                    mutableInteractionSource.emit(PressInteraction.Release(press))\n                }\n            },\n            onTap = onClick,\n            onLongPress = onLongClick,\n            onDoubleTap = onDoubleClick\n        )\n    }.let {\n        if (interactionSource != null && indication != null) {\n            it.indication(interactionSource, indication)\n        } else it\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/page/PageUI.kt",
    "content": "package com.abdownloadmanager.android.ui.page\n\nimport androidx.compose.foundation.basicMarquee\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.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.consumeWindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.text.font.FontWeight\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.pxToDp\n\n@Immutable\ndata class PageContentParams(\n    val paddingValues: PaddingValues,\n)\n\n@Composable\nfun PageUi(\n    header: @Composable () -> Unit,\n    footer: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    content: @Composable (params: PageContentParams) -> Unit,\n) {\n    var headerHeight by remember {\n        mutableIntStateOf(0)\n    }\n    var footerHeight by remember {\n        mutableIntStateOf(0)\n    }\n    val density = LocalDensity.current\n    val direction = LocalLayoutDirection.current\n    val horizontalInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)\n    val contentPadding = PaddingValues(\n        top = density.run { headerHeight.toDp() },\n        bottom = density.run { footerHeight.toDp() },\n        start = horizontalInsets.getLeft(density, direction).pxToDp(density),\n        end = horizontalInsets.getRight(density, direction).pxToDp(density),\n    )\n    Box(\n        modifier\n            .consumeWindowInsets(horizontalInsets)\n    ) {\n        content(\n            PageContentParams(contentPadding)\n        )\n        Box(\n            Modifier\n                .onSizeChanged {\n                    headerHeight = it.height\n                }\n                .align(Alignment.TopCenter)\n        ) {\n            header()\n        }\n        Box(\n            Modifier\n                .onSizeChanged {\n                    footerHeight = it.height\n                }\n                .align(Alignment.BottomCenter)\n        ) {\n            footer()\n        }\n    }\n}\n\n\n@Composable\nfun PageHeader(\n    modifier: Modifier = Modifier,\n    headerTitle: @Composable () -> Unit = {},\n    leadingIcon: (@Composable () -> Unit)? = null,\n    headerActions: @Composable RowScope.() -> Unit = {},\n) {\n\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n//            .padding(vertical = mySpacings.mediumSpace)\n            .padding(horizontal = mySpacings.mediumSpace)\n            .systemHorizontalPaddings(),\n    ) {\n        leadingIcon?.let {\n            it()\n            Spacer(Modifier.width(mySpacings.smallSpace))\n        }\n        Box(\n            Modifier.weight(1f),\n            contentAlignment = Alignment.CenterStart,\n        ) {\n            headerTitle()\n        }\n        Spacer(Modifier.width(mySpacings.smallSpace))\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            headerActions()\n        }\n    }\n}\n\n@Composable\nfun PageTitle(\n    title: String,\n) {\n    Text(\n        text = title,\n        fontWeight = FontWeight.Bold,\n        fontSize = myTextSizes.xl,\n        maxLines = 1,\n        modifier = Modifier\n            .padding(start = mySpacings.largeSpace)\n            .padding(vertical = mySpacings.largeSpace)\n            .basicMarquee()\n    )\n}\n\n@Composable\nfun PageTitleWithDescription(\n    title: String,\n    description: String,\n) {\n    Column(\n        Modifier\n            .padding(start = mySpacings.largeSpace)\n            .padding(vertical = mySpacings.largeSpace)\n    ) {\n        Text(\n            text = title,\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.xl,\n            modifier = Modifier\n        )\n        Spacer(Modifier.width(mySpacings.mediumSpace))\n        Text(\n            text = description,\n            modifier = Modifier,\n            color = LocalContentColor.current / 0.75f\n        )\n    }\n}\n\n@Composable\nfun PageFooter(\n    content: @Composable () -> Unit,\n) {\n    Box(\n        Modifier.systemHorizontalPaddings()\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun Modifier.systemHorizontalPaddings(): Modifier {\n    return composed {\n        val horizontalInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)\n        val direction = LocalLayoutDirection.current\n        val density = LocalDensity.current\n        val horizontalPaddings = PaddingValues(\n            start = horizontalInsets.getLeft(density, direction).pxToDp(density),\n            end = horizontalInsets.getRight(density, direction).pxToDp(density),\n        )\n        this\n            .consumeWindowInsets(horizontalInsets)\n            .padding(horizontalPaddings)\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/page/PageUtils.kt",
    "content": "package com.abdownloadmanager.android.ui.page\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport com.abdownloadmanager.shared.util.ui.myColors\n\n@Composable\nfun BoxScope.HeaderFade(\n    topHeight: Dp,\n    color: Color = myColors.background,\n) {\n    Box(\n        Modifier\n            .align(Alignment.TopCenter)\n            .fillMaxWidth()\n            .height(topHeight)\n            .background(\n                Brush.verticalGradient(\n                    listOf(\n                        color,\n                        Color.Transparent,\n                    )\n                )\n            )\n    )\n}\n\n@Composable\nfun BoxScope.FooterFade(\n    bottomHeight: Dp,\n    color: Color = myColors.background,\n) {\n    Box(\n        Modifier\n            .align(Alignment.BottomCenter)\n            .fillMaxWidth()\n            .height(bottomHeight)\n            .background(\n                Brush.verticalGradient(\n                    listOf(\n                        Color.Transparent,\n                        color,\n                    )\n                )\n            )\n    )\n}\n\n\n@Composable\nfun rememberHeaderAlpha(\n    listState: LazyListState,\n    headerHeightPx: Float,\n): State<Float> {\n    val headerHeightPx by rememberUpdatedState(headerHeightPx)\n    return remember {\n        derivedStateOf {\n            when {\n                listState.firstVisibleItemIndex > 0 -> 1f\n                headerHeightPx == 0f -> 1f\n                else -> {\n                    val scrolled = listState.firstVisibleItemScrollOffset.toFloat()\n                    (scrolled / headerHeightPx).coerceIn(0f, 1f)\n                }\n            }\n        }\n    }\n}\n\nfun createAlphaForHeader(\n    scrollOffset: Float,\n    headerHeight: Float,\n): Float {\n    if (headerHeight == 0f) return 0f\n    return (scrollOffset / headerHeight).coerceIn(0f..1f)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/ui/widget/ComposeWebView.kt",
    "content": "/*\n * Copyright 2021 The Android Open Source Project\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 */\npackage com.abdownloadmanager.android.ui.widget\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.view.ViewGroup.LayoutParams\nimport android.webkit.WebChromeClient\nimport android.webkit.WebResourceError\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport android.widget.FrameLayout\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.mapSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.viewinterop.AndroidView\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n/**\n * A wrapper around the Android View WebView to provide a basic WebView composable.\n *\n * If you require more customisation you are most likely better rolling your own and using this\n * wrapper as an example.\n *\n * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it\n * is incorrectly sizing, use the layoutParams composable function instead.\n *\n * @param state The webview state holder where the Uri to load is defined.\n * @param modifier A compose modifier\n * @param captureBackPresses Set to true to have this Composable capture back presses and navigate\n * the WebView back.\n * @param navigator An optional navigator object that can be used to control the WebView's\n * navigation from outside the composable.\n * @param onCreated Called when the WebView is first created, this can be used to set additional\n * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be\n * subsequently overwritten after this lambda is called.\n * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved\n * if you need to save and restore state in this WebView.\n * @param client Provides access to WebViewClient via subclassing\n * @param chromeClient Provides access to WebChromeClient via subclassing\n * @param factory An optional WebView factory for using a custom subclass of WebView\n * @sample com.google.accompanist.sample.webview.BasicWebViewSample\n */\n@Composable\npublic fun WebView(\n    state: WebViewState,\n    modifier: Modifier = Modifier,\n    captureBackPresses: Boolean = true,\n    navigator: WebViewNavigator = rememberWebViewNavigator(),\n    onCreated: (WebView) -> Unit = {},\n    onDispose: (WebView) -> Unit = {},\n    client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },\n    chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },\n    factory: ((Context) -> WebView)? = null,\n) {\n    BoxWithConstraints(modifier) {\n        // WebView changes it's layout strategy based on\n        // it's layoutParams. We convert from Compose Modifier to\n        // layout params here.\n        val width =\n            if (constraints.hasFixedWidth)\n                LayoutParams.MATCH_PARENT\n            else\n                LayoutParams.WRAP_CONTENT\n        val height =\n            if (constraints.hasFixedHeight)\n                LayoutParams.MATCH_PARENT\n            else\n                LayoutParams.WRAP_CONTENT\n\n        val layoutParams = FrameLayout.LayoutParams(\n            width,\n            height\n        )\n\n        WebView(\n            state,\n            layoutParams,\n            Modifier,\n            captureBackPresses,\n            navigator,\n            onCreated,\n            onDispose,\n            client,\n            chromeClient,\n            factory\n        )\n    }\n}\n\n/**\n * A wrapper around the Android View WebView to provide a basic WebView composable.\n *\n * If you require more customisation you are most likely better rolling your own and using this\n * wrapper as an example.\n *\n * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it\n * is incorrectly sizing, use the layoutParams composable function instead.\n *\n * @param state The webview state holder where the Uri to load is defined.\n * @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView.\n * @param modifier A compose modifier\n * @param captureBackPresses Set to true to have this Composable capture back presses and navigate\n * the WebView back.\n * @param navigator An optional navigator object that can be used to control the WebView's\n * navigation from outside the composable.\n * @param onCreated Called when the WebView is first created, this can be used to set additional\n * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be\n * subsequently overwritten after this lambda is called.\n * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved\n * if you need to save and restore state in this WebView.\n * @param client Provides access to WebViewClient via subclassing\n * @param chromeClient Provides access to WebChromeClient via subclassing\n * @param factory An optional WebView factory for using a custom subclass of WebView\n */\n@Composable\npublic fun WebView(\n    state: WebViewState,\n    layoutParams: FrameLayout.LayoutParams,\n    modifier: Modifier = Modifier,\n    captureBackPresses: Boolean = true,\n    navigator: WebViewNavigator = rememberWebViewNavigator(),\n    onCreated: (WebView) -> Unit = {},\n    onDispose: (WebView) -> Unit = {},\n    client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },\n    chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },\n    factory: ((Context) -> WebView)? = null,\n) {\n    val webView = state.webView\n\n    BackHandler(captureBackPresses && navigator.canGoBack) {\n        webView?.goBack()\n    }\n\n    webView?.let { wv ->\n        LaunchedEffect(wv, navigator) {\n            with(navigator) {\n                wv.handleNavigationEvents()\n            }\n        }\n\n        LaunchedEffect(wv, state) {\n            snapshotFlow { state.content }.collect { content ->\n                when (content) {\n                    is WebContent.Url -> {\n                        wv.loadUrl(content.url, content.additionalHttpHeaders)\n                    }\n\n                    is WebContent.Data -> {\n                        wv.loadDataWithBaseURL(\n                            content.baseUrl,\n                            content.data,\n                            content.mimeType,\n                            content.encoding,\n                            content.historyUrl\n                        )\n                    }\n\n                    is WebContent.Post -> {\n                        wv.postUrl(\n                            content.url,\n                            content.postData\n                        )\n                    }\n\n                    is WebContent.NavigatorOnly -> {\n                        // NO-OP\n                    }\n                }\n            }\n        }\n    }\n\n    // Set the state of the client and chrome client\n    // This is done internally to ensure they always are the same instance as the\n    // parent Web composable\n    client.state = state\n    client.navigator = navigator\n    chromeClient.state = state\n\n    AndroidView(\n        factory = { context ->\n            (factory?.invoke(context) ?: WebView(context)).apply {\n                onCreated(this)\n\n                this.layoutParams = layoutParams\n\n                state.viewState?.let {\n                    this.restoreState(it)\n                }\n\n                webChromeClient = chromeClient\n                webViewClient = client\n            }.also { state.webView = it }\n        },\n        modifier = modifier.clipToBounds(),\n        onRelease = {\n            onDispose(it)\n        }\n    )\n}\n\n/**\n * AccompanistWebViewClient\n *\n * A parent class implementation of WebViewClient that can be subclassed to add custom behaviour.\n *\n * As Accompanist Web needs to set its own web client to function, it provides this intermediary\n * class that can be overriden if further custom behaviour is required.\n */\npublic open class AccompanistWebViewClient : WebViewClient() {\n    public open lateinit var state: WebViewState\n        internal set\n    public open lateinit var navigator: WebViewNavigator\n        internal set\n\n    override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {\n        super.onPageStarted(view, url, favicon)\n        state.loadingState = LoadingState.Loading(0.0f)\n        state.errorsForCurrentRequest.clear()\n        state.pageTitle = null\n        state.pageIcon = null\n\n        state.lastLoadedUrl = url\n    }\n\n    override fun onPageFinished(view: WebView, url: String?) {\n        super.onPageFinished(view, url)\n        state.loadingState = LoadingState.Finished\n    }\n\n    override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {\n        super.doUpdateVisitedHistory(view, url, isReload)\n\n        navigator.canGoBack = view.canGoBack()\n        navigator.canGoForward = view.canGoForward()\n    }\n\n    override fun onReceivedError(\n        view: WebView,\n        request: WebResourceRequest?,\n        error: WebResourceError?\n    ) {\n        super.onReceivedError(view, request, error)\n\n        if (error != null) {\n            state.errorsForCurrentRequest.add(WebViewError(request, error))\n        }\n    }\n}\n\n/**\n * AccompanistWebChromeClient\n *\n * A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour.\n *\n * As Accompanist Web needs to set its own web client to function, it provides this intermediary\n * class that can be overriden if further custom behaviour is required.\n */\npublic open class AccompanistWebChromeClient : WebChromeClient() {\n    public open lateinit var state: WebViewState\n        internal set\n\n    override fun onReceivedTitle(view: WebView, title: String?) {\n        super.onReceivedTitle(view, title)\n        state.pageTitle = title\n    }\n\n    override fun onReceivedIcon(view: WebView, icon: Bitmap?) {\n        super.onReceivedIcon(view, icon)\n        state.pageIcon = icon\n    }\n\n    override fun onProgressChanged(view: WebView, newProgress: Int) {\n        super.onProgressChanged(view, newProgress)\n        if (state.loadingState is LoadingState.Finished) return\n        state.loadingState = LoadingState.Loading(newProgress / 100.0f)\n    }\n}\n\npublic sealed class WebContent {\n    public data class Url(\n        val url: String,\n        val additionalHttpHeaders: Map<String, String> = emptyMap(),\n    ) : WebContent()\n\n    public data class Data(\n        val data: String,\n        val baseUrl: String? = null,\n        val encoding: String = \"utf-8\",\n        val mimeType: String? = null,\n        val historyUrl: String? = null\n    ) : WebContent()\n\n    public data class Post(\n        val url: String,\n        val postData: ByteArray\n    ) : WebContent() {\n        override fun equals(other: Any?): Boolean {\n            if (this === other) return true\n            if (javaClass != other?.javaClass) return false\n\n            other as Post\n\n            if (url != other.url) return false\n            if (!postData.contentEquals(other.postData)) return false\n\n            return true\n        }\n\n        override fun hashCode(): Int {\n            var result = url.hashCode()\n            result = 31 * result + postData.contentHashCode()\n            return result\n        }\n    }\n\n    @Deprecated(\"Use state.lastLoadedUrl instead\")\n    public fun getCurrentUrl(): String? {\n        return when (this) {\n            is Url -> url\n            is Data -> baseUrl\n            is Post -> url\n            is NavigatorOnly -> throw IllegalStateException(\"Unsupported\")\n        }\n    }\n\n    public object NavigatorOnly : WebContent()\n\n    companion object {\n        fun fromNullableUrl(url: String?): WebContent {\n            return when (url) {\n                null -> NavigatorOnly\n                else -> Url(url)\n            }\n        }\n    }\n\n}\n\ninternal fun WebContent.withUrl(url: String) = when (this) {\n    is WebContent.Url -> copy(url = url)\n    else -> WebContent.Url(url)\n}\n\n/**\n * Sealed class for constraining possible loading states.\n * See [Loading] and [Finished].\n */\npublic sealed class LoadingState {\n    /**\n     * Describes a WebView that has not yet loaded for the first time.\n     */\n    public object Initializing : LoadingState()\n\n    /**\n     * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a\n     * [progress] property which is updated by the webview.\n     */\n    public data class Loading(val progress: Float) : LoadingState()\n\n    /**\n     * Describes a webview that has finished loading content.\n     */\n    public object Finished : LoadingState()\n}\n\n/**\n * A state holder to hold the state for the WebView. In most cases this will be remembered\n * using the rememberWebViewState(uri) function.\n */\n@Stable\npublic class WebViewState(webContent: WebContent) {\n    public var lastLoadedUrl: String? by mutableStateOf(null)\n        internal set\n\n    /**\n     *  The content being loaded by the WebView\n     */\n    public var content: WebContent by mutableStateOf(webContent)\n\n    /**\n     * Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with\n     * progress) or the data loading has [LoadingState.Finished]. See [LoadingState]\n     */\n    public var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)\n        internal set\n\n    /**\n     * Whether the webview is currently loading data in its main frame\n     */\n    public val isLoading: Boolean\n        get() = loadingState !is LoadingState.Finished\n\n    /**\n     * The title received from the loaded content of the current page\n     */\n    public var pageTitle: String? by mutableStateOf(null)\n        internal set\n\n    /**\n     * the favicon received from the loaded content of the current page\n     */\n    public var pageIcon: Bitmap? by mutableStateOf(null)\n        internal set\n\n    /**\n     * A list for errors captured in the last load. Reset when a new page is loaded.\n     * Errors could be from any resource (iframe, image, etc.), not just for the main page.\n     * For more fine grained control use the OnError callback of the WebView.\n     */\n    public val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()\n\n    /**\n     * The saved view state from when the view was destroyed last. To restore state,\n     * use the navigator and only call loadUrl if the bundle is null.\n     * See WebViewSaveStateSample.\n     */\n    public var viewState: Bundle? = null\n        internal set\n\n    // We need access to this in the state saver. An internal DisposableEffect or AndroidView\n    // onDestroy is called after the state saver and so can't be used.\n    internal var webView by mutableStateOf<WebView?>(null)\n}\n\n/**\n * Allows control over the navigation of a WebView from outside the composable. E.g. for performing\n * a back navigation in response to the user clicking the \"up\" button in a TopAppBar.\n *\n * @see [rememberWebViewNavigator]\n */\n@Stable\npublic class WebViewNavigator(private val coroutineScope: CoroutineScope) {\n    private sealed interface NavigationEvent {\n        object Back : NavigationEvent\n        object Forward : NavigationEvent\n        object Reload : NavigationEvent\n        object StopLoading : NavigationEvent\n\n        data class LoadUrl(\n            val url: String,\n            val additionalHttpHeaders: Map<String, String> = emptyMap()\n        ) : NavigationEvent\n\n        data class LoadHtml(\n            val html: String,\n            val baseUrl: String? = null,\n            val mimeType: String? = null,\n            val encoding: String? = \"utf-8\",\n            val historyUrl: String? = null\n        ) : NavigationEvent\n\n        data class PostUrl(\n            val url: String,\n            val postData: ByteArray\n        ) : NavigationEvent {\n            override fun equals(other: Any?): Boolean {\n                if (this === other) return true\n                if (javaClass != other?.javaClass) return false\n\n                other as PostUrl\n\n                if (url != other.url) return false\n                if (!postData.contentEquals(other.postData)) return false\n\n                return true\n            }\n\n            override fun hashCode(): Int {\n                var result = url.hashCode()\n                result = 31 * result + postData.contentHashCode()\n                return result\n            }\n        }\n    }\n\n    private val navigationEvents: MutableSharedFlow<NavigationEvent> = MutableSharedFlow(replay = 1)\n\n    // Use Dispatchers.Main to ensure that the webview methods are called on UI thread\n    internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) {\n        navigationEvents.collect { event ->\n            when (event) {\n                is NavigationEvent.Back -> goBack()\n                is NavigationEvent.Forward -> goForward()\n                is NavigationEvent.Reload -> reload()\n                is NavigationEvent.StopLoading -> stopLoading()\n                is NavigationEvent.LoadHtml -> loadDataWithBaseURL(\n                    event.baseUrl,\n                    event.html,\n                    event.mimeType,\n                    event.encoding,\n                    event.historyUrl\n                )\n\n                is NavigationEvent.LoadUrl -> {\n                    loadUrl(event.url, event.additionalHttpHeaders)\n                }\n\n                is NavigationEvent.PostUrl -> {\n                    postUrl(event.url, event.postData)\n                }\n            }\n        }\n    }\n\n    /**\n     * True when the web view is able to navigate backwards, false otherwise.\n     */\n    public var canGoBack: Boolean by mutableStateOf(false)\n        internal set\n\n    /**\n     * True when the web view is able to navigate forwards, false otherwise.\n     */\n    public var canGoForward: Boolean by mutableStateOf(false)\n        internal set\n\n    public fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {\n        coroutineScope.launch {\n            navigationEvents.emit(\n                NavigationEvent.LoadUrl(\n                    url,\n                    additionalHttpHeaders\n                )\n            )\n        }\n    }\n\n    public fun loadHtml(\n        html: String,\n        baseUrl: String? = null,\n        mimeType: String? = null,\n        encoding: String? = \"utf-8\",\n        historyUrl: String? = null\n    ) {\n        coroutineScope.launch {\n            navigationEvents.emit(\n                NavigationEvent.LoadHtml(\n                    html,\n                    baseUrl,\n                    mimeType,\n                    encoding,\n                    historyUrl\n                )\n            )\n        }\n    }\n\n    public fun postUrl(\n        url: String,\n        postData: ByteArray\n    ) {\n        coroutineScope.launch {\n            navigationEvents.emit(\n                NavigationEvent.PostUrl(\n                    url,\n                    postData\n                )\n            )\n        }\n    }\n\n    /**\n     * Navigates the webview back to the previous page.\n     */\n    public fun navigateBack() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }\n    }\n\n    /**\n     * Navigates the webview forward after going back from a page.\n     */\n    public fun navigateForward() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }\n    }\n\n    /**\n     * Reloads the current page in the webview.\n     */\n    public fun reload() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }\n    }\n\n    /**\n     * Stops the current page load (if one is loading).\n     */\n    public fun stopLoading() {\n        coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }\n    }\n}\n\n/**\n * Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided\n * override.\n */\n@Composable\npublic fun rememberWebViewNavigator(\n    coroutineScope: CoroutineScope = rememberCoroutineScope()\n): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }\n\n/**\n * A wrapper class to hold errors from the WebView.\n */\n@Immutable\npublic data class WebViewError(\n    /**\n     * The request the error came from.\n     */\n    val request: WebResourceRequest?,\n    /**\n     * The error that was reported.\n     */\n    val error: WebResourceError\n)\n\n/**\n * Creates a WebView state that is remembered across Compositions.\n *\n * @param url The url to load in the WebView\n * @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl].\n *                              Note that these headers are used for all subsequent requests of the WebView.\n */\n@Composable\npublic fun rememberWebViewState(\n    url: String,\n    additionalHttpHeaders: Map<String, String> = emptyMap()\n): WebViewState =\n// Rather than using .apply {} here we will recreate the state, this prevents\n    // a recomposition loop when the webview updates the url itself.\n    remember {\n        WebViewState(\n            WebContent.Url(\n                url = url,\n                additionalHttpHeaders = additionalHttpHeaders\n            )\n        )\n    }.apply {\n        this.content = WebContent.Url(\n            url = url,\n            additionalHttpHeaders = additionalHttpHeaders\n        )\n    }\n\n/**\n * Creates a WebView state that is remembered across Compositions.\n *\n * @param data The uri to load in the WebView\n */\n@Composable\npublic fun rememberWebViewStateWithHTMLData(\n    data: String,\n    baseUrl: String? = null,\n    encoding: String = \"utf-8\",\n    mimeType: String? = null,\n    historyUrl: String? = null\n): WebViewState =\n    remember {\n        WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl))\n    }.apply {\n        this.content = WebContent.Data(\n            data, baseUrl, encoding, mimeType, historyUrl\n        )\n    }\n\n/**\n * Creates a WebView state that is remembered across Compositions.\n *\n * @param url The url to load in the WebView\n * @param postData The data to be posted to the WebView with the url\n */\n@Composable\npublic fun rememberWebViewState(\n    url: String,\n    postData: ByteArray\n): WebViewState =\n// Rather than using .apply {} here we will recreate the state, this prevents\n    // a recomposition loop when the webview updates the url itself.\n    remember {\n        WebViewState(\n            WebContent.Post(\n                url = url,\n                postData = postData\n            )\n        )\n    }.apply {\n        this.content = WebContent.Post(\n            url = url,\n            postData = postData\n        )\n    }\n\n/**\n * Creates a WebView state that is remembered across Compositions and saved\n * across activity recreation.\n * When using saved state, you cannot change the URL via recomposition. The only way to load\n * a URL is via a WebViewNavigator.\n *\n * @param data The uri to load in the WebView\n */\n@Composable\npublic fun rememberSaveableWebViewState(): WebViewState =\n    rememberSaveable(saver = WebStateSaver) {\n        WebViewState(WebContent.NavigatorOnly)\n    }\n\npublic val WebStateSaver: Saver<WebViewState, Any> = run {\n    val pageTitleKey = \"pagetitle\"\n    val lastLoadedUrlKey = \"lastloaded\"\n    val stateBundle = \"bundle\"\n\n    mapSaver(\n        save = {\n            val viewState = Bundle().apply { it.webView?.saveState(this) }\n            mapOf(\n                pageTitleKey to it.pageTitle,\n                lastLoadedUrlKey to it.lastLoadedUrl,\n                stateBundle to viewState\n            )\n        },\n        restore = {\n            WebViewState(WebContent.NavigatorOnly).apply {\n                this.pageTitle = it[pageTitleKey] as String?\n                this.lastLoadedUrl = it[lastLoadedUrlKey] as String?\n                this.viewState = it[stateBundle] as Bundle?\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/ABDMAppManager.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.widget.Toast\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.core.content.ContextCompat\nimport com.abdownloadmanager.android.pages.onboarding.permissions.PermissionManager\nimport com.abdownloadmanager.android.service.DownloadSystemService\nimport com.abdownloadmanager.android.service.KeepAliveServiceReason\nimport com.abdownloadmanager.android.storage.AppSettingsStorage\nimport com.abdownloadmanager.android.util.notification.playNotificationSoundIfAllowed\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.category.CategorySelectionMode\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.downloaditem.contexts.ResumedBy\nimport ir.amirab.downloader.downloaditem.contexts.User\nimport ir.amirab.downloader.exception.TooManyErrorException\nimport ir.amirab.downloader.queue.DefaultQueueInfo\nimport ir.amirab.downloader.queue.activeQueuesFlow\nimport ir.amirab.downloader.queue.queueModelsFlow\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.combineStringSources\nimport ir.amirab.util.coroutines.launchWithDeferred\nimport ir.amirab.util.guardedEntry\nimport ir.amirab.util.suspendGuardedEntry\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.emptyFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.core.component.KoinComponent\nimport java.util.UUID\nimport kotlin.system.exitProcess\n\nclass ABDMAppManager(\n    private val context: Context,\n    private val scope: CoroutineScope,\n    val downloadSystem: DownloadSystem,\n    val permissionManager: PermissionManager,\n    val notificationManager: NotificationManager,\n    val serviceNotificationManager: ABDMServiceNotificationManager,\n    private val appSettingsStorage: AppSettingsStorage,\n) : KoinComponent, NotificationSender {\n    private var booted = guardedEntry()\n    private var downloadSystemBooted = suspendGuardedEntry()\n    fun isSoundAllowed(): Boolean {\n        return appSettingsStorage.notificationSound.value\n    }\n\n    fun boot() {\n        booted.action {\n            registerAsFallbackNotification()\n        }\n    }\n\n    fun canStartDownloadEngine(): Boolean {\n        return permissionManager.isReady()\n    }\n\n    fun isDownloadSystemBooted(): Boolean {\n        return downloadSystemBooted.isDone()\n    }\n\n    fun isBackgroundServiceRunning(): Boolean {\n        return DownloadSystemService.isServiceRunning()\n    }\n\n    suspend fun startDownloadSystem() {\n        downloadSystemBooted.action {\n            downloadSystem.boot()\n            registerReceivers()\n            registerDownloadEventNotifications()\n        }\n    }\n\n    private var shouldShowToastsNotifications = MutableStateFlow(true)\n    fun setNotificationsHandledInUi(shownInUi: Boolean) {\n        shouldShowToastsNotifications.value = !shownInUi\n    }\n\n    private fun registerAsFallbackNotification(): () -> Unit {\n        val context = context\n        var lastNotificationSound = 0L\n        val job = scope.headlessComposeRuntime {\n            val scope = rememberCoroutineScope()\n            val notifications by notificationManager.activeNotificationList.collectAsState()\n            val shouldShowNotifications by shouldShowToastsNotifications.collectAsState()\n            if (!shouldShowNotifications) {\n                return@headlessComposeRuntime\n            }\n            notifications\n                .firstOrNull()?.let { notification ->\n                    DisposableEffect(notification) {\n                        val title = notification.title.getString()\n                        val description = notification.description.getString()\n                        val iconText = when (notification.notificationType) {\n                            NotificationType.Error -> \"❌\"\n                            NotificationType.Info -> \"ℹ\\uFE0F\"\n                            is NotificationType.Loading -> \"⏳\"\n                            NotificationType.Success -> \"✔\\uFE0F\"\n                            NotificationType.Warning -> \"⚠\\uFE0F\"\n                        }\n                        val fullTitle = \"$iconText $title - $description\"\n                        val toastJob = scope.launch(Dispatchers.Main) {\n                            val toast = Toast.makeText(\n                                context,\n                                fullTitle,\n                                Toast.LENGTH_LONG,\n                            )\n                            if (isSoundAllowed()) {\n                                val now = System.currentTimeMillis()\n                                val sinceLastSoundMillis = now - lastNotificationSound\n                                // don't repeatedly play notification!\n                                if (sinceLastSoundMillis > 5_000) {\n                                    runCatching {\n                                        playNotificationSoundIfAllowed(context)\n                                        lastNotificationSound = now\n                                    }.onFailure {\n                                        it.printStackTrace()\n                                    }\n                                }\n                            }\n                            toast.show()\n                            currentCoroutineContext().job.invokeOnCompletion {\n                                it?.let {\n                                    toast.cancel()\n                                }\n                            }\n                        }\n                        onDispose {\n                            scope.launch(Dispatchers.Main) {\n                                toastJob.cancel()\n                            }\n                        }\n                    }\n                }\n        }\n        return { job.cancel() }\n    }\n\n    private fun registerDownloadEventNotifications() {\n        downloadSystem.downloadEvents.onEach {\n            onNewDownloadEvent(it)\n        }.launchIn(scope)\n    }\n\n    private fun onNewDownloadEvent(it: DownloadManagerEvents) {\n        if (it.context[ResumedBy]?.by !is User) {\n            //only notify events that is started by user\n            return\n        }\n        if (it is DownloadManagerEvents.OnJobCanceled) {\n            val exception = it.e\n            if (ExceptionUtils.isNormalCancellation(exception)) {\n                return\n            }\n            var isMaxTryReachedError = false\n            val actualCause = if (exception is TooManyErrorException) {\n                isMaxTryReachedError = true\n                exception.findActualDownloadErrorCause()\n            } else exception\n            if (ExceptionUtils.isNormalCancellation(actualCause)) {\n                return\n            }\n            val prefix = if (isMaxTryReachedError) {\n                \"Too Many Error: \"\n            } else {\n                \"Error: \"\n            }.asStringSource()\n            val reason = actualCause.message?.asStringSource() ?: Res.string.unknown.asStringSource()\n            sendNotification(\n                \"downloadId=${it.downloadItem.id}\",\n                description = it.downloadItem.name.asStringSource(),\n                title = listOf(prefix, reason).combineStringSources(),\n                type = NotificationType.Error,\n            )\n        }\n        if (it is DownloadManagerEvents.OnJobCompleted) {\n            sendNotification(\n                tag = \"downloadId=${it.downloadItem.id}\",\n                description = it.downloadItem.name.asStringSource(),\n                title = Res.string.finished.asStringSource(),\n                type = NotificationType.Success,\n            )\n        }\n    }\n\n    suspend fun awaitDownloadEngineBoot() {\n        downloadSystemBooted.awaitDone()\n    }\n\n    private fun registerReceivers(): () -> Unit {\n        val receiver = object : BroadcastReceiver() {\n            override fun onReceive(context: Context, intent: Intent) {\n                when (intent.action) {\n                    AndroidConstants.Intents.STOP_ACTION -> {\n                        intent\n                            .getLongExtra(AndroidConstants.Intents.TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID, -1)\n                            .takeIf { it > -1 }\n                            ?.let {\n                                scope.launch {\n                                    downloadSystem.manualPause(it)\n                                }\n                            }\n                    }\n\n                    AndroidConstants.Intents.RESUME_ACTION -> {\n                        intent\n                            .getLongExtra(AndroidConstants.Intents.TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID, -1)\n                            .takeIf { it > -1 }\n                            ?.let {\n                                scope.launch {\n                                    downloadSystem.userManualResume(it)\n                                }\n                            }\n                    }\n\n                    AndroidConstants.Intents.TOGGLE_ACTION -> {\n                        intent\n                            .getLongExtra(AndroidConstants.Intents.TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID, -1)\n                            .takeIf { it > -1 }\n                            ?.let {\n                                scope.launch {\n                                    TODO(\"Toggle action not implemented yet\")\n                                }\n                            }\n                    }\n\n                    AndroidConstants.Intents.STOP_ALL_ACTION -> {\n                        scope.launch {\n                            downloadSystem.stopAnything()\n                        }\n                    }\n\n                    AndroidConstants.Intents.EXIT_ACTION -> {\n                        val job = scope.launch {\n                            downloadSystem.stopAnything()\n                            stopOurService()\n                        }\n                        job.invokeOnCompletion {\n                            exitProcess(0)\n                        }\n                    }\n                }\n            }\n        }\n        ContextCompat.registerReceiver(\n            context,\n            receiver,\n            IntentFilter().apply {\n                addAction(AndroidConstants.Intents.TOGGLE_ACTION)\n                addAction(AndroidConstants.Intents.RESUME_ACTION)\n                addAction(AndroidConstants.Intents.STOP_ACTION)\n                addAction(AndroidConstants.Intents.STOP_ALL_ACTION)\n                addAction(AndroidConstants.Intents.EXIT_ACTION)\n            },\n            ContextCompat.RECEIVER_EXPORTED,\n        )\n        return {\n            for (receiver in listOf(receiver)) {\n                context.unregisterReceiver(receiver)\n            }\n        }\n    }\n\n    suspend fun startOurService() {\n        awaitDownloadEngineBoot()\n        val intent = Intent(context, DownloadSystemService::class.java)\n        withContext(Dispatchers.Main) {\n            ContextCompat.startForegroundService(context, intent)\n        }\n        DownloadSystemService.awaitStart()\n        autoStopService()\n    }\n\n    suspend fun stopOurService() {\n        awaitDownloadEngineBoot()\n        val intent = Intent(context, DownloadSystemService::class.java)\n        withContext(Dispatchers.Main) {\n            context.stopService(intent)\n        }\n    }\n\n    fun startNewDownload(\n        item: NewDownloadItemProps,\n        categoryId: Long?,\n    ): Deferred<Long> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newDownload = item,\n                queueId = DefaultQueueInfo.ID,\n                categoryId = categoryId,\n            ).also {\n                downloadSystem.userManualResume(it)\n            }\n        }\n    }\n\n    fun addDownload(\n        item: NewDownloadItemProps,\n        queueId: Long?,\n        categoryId: Long?,\n    ): Deferred<Long> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newDownload = item,\n                queueId = queueId,\n                categoryId = categoryId,\n            )\n        }\n    }\n\n    fun addDownloads(\n        items: List<NewDownloadItemProps>,\n        categorySelectionMode: CategorySelectionMode?,\n        queueId: Long?,\n    ): Deferred<List<Long>> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newItemsToAdd = items,\n                queueId = queueId,\n                categorySelectionMode = categorySelectionMode,\n            )\n        }\n    }\n\n    override fun sendDialogNotification(\n        title: StringSource,\n        description: StringSource,\n        type: MessageDialogType\n    ) {\n        sendNotification(\n            title = title,\n            description = description,\n            type = when (type) {\n                MessageDialogType.Info -> NotificationType.Info\n                MessageDialogType.Error -> NotificationType.Error\n                MessageDialogType.Success -> NotificationType.Success\n                MessageDialogType.Warning -> NotificationType.Warning\n            },\n            tag = UUID.randomUUID(),\n        )\n    }\n\n    override fun sendNotification(\n        tag: Any,\n        title: StringSource,\n        description: StringSource,\n        type: NotificationType\n    ) {\n        scope.launch {\n            notificationManager.showNotification(\n                title,\n                description,\n                delay = 5_000,\n                type = type,\n            )\n        }\n    }\n\n    /**\n     * in case of the notification permission is granted recently\n     * we ask service notification manager to repost the notification\n     */\n    fun repostServiceNotification() {\n        serviceNotificationManager.updateNotificationWithDefaultValue()\n    }\n\n    fun bootDownloadSystemAndService(): Boolean {\n        if (isDownloadSystemBooted() && isBackgroundServiceRunning()) {\n            return true\n        }\n        if (canStartDownloadEngine()) {\n            scope.launch {\n                startDownloadSystem()\n                if (!isBackgroundServiceRunning()) {\n                    startOurService()\n                }\n            }\n            return true\n        }\n        return false\n    }\n\n    private val mustStayAliveFlow = combine(\n        downloadSystem.downloadMonitor.activeDownloadCount,\n        downloadSystem.queueManager.activeQueuesFlow(),\n        downloadSystem.queueManager.queueModelsFlow(),\n        ApplicationBackgroundTracker.isInBackgroundFlow,\n    ) { activeDownloads, activeQueues, queueModels, isInBackground ->\n        if (activeQueues.isNotEmpty()) {\n            return@combine KeepAliveServiceReason.ActiveQueue(activeQueues.map { it.getQueueModel() })\n        }\n        if (activeDownloads > 0) {\n            return@combine KeepAliveServiceReason.ActiveDownloads(activeDownloads)\n        }\n        val scheduledTimeQueue = queueModels.filter { it.scheduledTimes.enabledStartTime }\n        if (scheduledTimeQueue.isNotEmpty()) {\n            return@combine KeepAliveServiceReason.ScheduledQueues(scheduledTimeQueue)\n        }\n        if (!isInBackground) {\n            return@combine KeepAliveServiceReason.AppIsInForeground\n        }\n        return@combine null\n    }\n\n    private var autoStopServiceJob: Job? = null\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    private fun autoStopService() {\n        synchronized(this) {\n            autoStopServiceJob?.cancel()\n            autoStopServiceJob = scope.launch {\n                mustStayAliveFlow\n                    .distinctUntilChanged()\n                    .onEach {\n                        serviceNotificationManager.setKeepAliveServiceReason(it)\n                    }\n                    .flatMapLatest {\n                        if (it == null) flow {\n                            // let it be null for 10 seconds\n                            delay(10_000)\n                            emit(Unit)\n                        }\n                        else emptyFlow()\n                    }.first()\n                stopOurService()\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/ABDMServiceNotificationManager.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.remember\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.content.ContextCompat\nimport com.abdownloadmanager.android.ui.MainActivity\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.asStringSource\nimport com.abdownloadmanager.android.R\nimport com.abdownloadmanager.android.pages.singledownload.SingleDownloadPageActivity\nimport com.abdownloadmanager.android.service.KeepAliveServiceReason\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.TimeNames\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.convertTimeRemainingToHumanReadable\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass ABDMServiceNotificationManager(\n    private val context: Context,\n    private val downloadMonitor: IDownloadMonitor,\n    private val scope: CoroutineScope,\n    private val downloadEvents: DownloadManagerMinimalControl,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n) {\n    private val _keepAliveServiceReason: MutableStateFlow<KeepAliveServiceReason?> = MutableStateFlow(null)\n\n    fun setKeepAliveServiceReason(reason: KeepAliveServiceReason?) {\n        _keepAliveServiceReason.value = reason\n    }\n\n    val notificationCreationTime = System.currentTimeMillis()\n    private val notificationManagerCompat by lazy {\n        NotificationManagerCompat.from(context)\n    }\n\n    init {\n        registerReceiver()\n    }\n\n    fun registerReceiver() {\n        ContextCompat.registerReceiver(\n            context,\n            object : BroadcastReceiver() {\n                override fun onReceive(context: Context, intent: Intent) {\n                    when (intent.action) {\n                        AndroidConstants.Intents.NOTIFICATION_DELETED -> {\n                            onNotificationDismissed()\n                        }\n                    }\n                }\n            },\n            IntentFilter().apply {\n                this.addAction(AndroidConstants.Intents.NOTIFICATION_DELETED)\n            },\n            ContextCompat.RECEIVER_EXPORTED,\n        )\n    }\n\n    fun updateNotificationWithDefaultValue() {\n        notificationUpdateSignal.update {\n            it + 1\n        }\n    }\n\n    fun onNotificationDismissed() {\n        if (notificationUpdateJob?.isActive == true) {\n            updateNotificationWithDefaultValue()\n        }\n    }\n\n    fun initNotificationChannel() {\n        val notificationChanel = NotificationChannel(\n            AndroidConstants.NOTIFICATION_DOWNLOAD_CHANEL_ID,\n            AndroidConstants.NOTIFICATION_DOWNLOAD_CHANEL_NAME,\n            NotificationManager.IMPORTANCE_LOW,\n        )\n        notificationChanel.setShowBadge(false)\n        notificationManagerCompat.createNotificationChannel(notificationChanel)\n    }\n\n    private val notificationUpdateSignal = MutableStateFlow(0)\n\n    val downloads = downloadMonitor\n        .activeDownloadListFlow\n\n    private var notificationUpdateJob: Job? = null\n    fun startUpdatingNotifications() {\n        synchronized(this) {\n            notificationUpdateJob?.cancel()\n            notificationUpdateJob = scope.headlessComposeRuntime {\n                RenderNotifications(downloadMonitor)\n            }\n        }\n    }\n\n    fun stopUpdatingNotifications() {\n        notificationUpdateJob?.cancel()\n        notificationUpdateJob = null\n    }\n\n    private fun getNotificationIdForDownloadItem(downloadId: Long): Int {\n        return AndroidConstants.SERVICE_NOTIFICATION_ID + 1 + downloadId.hashCode()\n    }\n\n    private fun dismissDownloadNotification(downloadId: Long) {\n        notificationManagerCompat.cancel(getNotificationIdForDownloadItem(downloadId))\n    }\n\n\n    fun dismissNotification() {\n        notificationManagerCompat.cancel(\n            AndroidConstants.SERVICE_NOTIFICATION_ID,\n        )\n    }\n\n    fun createMainNotification(): Notification {\n        return createMainNotification(null, null)\n    }\n\n    fun createMainNotification(\n        reason: KeepAliveServiceReason?,\n        statusString: String?,\n    ): Notification {\n        val flagOfPendingIntent = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT\n        val serviceIsRunningText = Res.string.service_is_running.asStringSource().getString()\n        val exit = Res.string.exit.asStringSource().getString()\n        val stopAll = Res.string.stop_all.asStringSource().getString()\n        val openMainActivityIntent = PendingIntent.getActivity(\n            context,\n            AndroidConstants.SERVICE_NOTIFICATION_ID,\n            Intent(context, MainActivity::class.java),\n            flagOfPendingIntent\n        )\n        return NotificationCompat\n            .Builder(context, AndroidConstants.NOTIFICATION_DOWNLOAD_CHANEL_ID)\n            .setContentTitle(serviceIsRunningText)\n            .setContentText(statusString)\n            .setSmallIcon(R.drawable.ic_monochrome)\n            // group\n//            .setGroupSummary(true)\n//            .setGroup(DOWNLOAD_GROUP_NAME)\n            .setOnlyAlertOnce(true)\n            .setOngoing(true)\n            .setShowWhen(false)\n            .setWhen(notificationCreationTime)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            // prevent delete by user until we are active\n            .setDeleteIntent(\n                PendingIntent.getBroadcast(\n                    context,\n                    AndroidConstants.SERVICE_NOTIFICATION_ID,\n                    Intent(AndroidConstants.Intents.NOTIFICATION_DELETED),\n                    flagOfPendingIntent,\n                )\n            )\n            // actions\n            .setContentIntent(openMainActivityIntent)\n            .addAction(\n                0, exit, PendingIntent.getBroadcast(\n                    context,\n                    AndroidConstants.SERVICE_NOTIFICATION_ID,\n                    Intent(AndroidConstants.Intents.EXIT_ACTION),\n                    flagOfPendingIntent,\n                )\n            )\n            .addAction(\n                0, stopAll, PendingIntent.getBroadcast(\n                    context,\n                    AndroidConstants.SERVICE_NOTIFICATION_ID,\n                    Intent(AndroidConstants.Intents.STOP_ALL_ACTION),\n                    flagOfPendingIntent,\n                )\n            )\n            .build()\n    }\n\n    fun createDownloadItemNotification(\n        downloadItemState: ProcessingDownloadItemState\n    ): Notification {\n        val flagOfPendingIntent = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT\n\n        val title = downloadItemState.name\n\n        val speedUnit = sizeAndSpeedUnitProvider.speedUnit.value\n        val percent = downloadItemState.percent?.let { \"$it%\" }\n        val eta = downloadItemState.remainingTime?.let {\n            convertTimeRemainingToHumanReadable(it, TimeNames.ShortNames)\n        }\n        val speed = convertPositiveSpeedToHumanReadable(downloadItemState.speed, speedUnit)\n        val statusString = listOfNotNull(speed, eta)\n            .joinToString(\" - \")\n            .takeIf { it.isNotEmpty() }\n\n\n        val openMainActivityIntent = PendingIntent.getActivity(\n            context,\n            AndroidConstants.SERVICE_NOTIFICATION_ID,\n            SingleDownloadPageActivity.createIntent(\n                context, downloadItemState.id, true\n            ),\n            flagOfPendingIntent\n        )\n        val status = downloadItemState.status\n        return NotificationCompat\n            .Builder(context, AndroidConstants.NOTIFICATION_DOWNLOAD_CHANEL_ID)\n            .setContentTitle(title)\n            .setContentText(statusString)\n            .setSubText(percent)\n            .setProgress(100, downloadItemState.percent ?: 0, downloadItemState.percent == null)\n            .setSmallIcon(R.drawable.ic_monochrome)\n            .setGroup(DOWNLOAD_GROUP_NAME)\n            .setOngoing(true)\n            .setOnlyAlertOnce(true)\n            .setShowWhen(false)\n            .setWhen(notificationCreationTime)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .apply {\n                if (status is DownloadJobStatus.IsActive) {\n                    addAction(\n                        0,\n                        Res.string.pause.asStringSource().getString(),\n                        PendingIntent.getBroadcast(\n                            context,\n                            AndroidConstants.SERVICE_NOTIFICATION_ID,\n                            Intent(AndroidConstants.Intents.STOP_ACTION).apply {\n                                putExtra(\n                                    AndroidConstants.Intents.TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID,\n                                    downloadItemState.id\n                                )\n                            },\n                            flagOfPendingIntent,\n                        )\n                    )\n                } else if (status is DownloadJobStatus.CanBeResumed) {\n                    addAction(\n                        0,\n                        Res.string.resume.asStringSource().getString(),\n                        PendingIntent.getBroadcast(\n                            context,\n                            AndroidConstants.SERVICE_NOTIFICATION_ID,\n                            Intent(AndroidConstants.Intents.RESUME_ACTION).apply {\n                                putExtra(\n                                    AndroidConstants.Intents.TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID,\n                                    downloadItemState.id\n                                )\n                            },\n                            flagOfPendingIntent,\n                        )\n                    )\n                }\n            }\n            .setContentIntent(openMainActivityIntent)\n            .build()\n    }\n\n    @Composable\n    fun RenderNotifications(\n        downloadMonitor: IDownloadMonitor\n    ) {\n        val notFinishedDownloads by downloadMonitor.activeDownloadListFlow.collectAsState()\n        val keepAliveServiceReason by _keepAliveServiceReason.collectAsState()\n        val notifyUpdate by notificationUpdateSignal.collectAsState()\n        CompositionLocalProvider(\n            LocalNotificationUpdateSignal provides notifyUpdate\n        ) {\n            RenderMainNotification(\n                reason = keepAliveServiceReason\n            )\n            RenderDownloadItemNotifications(\n                remember(notFinishedDownloads) {\n                    notFinishedDownloads.filter {\n                        it.status is DownloadJobStatus.IsActive\n                    }\n                }\n            )\n        }\n    }\n\n\n    @Composable\n    fun RenderMainNotification(\n        reason: KeepAliveServiceReason?,\n    ) {\n        val statusString = reason?.rememberReasonString()\n        LaunchedEffect(reason, statusString, LocalNotificationUpdateSignal.current) {\n            notificationManagerCompat.notify(\n                AndroidConstants.SERVICE_NOTIFICATION_ID,\n                createMainNotification(reason, statusString)\n            )\n        }\n        DisposableEffect(Unit) {\n            onDispose {\n                dismissNotification()\n            }\n        }\n    }\n\n    @Composable\n    fun RenderDownloadItemNotifications(\n        activeDownloads: List<ProcessingDownloadItemState>\n    ) {\n        for (downloadItemState in activeDownloads) {\n            key(downloadItemState.id) {\n                RenderDownloadItemNotification(\n                    downloadItemState\n                )\n            }\n        }\n    }\n\n    @Composable\n    fun RenderDownloadItemNotification(\n        iDownloadItemState: ProcessingDownloadItemState\n    ) {\n        LaunchedEffect(iDownloadItemState, LocalNotificationUpdateSignal.current) {\n            notificationManagerCompat.notify(\n                getNotificationIdForDownloadItem(iDownloadItemState.id),\n                createDownloadItemNotification(iDownloadItemState)\n            )\n        }\n        DisposableEffect(iDownloadItemState.id) {\n            onDispose {\n                dismissDownloadNotification(iDownloadItemState.id)\n            }\n        }\n    }\n\n    companion object {\n        private const val DOWNLOAD_GROUP_NAME = \"Downloads\"\n    }\n}\n\nprivate val LocalNotificationUpdateSignal = compositionLocalOf<Int> {\n    error(\"LocalNotificationUpdateSignal not provided\")\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidConstants.kt",
    "content": "package com.abdownloadmanager.android.util\n\nobject AndroidConstants {\n    const val SERVICE_NOTIFICATION_ID = 1\n    const val NOTIFICATION_DOWNLOAD_CHANEL_ID = \"downloads\"\n    const val NOTIFICATION_DOWNLOAD_CHANEL_NAME = \"Download Manager Service\"\n\n    const val NOTIFICATION_CRASH_REPORT_CHANEL_ID = \"crashReport\"\n    const val NOTIFICATION_CRASH_REPORT_CHANEL_NAME = \"Crash Report\"\n\n    object Intents {\n        private const val prefix = \"com.abdownloadmanager.\"\n        const val STOP_ALL_ACTION = prefix + \"STOP_ALL\"\n        const val STOP_ACTION = prefix + \"STOP\"\n        const val RESUME_ACTION = prefix + \"RESUME\"\n        const val TOGGLE_ACTION = prefix + \"TOGGLE\"\n        const val NOTIFICATION_DELETED = prefix + \"NOTIFICATION_DELETED\"\n\n        // download id\n        const val TOGGLE_DOWNLOAD_ACTION_DOWNLOAD_ID = \"downloadId\"\n        const val EXIT_ACTION = prefix + \"EXIT\"\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidDefinedPaths.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport com.abdownloadmanager.shared.util.DefinedPaths\nimport okio.Path\n\nclass AndroidDefinedPaths(\n    dataDir: Path,\n) : DefinedPaths(\n    dataDir = dataDir\n) {\n    val lastSavedLocationFile = pagesStateDir.resolve(\"lastSavedLocation.json\")\n    val onboardingFile = pagesStateDir.resolve(\"onboarding.json\")\n    val homePageFile = pagesStateDir.resolve(\"home.json\")\n    val browserBookmarksFile = pagesStateDir.resolve(\"browser_bookmarks.json\")\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidDownloadItemOpener.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.util.osfileutil.FileUtils\nimport java.io.File\n\nclass AndroidDownloadItemOpener(\n    private val downloadSystem: DownloadSystem\n) : DownloadItemOpener {\n    override suspend fun openDownloadItem(id: Long) {\n        downloadSystem.getDownloadItemById(id)?.let {\n            openDownloadItem(it)\n        }\n    }\n\n    override suspend fun openDownloadItem(downloadItem: IDownloadItem) {\n        try {\n            FileUtils.openFile(File(downloadItem.folder, downloadItem.name))\n        } catch (e: Exception) {\n            // toast something\n        }\n    }\n\n    override suspend fun openDownloadItemFolder(id: Long) {\n        downloadSystem.getDownloadItemById(id)?.let {\n            openDownloadItemFolder(it)\n        }\n    }\n\n    override suspend fun openDownloadItemFolder(downloadItem: IDownloadItem) {\n        try {\n            FileUtils.openFolderOfFile(File(downloadItem.folder, downloadItem.name))\n        } catch (e: Exception) {\n            // toast something\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidGlobalExceptionHandler.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.util.Log\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport com.abdownloadmanager.android.R\nimport com.abdownloadmanager.android.pages.crashreport.CrashReportActivity\nimport kotlin.system.exitProcess\n\nclass AndroidGlobalExceptionHandler(\n    private val context: Context,\n    private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler?,\n) : Thread.UncaughtExceptionHandler {\n    private val crashNotificationManager = CrashNotificationManager(context)\n    override fun uncaughtException(t: Thread, e: Throwable) {\n        runCatching {\n            handleUncaughtException(t, e)\n        }\n        defaultUncaughtExceptionHandler\n            ?.uncaughtException(t, e)\n            ?: run {\n                Log.e(\"Crash\", e.localizedMessage, e)\n                exitProcess(1)\n            }\n    }\n\n    private fun handleUncaughtException(t: Thread, e: Throwable) {\n        val intent = CrashReportActivity\n            .createIntent(context, e)\n            .addFlags(\n                Intent.FLAG_ACTIVITY_CLEAR_TOP or\n                        Intent.FLAG_ACTIVITY_NEW_TASK or\n                        Intent.FLAG_ACTIVITY_CLEAR_TASK\n            )\n        if (ApplicationBackgroundTracker.isInBackground()) {\n            // show a notification so user can press and see the crash screen\n            crashNotificationManager.postNotificationAboutTheCrash(context, intent)\n        } else {\n            // in case we are in the foreground directly show the error\n            context.startActivity(intent)\n        }\n    }\n\n    private class CrashNotificationManager(private val context: Context) {\n        val notificationManagerCompat by lazy {\n            NotificationManagerCompat.from(context)\n        }\n        private var initialized = false\n        fun initNotificationChannel() {\n            if (initialized) {\n                return\n            }\n            runCatching {\n                val notificationChanel = NotificationChannel(\n                    AndroidConstants.NOTIFICATION_CRASH_REPORT_CHANEL_ID,\n                    AndroidConstants.NOTIFICATION_CRASH_REPORT_CHANEL_NAME,\n                    NotificationManager.IMPORTANCE_LOW,\n                )\n                notificationChanel.setShowBadge(false)\n                notificationManagerCompat.createNotificationChannel(notificationChanel)\n            }\n            initialized = true\n        }\n\n        fun postNotificationAboutTheCrash(context: Context, intent: Intent) {\n            initNotificationChannel()\n            val notificationId = 555\n            val pendingIntent =\n                PendingIntent.getActivity(\n                    context, notificationId, intent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n                )\n            val notification = NotificationCompat\n                .Builder(context, AndroidConstants.NOTIFICATION_CRASH_REPORT_CHANEL_ID)\n                .setSmallIcon(R.drawable.ic_monochrome)\n                .setContentTitle(\"Application crashed!\")\n                .setSubText(\"Click to show info\")\n                .setGroup(\"Crash Report\")\n                .setPriority(NotificationCompat.PRIORITY_LOW)\n                .setAutoCancel(true)\n                .setContentIntent(pendingIntent)\n                .build()\n            runCatching {\n                notificationManagerCompat.notify(notificationId, notification)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidIntentUtils.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.content.FileProvider\nimport java.io.File\n\nobject AndroidIntentUtils {\n    fun shareText(context: Context, text: String) {\n        val intent = Intent(Intent.ACTION_SEND)\n        intent.type = \"text/plain\"\n        intent.putExtra(Intent.EXTRA_TEXT, text)\n        context.startActivity(Intent.createChooser(intent, \"Share Via\"))\n    }\n    fun shareFiles(context: Context, files: List<File>) {\n        if (files.isEmpty()) return\n\n        val uris = files.map { file ->\n            FileProvider.getUriForFile(\n                context,\n                \"${context.packageName}.provider\",\n                file\n            )\n        }\n\n        val intent = if (uris.size == 1) {\n            Intent(Intent.ACTION_SEND).apply {\n                type = \"*/*\" // You can detect type from file extension if you want\n                putExtra(Intent.EXTRA_STREAM, uris[0])\n            }\n        } else {\n            Intent(Intent.ACTION_SEND_MULTIPLE).apply {\n                type = \"*/*\"\n                putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))\n            }\n        }\n\n        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        context.startActivity(Intent.createChooser(intent, \"Share via\"))\n    }\n\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AndroidUi.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.guardedEntry\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nobject AndroidUi : KoinComponent {\n    val themeManager: ThemeManager by inject()\n    val languageManager: LanguageManager by inject()\n    private var booted = guardedEntry()\n    fun boot() {\n        booted.action {\n            themeManager.boot()\n            languageManager.boot()\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/AppInfo.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.app.Application\nimport com.abdownloadmanager.android.BuildConfig\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport ir.amirab.util.platform.Platform\nimport okio.Path.Companion.toOkioPath\n\nobject AppInfo {\n    val isInDebugMode: Boolean = BuildConfig.DEBUG\n    lateinit var context: Application\n    fun init(context: Application) {\n        this.context = context\n    }\n\n    val platform = Platform.Android\n    val version = AppVersion.get()\n\n    val definedPaths by lazy {\n        AndroidDefinedPaths(\n            dataDir = context.filesDir.resolve(\n                SharedConstants.dataDirName\n            ).toOkioPath()\n        )\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/ApplicationBackgroundTracker.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport android.app.Activity\nimport android.app.Application\nimport android.os.Bundle\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\nobject ApplicationBackgroundTracker {\n    fun startTracking(application: Application) {\n        application.registerActivityLifecycleCallbacks(Tracker)\n    }\n    val isInBackgroundFlow = Tracker.count.mapStateFlow {\n        it == 0\n    }\n    fun isInBackground(): Boolean {\n        return isInBackgroundFlow.value\n    }\n}\n\nprivate object Tracker : Application.ActivityLifecycleCallbacks {\n    val count = MutableStateFlow(0)\n    override fun onActivityStarted(activity: Activity) {\n        count.update { it + 1 }\n    }\n\n    override fun onActivityStopped(activity: Activity) {\n        count.update { it - 1 }\n    }\n\n\n    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {\n    }\n\n    override fun onActivityDestroyed(activity: Activity) {\n    }\n\n    override fun onActivityPaused(activity: Activity) {\n    }\n\n    override fun onActivityResumed(activity: Activity) {\n    }\n\n    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/HeadlessComposeRuntime.kt",
    "content": "package com.abdownloadmanager.android.util\n\nimport androidx.compose.runtime.AbstractApplier\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Composition\nimport androidx.compose.runtime.MonotonicFrameClock\nimport androidx.compose.runtime.Recomposer\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\n\nfun CoroutineScope.headlessComposeRuntime(\n    content: @Composable () -> Unit,\n): Job {\n    val effectCoroutineContext = coroutineContext + HeadlessDefaultMonotonicFrameClock\n    val recomposer = Recomposer(effectCoroutineContext)\n    val composition = Composition(UnitApplier, recomposer)\n    composition.setContent(content)\n    val job = launch(HeadlessDefaultMonotonicFrameClock) {\n        try {\n            recomposer.runRecomposeAndApplyChanges()\n        } catch (e: Throwable) {\n            e.printStackTrace()\n            composition.dispose()\n        }\n    }\n    return job\n}\n\nprivate object UnitApplier : AbstractApplier<Unit>(Unit) {\n    override fun insertBottomUp(index: Int, instance: Unit) {}\n    override fun insertTopDown(index: Int, instance: Unit) {}\n    override fun move(from: Int, to: Int, count: Int) {}\n    override fun remove(index: Int, count: Int) {}\n    override fun onClear() {}\n}\n\nprivate object HeadlessDefaultMonotonicFrameClock : MonotonicFrameClock {\n    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {\n        return onFrame(System.nanoTime())\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/activity/ABDMActivity.kt",
    "content": "package com.abdownloadmanager.android.util.activity\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.SystemBarStyle\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.core.view.WindowInsetsControllerCompat\nimport com.abdownloadmanager.android.storage.AndroidOnBoardingStorage\nimport com.abdownloadmanager.android.storage.HomePageStorage\nimport com.abdownloadmanager.android.ui.ABDownloadManagerApplicationContent\nimport com.abdownloadmanager.android.util.ABDMAppManager\nimport com.abdownloadmanager.android.util.AndroidUi\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.ui.MyColors\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.retainedComponent\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.getValue\n\nabstract class ABDMActivity : ComponentActivity(), KoinComponent {\n    val languageManager: LanguageManager by inject()\n    val themeManager: ThemeManager by inject()\n    val appSettingsStorage: BaseAppSettingsStorage by inject()\n    val iconResolver: IIconResolver by inject()\n    val appRepository: BaseAppRepository by inject()\n    val notificationManager: NotificationManager by inject()\n    val applicationScope: CoroutineScope by inject()\n    val perHostSettingsManager: PerHostSettingsManager by inject()\n    val abdmAppManager: ABDMAppManager by inject()\n    val onBoardingStorage: AndroidOnBoardingStorage by inject()\n    val homePageStorage: HomePageStorage by inject()\n\n    open fun handleIntent(intent: Intent) {}\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        handleIntent(intent)\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        AndroidUi.boot()\n        val isLight = themeManager.currentThemeColor.value.isLight\n        val transparent = Color.Transparent.toArgb()\n        val systemBarStyle = if (isLight) {\n            SystemBarStyle.light(transparent, transparent)\n        } else {\n            SystemBarStyle.dark(transparent)\n        }\n        enableEdgeToEdge(\n            statusBarStyle = systemBarStyle,\n            navigationBarStyle = systemBarStyle,\n        )\n        if (savedInstanceState == null) {\n            handleIntent(intent)\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        abdmAppManager.bootDownloadSystemAndService()\n    }\n\n    @Composable\n    private fun UpdateSystemBarColors(\n        myColors: MyColors,\n    ) {\n        val window = window\n        val isLight = myColors.isLight\n        LaunchedEffect(isLight) {\n            val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)\n            windowInsetsController.isAppearanceLightStatusBars = isLight\n            windowInsetsController.isAppearanceLightNavigationBars = isLight\n        }\n    }\n\n    fun setABDMContent(\n        content: @Composable () -> Unit,\n    ) {\n        setContent {\n            val theme by themeManager.currentThemeColor.collectAsState()\n            UpdateSystemBarColors(theme)\n            ABDownloadManagerApplicationContent(\n                languageManager = languageManager,\n                themeManager = themeManager,\n                appSettingsStorage = appSettingsStorage,\n                iconResolver = iconResolver,\n                appRepository = appRepository,\n                notificationManager = notificationManager,\n                content = content,\n            )\n        }\n    }\n\n    fun <T> myRetainedComponent(factory: RetainedComponentContainer<T>.(ComponentContext) -> T): RetainedComponentContainer<T> {\n        return retainedComponent { RetainedComponentContainer(it, factory) }\n            .reinitialize(this)\n    }\n}\n\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/activity/ActivityActions.kt",
    "content": "package com.abdownloadmanager.android.util.activity\n\nimport android.content.Intent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\n\ninterface ActivityActions {\n    fun startActivityAction(intent: Intent)\n    fun finishActivityAction()\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/activity/RetainedComponentContainer.kt",
    "content": "package com.abdownloadmanager.android.util.activity\n\nimport android.content.Intent\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.runtime.Composable\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.childContext\nimport java.lang.ref.WeakReference\n\nclass RetainedComponentContainer<T>(\n    ctx: ComponentContext,\n    factory: RetainedComponentContainer<T>.(ComponentContext) -> T\n) : BaseComponent(ctx),\n    ContainsEffects<RetainedComponentContainer.Effects> by supportEffects(),\n    ActivityActions {\n    private var currentActivity: WeakReference<ComponentActivity> = WeakReference(null)\n    fun reinitialize(activity: ComponentActivity) = apply {\n        this.currentActivity = WeakReference(activity)\n    }\n\n    fun getCurrentActivity(): ComponentActivity? {\n        return currentActivity.get()\n    }\n\n    override fun startActivityAction(intent: Intent) {\n        sendEffect(Effects.StartActivity(intent))\n    }\n\n    override fun finishActivityAction() {\n        sendEffect(Effects.FinishActivity)\n    }\n\n    // it's better to create scope for factory to prevent accidentally accessing this\n    // for now make sure to not use [component] inside factory!\n    val component: T by lazy {\n        factory(childContext(\"main\"))\n    }\n\n    sealed interface Effects {\n        data class StartActivity(val intent: Intent) : Effects\n        data object FinishActivity : Effects\n    }\n}\n\n@Composable\nfun RetainedComponentContainer<*>.HandleActivityEffects() {\n    val activity = LocalActivity.current\n    HandleEffects(this) {\n        when (it) {\n            RetainedComponentContainer.Effects.FinishActivity -> {\n                activity?.finish()\n            }\n\n            is RetainedComponentContainer.Effects.StartActivity -> {\n                activity?.startActivity(it.intent)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/activity/SerializableExtra.kt",
    "content": "package com.abdownloadmanager.android.util.activity\n\nimport android.content.Intent\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.serializer\n\n\ncontext(json: Json)\nfun <T> Intent.getSerializedExtra(name: String, serializer: KSerializer<T>): T? {\n    return getStringExtra(name)?.let {\n        json.decodeFromString(serializer, it)\n    }\n}\n\ncontext(json: Json)\nfun <T> Intent.putSerializedExtra(name: String, data: T, serializer: KSerializer<T>) {\n    putExtra(\n        name,\n        json.encodeToString(serializer, data)\n    )\n}\n\ncontext(json: Json)\ninline fun <reified T> Intent.getSerializedExtra(name: String): T? {\n    return getSerializedExtra(name, serializer())\n}\n\ncontext(json: Json)\ninline fun <reified T> Intent.putSerializedExtra(name: String, data: T) {\n    putSerializedExtra(name, data, serializer())\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/compose/ObserveUiVisibility.kt",
    "content": "package com.abdownloadmanager.android.util.compose\n\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.rememberUpdatedState\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.lifecycle.compose.currentStateAsState\nimport androidx.lifecycle.repeatOnLifecycle\nimport kotlinx.coroutines.awaitCancellation\n\n@Composable\nfun ObserveUiVisibility(\n    onVisibilityChange: (isVisible: Boolean) -> Unit,\n) {\n    val onVisibilityChange by rememberUpdatedState(onVisibilityChange)\n    val lifecycleOwner = LocalLifecycleOwner.current\n    LaunchedEffect(lifecycleOwner) {\n        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {\n            try {\n                onVisibilityChange(true)\n                awaitCancellation()\n            } finally {\n                onVisibilityChange(false)\n            }\n        }\n    }\n}\n\n@Composable\nfun rememberIsUiVisible(): Boolean {\n    val lifecycleOwner = LocalLifecycleOwner.current\n    val currentState by lifecycleOwner.lifecycle.currentStateAsState()\n    return currentState.isAtLeast(Lifecycle.State.RESUMED)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/compose/useBack.kt",
    "content": "package com.abdownloadmanager.android.util.compose\n\nimport androidx.activity.OnBackPressedDispatcher\nimport androidx.activity.compose.LocalOnBackPressedDispatcherOwner\nimport androidx.compose.runtime.Composable\n\n@Composable\nfun useBack(): OnBackPressedDispatcher? {\n    return LocalOnBackPressedDispatcherOwner.current\n        ?.onBackPressedDispatcher\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/notification/playNotificationSoundIfAllowed.kt",
    "content": "package com.abdownloadmanager.android.util.notification\n\nimport android.app.NotificationManager\nimport android.content.Context\nimport android.media.AudioAttributes\nimport android.media.AudioManager\nimport android.media.RingtoneManager\nimport androidx.core.content.getSystemService\n\nfun playNotificationSoundIfAllowed(\n    context: Context\n) {\n    if (isInDNDMode(context)) {\n        return\n    }\n    if (isInSilentMode(context)) {\n        return\n    }\n    val uri = RingtoneManager\n        .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)\n        ?: return\n    val ringtone = RingtoneManager\n        .getRingtone(context, uri)\n        ?: return\n\n    ringtone.audioAttributes = AudioAttributes.Builder()\n        .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)\n        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)\n        .build()\n\n    ringtone.play()\n    return\n}\n\nprivate fun isInDNDMode(context: Context): Boolean {\n    val notificationManager = context.getSystemService<NotificationManager>() ?: return false\n    return notificationManager.currentInterruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL\n}\n\nprivate fun isInSilentMode(context: Context): Boolean {\n    val audioManager = context.getSystemService<AudioManager>() ?: return false\n    val volume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION)\n    return audioManager.ringerMode != AudioManager.RINGER_MODE_NORMAL || volume == 0\n}\n\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/pagemanager/BrowserPageManager.kt",
    "content": "package com.abdownloadmanager.android.util.pagemanager\n\ninterface IBrowserPageManager {\n    fun openBrowser(url: String?)\n}\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/abdownloadmanager/android/util/pagemanager/PermissionsPageManager.kt",
    "content": "package com.abdownloadmanager.android.util.pagemanager\n\ninterface PermissionsPageManager {\n    fun openPermissionsPage(openHomeAfterFinish: Boolean)\n    fun closePermissionsPage()\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:aapt=\"http://schemas.android.com/aapt\"\n        android:width=\"108dp\"\n        android:height=\"108dp\"\n        android:viewportWidth=\"512\"\n        android:viewportHeight=\"512\">\n    <path\n            android:pathData=\"M0,0H512V512H0V0Z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                    android:startX=\"0\"\n                    android:startY=\"512\"\n                    android:endX=\"512\"\n                    android:endY=\"512\"\n                    android:type=\"linear\">\n                <item android:offset=\"0\" android:color=\"#FF242E35\"/>\n                <item android:offset=\"1\" android:color=\"#FF2E2135\"/>\n            </gradient>\n        </aapt:attr>\n    </path>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:aapt=\"http://schemas.android.com/aapt\"\n        android:width=\"108dp\"\n        android:height=\"108dp\"\n        android:viewportWidth=\"512\"\n        android:viewportHeight=\"512\">\n    <group android:scaleX=\"0.5\"\n           android:scaleY=\"0.5\"\n           android:translateX=\"128\"\n           android:translateY=\"128\">\n        <path\n                android:pathData=\"M76.81,256C104.57,256 126.2,279.34 136.82,304.98C143.26,320.51 152.68,334.62 164.57,346.51C176.46,358.4 190.57,367.82 206.1,374.26C221.63,380.69 238.27,384 255.08,384C271.89,384 288.53,380.69 304.06,374.26C319.59,367.82 333.7,358.4 345.59,346.51C357.48,334.62 366.9,320.51 373.34,304.98C383.96,279.34 405.58,256 433.34,256H512C512,289.62 505.38,322.91 492.51,353.97C479.65,385.03 460.79,413.25 437.02,437.02C413.25,460.79 385.03,479.65 353.97,492.51C322.91,505.38 289.62,512 256,512C222.38,512 189.09,505.38 158.03,492.51C126.97,479.65 98.75,460.79 74.98,437.02C51.21,413.25 32.35,385.03 19.49,353.97C6.62,322.91 0,289.62 0,256H76.81ZM277.76,0C282.77,0 286.83,4.26 286.83,9.51V190.29C286.83,195.55 290.89,199.81 295.89,199.81H310.92C318.29,199.81 322.58,208.56 318.3,214.85L263.38,295.54C259.76,300.85 252.24,300.85 248.62,295.54L193.71,214.85C189.42,208.56 193.71,199.81 201.08,199.81H216.1C221.11,199.81 225.17,195.55 225.17,190.29V9.51C225.17,4.26 229.23,0 234.24,0H277.76ZM50.44,139.02C53.65,130.7 62.88,126.61 71.04,129.89C79.2,133.18 83.2,142.59 79.99,150.91L69.06,179.18C65.84,187.5 56.62,191.59 48.46,188.31C40.3,185.02 36.29,175.61 39.51,167.29L50.44,139.02ZM443.52,129.89C451.68,126.61 460.91,130.7 464.12,139.02L475.05,167.29C478.27,175.61 474.26,185.02 466.1,188.31C457.94,191.59 448.72,187.5 445.5,179.18L434.57,150.91C431.36,142.59 435.36,133.18 443.52,129.89ZM148.94,36.76C155.83,31.22 165.82,32.42 171.25,39.45C176.68,46.47 175.51,56.66 168.62,62.2L145.24,81.03C138.36,86.57 128.37,85.37 122.94,78.35C117.5,71.32 118.68,61.13 125.57,55.59L148.94,36.76ZM343.31,39.45C348.74,32.42 358.73,31.22 365.62,36.76L388.99,55.59C395.88,61.13 397.06,71.32 391.62,78.35C386.19,85.37 376.2,86.57 369.31,81.03L345.94,62.2C339.05,56.66 337.88,46.47 343.31,39.45Z\">\n            <aapt:attr name=\"android:fillColor\">\n                <gradient\n                        android:startX=\"533.99\"\n                        android:startY=\"245.48\"\n                        android:endX=\"-17.93\"\n                        android:endY=\"245.48\"\n                        android:type=\"linear\">\n                    <item android:offset=\"0\" android:color=\"#FFC631FF\"/>\n                    <item android:offset=\"1\" android:color=\"#FF4DC4FE\"/>\n                </gradient>\n            </aapt:attr>\n        </path>\n        <path\n                android:pathData=\"M437.02,437.02C389.01,485.03 323.89,512 256,512C188.1,512 122.99,485.03 74.98,437.02L88.47,423.53C108,404 139.47,404.74 163.46,418.41C191.41,434.34 223.26,442.92 256,442.92C288.73,442.92 320.58,434.34 348.54,418.41C372.53,404.74 404,404 423.53,423.53L437.02,437.02Z\"\n                android:fillColor=\"#000000\"\n                android:fillAlpha=\"0.25\"/>\n    </group>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"512dp\"\n        android:height=\"512dp\"\n        android:viewportWidth=\"512\"\n        android:viewportHeight=\"512\">\n    <group android:scaleX=\"0.5\"\n           android:scaleY=\"0.5\"\n           android:translateX=\"128\"\n           android:translateY=\"128\">\n        <path\n                android:pathData=\"M76.81,256C104.57,256 126.2,279.34 136.82,304.98C143.26,320.51 152.68,334.62 164.57,346.51C176.46,358.4 190.57,367.82 206.1,374.26C221.63,380.69 238.27,384 255.08,384C271.89,384 288.53,380.69 304.06,374.26C319.59,367.82 333.7,358.4 345.59,346.51C357.48,334.62 366.9,320.51 373.34,304.98C383.96,279.34 405.58,256 433.34,256H512C512,289.62 505.38,322.91 492.51,353.97C479.65,385.03 460.79,413.25 437.02,437.02C413.25,460.79 385.03,479.65 353.97,492.51C322.91,505.38 289.62,512 256,512C222.38,512 189.09,505.38 158.03,492.51C126.97,479.65 98.75,460.79 74.98,437.02C51.21,413.25 32.35,385.03 19.49,353.97C6.62,322.91 0,289.62 0,256H76.81ZM277.76,0C282.77,0 286.83,4.26 286.83,9.51V190.29C286.83,195.55 290.89,199.81 295.89,199.81H310.92C318.29,199.81 322.58,208.56 318.3,214.85L263.38,295.54C259.76,300.85 252.24,300.85 248.62,295.54L193.71,214.85C189.42,208.56 193.71,199.81 201.08,199.81H216.1C221.11,199.81 225.17,195.55 225.17,190.29V9.51C225.17,4.26 229.23,0 234.24,0H277.76ZM50.44,139.02C53.65,130.7 62.88,126.61 71.04,129.89C79.2,133.18 83.2,142.59 79.99,150.91L69.06,179.18C65.84,187.5 56.62,191.59 48.46,188.31C40.3,185.02 36.29,175.61 39.51,167.29L50.44,139.02ZM443.52,129.89C451.68,126.61 460.91,130.7 464.12,139.02L475.05,167.29C478.27,175.61 474.26,185.02 466.1,188.31C457.94,191.59 448.72,187.5 445.5,179.18L434.57,150.91C431.36,142.59 435.36,133.18 443.52,129.89ZM148.94,36.76C155.83,31.22 165.82,32.42 171.25,39.45C176.68,46.47 175.51,56.66 168.62,62.2L145.24,81.03C138.36,86.57 128.37,85.37 122.94,78.35C117.5,71.32 118.68,61.13 125.57,55.59L148.94,36.76ZM343.31,39.45C348.74,32.42 358.73,31.22 365.62,36.76L388.99,55.59C395.88,61.13 397.06,71.32 391.62,78.35C386.19,85.37 376.2,86.57 369.31,81.03L345.94,62.2C339.05,56.66 337.88,46.47 343.31,39.45Z\"\n                android:fillColor=\"#ffffff\"/>\n    </group>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_monochrome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"512dp\"\n        android:height=\"512dp\"\n        android:viewportWidth=\"512\"\n        android:viewportHeight=\"512\">\n    <path\n            android:pathData=\"M76.81,256C104.57,256 126.2,279.34 136.82,304.98C143.26,320.51 152.68,334.62 164.57,346.51C176.46,358.4 190.57,367.82 206.1,374.26C221.63,380.69 238.27,384 255.08,384C271.89,384 288.53,380.69 304.06,374.26C319.59,367.82 333.7,358.4 345.59,346.51C357.48,334.62 366.9,320.51 373.34,304.98C383.96,279.34 405.58,256 433.34,256H512C512,289.62 505.38,322.91 492.51,353.97C479.65,385.03 460.79,413.25 437.02,437.02C413.25,460.79 385.03,479.65 353.97,492.51C322.91,505.38 289.62,512 256,512C222.38,512 189.09,505.38 158.03,492.51C126.97,479.65 98.75,460.79 74.98,437.02C51.21,413.25 32.35,385.03 19.49,353.97C6.62,322.91 0,289.62 0,256H76.81ZM277.76,0C282.77,0 286.83,4.26 286.83,9.51V190.29C286.83,195.55 290.89,199.81 295.89,199.81H310.92C318.29,199.81 322.58,208.56 318.3,214.85L263.38,295.54C259.76,300.85 252.24,300.85 248.62,295.54L193.71,214.85C189.42,208.56 193.71,199.81 201.08,199.81H216.1C221.11,199.81 225.17,195.55 225.17,190.29V9.51C225.17,4.26 229.23,0 234.24,0H277.76ZM50.44,139.02C53.65,130.7 62.88,126.61 71.04,129.89C79.2,133.18 83.2,142.59 79.99,150.91L69.06,179.18C65.84,187.5 56.62,191.59 48.46,188.31C40.3,185.02 36.29,175.61 39.51,167.29L50.44,139.02ZM443.52,129.89C451.68,126.61 460.91,130.7 464.12,139.02L475.05,167.29C478.27,175.61 474.26,185.02 466.1,188.31C457.94,191.59 448.72,187.5 445.5,179.18L434.57,150.91C431.36,142.59 435.36,133.18 443.52,129.89ZM148.94,36.76C155.83,31.22 165.82,32.42 171.25,39.45C176.68,46.47 175.51,56.66 168.62,62.2L145.24,81.03C138.36,86.57 128.37,85.37 122.94,78.35C117.5,71.32 118.68,61.13 125.57,55.59L148.94,36.76ZM343.31,39.45C348.74,32.42 358.73,31.22 365.62,36.76L388.99,55.59C395.88,61.13 397.06,71.32 391.62,78.35C386.19,85.37 376.2,86.57 369.31,81.03L345.94,62.2C339.05,56.66 337.88,46.47 343.31,39.45Z\"\n            android:fillColor=\"#ffffff\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/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=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>\n</adaptive-icon>\n"
  },
  {
    "path": "android/app/src/main/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=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>\n</adaptive-icon>\n"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">AB Download Manager</string>\n    <string name=\"app_short_name\">AB DM</string>\n    <string name=\"app_browser\">AB DM Browser</string>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values/theme.xml",
    "content": "<resources>\n    <style name=\"Theme.ABDownloadManager\" parent=\"android:Theme.Material.NoActionBar\">\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n    </style>\n    <style name=\"Theme.ABDownloadManager.Transparent\" parent=\"Theme.ABDownloadManager\">\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowContentOverlay\">@null</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:backgroundDimEnabled\">false</item>\n        <item name=\"android:windowAnimationStyle\">@null</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/xml/provider_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path name=\"external_files\" path=\".\"/>\n    <files-path name=\"updates\" path=\".abdm/system/update/downloads\"/>\n</paths>\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import buildlogic.CiUtils\nimport buildlogic.versioning.getAppVersionString\nimport io.github.z4kn4fein.semver.toVersion\nimport io.github.z4kn4fein.semver.toVersionOrNull\nimport ir.amirab.git_version.core.semanticVersionRegex\nimport org.jetbrains.changelog.Changelog\n\nplugins {\n    ir.amirab.`git-version-plugin`\n    /**\n     * retrieve latest versions of dependencies\n     */\n    com.github.`ben-manes`.versions\n    id(Plugins.changeLog)\n}\n\nval defaultSemVersion = \"1.0.0\"\nval fallBackVersion = \"$defaultSemVersion-untagged\"\n\ngitVersion {\n    on {\n        branch(\".+\") {\n            \"$defaultSemVersion-${it.refInfo.shortenName}-snapshot\"\n        }\n        tag(\"v?${semanticVersionRegex}\") {\n            it.matchResult.groups.get(\"version\")!!.value\n        }\n        commit {\n            \"$defaultSemVersion-sha.${it.refInfo.commitHash.take(5)}\"\n        }\n    }\n}\nversion = (gitVersion.getVersion() ?: fallBackVersion).toVersion()\nlogger.lifecycle(\"version: $version\")\n\ntasks.dependencyUpdates {\n    revision = \"release\"\n    outputFormatter = \"html\"\n    rejectVersionIf {\n        val candidateVersion = candidate.version.toVersionOrNull() ?: return@rejectVersionIf true\n        !candidateVersion.isStable\n    }\n}\n\n// ======= begin of GitHub action stuff\n\nval ciDir = CiUtils.getCiDir(project)\nchangelog {\n    path.set(rootProject.layout.projectDirectory.dir(\"CHANGELOG.md\").asFile.path)\n    version.set(getAppVersionString())\n}\nval createChangeNoteForCi by tasks.registering {\n    inputs.property(\"appVersion\", getAppVersionString())\n    inputs.file(changelog.path)\n    outputs.file(ciDir.changeNotesFile)\n    doLast {\n        val output = ciDir.changeNotesFile.get().asFile\n        val bodyText = with(changelog) {\n            getOrNull(getAppVersionString())?.let { item ->\n                renderItem(item, Changelog.OutputType.MARKDOWN)\n            }\n        }.orEmpty()\n        logger.lifecycle(\"changeNotes written in $output\")\n        output.writeText(bodyText)\n    }\n}\n\nval createReleaseFolderForCi by tasks.registering {\n    val createBinariesForCi = CiUtils.getCreateBinaryFolderForCiTaskName()\n    dependsOn(\"desktop:app:$createBinariesForCi\")\n    val skipAndroidBuild = System.getenv(\"SKIP_ANDROID_BUILD\")\n        ?.toBoolean()\n        ?: false\n    if (!skipAndroidBuild) {\n        dependsOn(\"android:app:$createBinariesForCi\")\n    }\n    val shouldGenerateChangelog = true\n    if (shouldGenerateChangelog) {\n        dependsOn(createChangeNoteForCi)\n    }\n}\n\n// ======= end of GitHub action stuff\n"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "content": "plugins{\n    `kotlin-dsl`\n}\nrepositories {\n    gradlePluginPortal()\n    mavenCentral()\n    google()\n}\ndependencies{\n    implementation(libs.pluginKotlin)\n    implementation(libs.pluginAndroidGradle)\n    implementation(libs.pluginComposeCompiler)\n    implementation(libs.pluginKsp)\n    implementation(libs.pluginSerialization)\n    implementation(libs.pluginComposeMultiplatform)\n    implementation(libs.pluginChangeLog)\n    implementation(libs.pluginBuildConfig)\n    implementation(libs.pluginAboutLibraries)\n    implementation(libs.pluginGradleVersions)\n    implementation(libs.semver)\n    implementation(\"ir.amirab.util:platform:1\")\n    implementation(\"ir.amirab.plugin:git-version-plugin:1\")\n    implementation(\"ir.amirab.plugin:installer-plugin:1\")\n    implementation(\"ir.amirab.plugin:common-android:1\")\n}\n"
  },
  {
    "path": "buildSrc/settings.gradle.kts",
    "content": "dependencyResolutionManagement{\n    versionCatalogs {\n        create(\"libs\"){\n            from(files(\"../gradle/libs.versions.toml\"))\n        }\n    }\n}"
  },
  {
    "path": "buildSrc/src/main/kotlin/Plugins.kt",
    "content": "import ir.amirab.util.platform.Platform\n\nobject MyPlugins {\n    private const val namespace = \"myPlugins\"\n    const val kotlin = \"$namespace.kotlin\"\n    const val kotlinAndroid = \"$namespace.kotlinAndroid\"\n    const val kotlinMultiplatform = \"$namespace.kotlinMultiplatform\"\n    const val composeAndroid = \"$namespace.composeAndroid\"\n    const val composeDesktop = \"$namespace.composeDesktop\"\n    const val composeBase = \"$namespace.composeBase\"\n    const val proguardDesktop = \"$namespace.proguardDesktop\"\n}\nobject MyPlatform{\n    fun getPlatform() = Platform\n}\nobject Plugins {\n    object Kotlin {\n        private const val baseName = \"org.jetbrains.kotlin\"\n        const val serialization = \"$baseName.plugin.serialization\"\n    }\n\n    object Android {\n        private const val baseName = \"com.android\"\n        const val application = \"$baseName.application\"\n        const val library = \"$baseName.library\"\n    }\n\n    const val ksp = \"com.google.devtools.ksp\"\n    const val compose = \"org.jetbrains.compose\"\n    const val composeCompiler = \"org.jetbrains.kotlin.plugin.compose\"\n    const val changeLog = \"org.jetbrains.changelog\"\n    const val buildConfig = \"com.github.gmazzo.buildconfig\"\n    const val aboutLibraries = \"com.mikepenz.aboutlibraries.plugin\"\n    const val aboutLibrariesAndroid = \"com.mikepenz.aboutlibraries.plugin.android\"\n\n    const val multiplatformResources = \"dev.icerock.mobile.multiplatform-resources\"\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/buildlogic/CiDirs.kt",
    "content": "package buildlogic\n\nimport org.gradle.api.file.Directory\nimport org.gradle.api.provider.Provider\n\nclass CiDirs(baseDir: Provider<Directory>) {\n    val releaseDir = baseDir.map { it.dir(\"ci-release\") }\n    val binariesDir = releaseDir.map { it.dir(\"binaries\") }\n    val changeNotesFile = releaseDir.map { it.file(\"release-notes.md\") }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/buildlogic/CiUtils.kt",
    "content": "package buildlogic\n\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.installer.InstallerTargetFormat\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\nimport org.gradle.api.Project\nimport java.io.File\n\nobject CiUtils {\n    fun getTargetFileName(\n        packageName: String,\n        appVersion: Version,\n        target: InstallerTargetFormat?,\n        archName: String?,\n    ): String {\n        val fileExtension = when (target) {\n            // we use archived for app image distribution ( app image is a folder actually so there is no installer so we zip it instead)\n            null -> {\n                when (Platform.getCurrentPlatform()) {\n                    Platform.Desktop.Linux -> \"tar.gz\"\n                    Platform.Desktop.MacOS -> \"tar.gz\"\n                    Platform.Desktop.Windows -> \"zip\"\n                    Platform.Android -> error(\"this can only be used with desktop formats\")\n                }\n            }\n\n            else -> target.fileExtensionWithoutDot()\n        }\n\n        val platformName = when (target) {\n            null -> Platform.getCurrentPlatform()\n            else -> {\n                val packageFileExt = target.fileExtensionWithoutDot()\n                requireNotNull(Platform.fromExecutableFileExtension(packageFileExt)) {\n                    \"can't find platform name with this file extension: ${packageFileExt}\"\n                }\n            }\n        }.name.lowercase()\n        val nameWithoutExtension = listOf(\n            packageName,\n            appVersion.toString(),\n            platformName,\n            archName?: \"universal\",\n        ).joinToString(\"_\")\n        return \"$nameWithoutExtension.${fileExtension}\"\n    }\n\n    fun getFileOfPackagedTarget(\n        baseOutputDir: File,\n        target: InstallerTargetFormat,\n    ): File {\n        val folder = baseOutputDir\n//        val folder = baseOutputDir.resolve(target.outputDirName)\n        val exeFile = kotlin.runCatching {\n            folder.walk().first {\n                it.name.endsWith(target.fileExt)\n            }\n        }.onFailure {\n            println(\"error when finding packaged app for $target in: $baseOutputDir\")\n        }\n        return exeFile.getOrThrow()\n    }\n\n    fun getFileOfDistributedArchivedTarget(\n        baseOutputDir: File,\n    ): File {\n        val folder = baseOutputDir\n        val extension = when (Platform.getCurrentPlatform()) {\n            Platform.Desktop.Linux,\n            Platform.Desktop.MacOS -> \"tar.gz\"\n\n            Platform.Android,\n            Platform.Desktop.Windows -> \"zip\"\n        }\n        val archiveFile = kotlin.runCatching {\n            folder.walk().first {\n                it.name.endsWith(extension)\n            }\n        }.onFailure {\n            println(\"error when finding archive of unpackaged app in: $baseOutputDir\")\n        }\n        return archiveFile.getOrThrow()\n    }\n\n    fun copyAndHashToDestination(\n        src: File,\n        destinationFolder: File,\n        name: String,\n    ) {\n        val destinationExeFile = destinationFolder.resolve(name)\n        src.copyTo(destinationExeFile)\n        val md5File = destinationFolder.resolve(\"$name.md5\")\n        md5File.writeText(HashUtils.md5(src))\n    }\n\n    fun movePackagedAndCreateSignature(\n        appVersion: Version,\n        packageName: String,\n        target: InstallerTargetFormat,\n        basePackagedAppsDir: File,\n        outputDir: File,\n    ) {\n        require(!outputDir.isFile) {\n            \"$outputDir is a file\"\n        }\n        outputDir.mkdirs()\n        require(outputDir.isDirectory) {\n            \"$outputDir is not directory\"\n        }\n\n        val exeFile = getFileOfPackagedTarget(\n            baseOutputDir = basePackagedAppsDir,\n            target = target\n        )\n        val arch = Arch.getCurrentArch().name\n        val newName = getTargetFileName(\n            packageName = packageName,\n            appVersion = appVersion,\n            target = target,\n            archName = arch,\n        )\n        copyAndHashToDestination(\n            src = exeFile,\n            destinationFolder = outputDir,\n            name = newName,\n        )\n    }\n\n    fun getCiDir(project: Project): CiDirs {\n        return CiDirs(project.rootProject.layout.buildDirectory)\n    }\n    fun getCreateBinaryFolderForCiTaskName(): String {\n        return \"createBinariesForCi\"\n    }\n    /*\n        fun moveAndCreateSignature(\n            appVersion: Version,\n            nativeDistributions: JvmApplicationDistributions,\n            target: TargetFormat,\n            path: File,\n            output: File,\n        ) {\n            require(!output.isFile) {\n                \"$output is a file\"\n            }\n            output.mkdirs()\n            require(output.isDirectory) {\n                \"$output is not directory\"\n            }\n            val folder = path.resolve(target.outputDirName)\n            val exeFile = folder.walk().first {\n                it.name.endsWith(target.fileExt)\n            }\n            val appName = requireNotNull(nativeDistributions.packageName){\n                \"package name must not null\"\n            }\n            val fileExtension = exeFile.extension\n            val platformName = requireNotNull(Platform.fromExecutableFileExtension(fileExtension)){\n                \"can't find platform name with this file extension :${fileExtension}\"\n            }.name.lowercase()\n            val newName = \"${appName}_${appVersion}_${platformName}.${fileExtension}\"\n            val destinationExeFile = output.resolve(newName)\n            val md5File = output.resolve(\"$newName.md5\")\n            exeFile.copyTo(destinationExeFile, true)\n            md5File.writeText(HashUtils.md5(exeFile))\n        }\n    */\n}\n\nprivate fun InstallerTargetFormat.fileExtensionWithoutDot() = fileExt.substring(\".\".length)\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/buildlogic/HashUtils.kt",
    "content": "package buildlogic\n\nimport java.io.File\nimport java.security.MessageDigest\n\n// I should move these classes/objects somewhere organized\nobject HashUtils {\n    private fun ByteArray.toHexString(): String = joinToString(\"\", transform = { \"%02x\".format(it) })\n    fun md5(file: File): String {\n        val md = MessageDigest.getInstance(\"MD5\")\n        val digest = md.digest(file.readBytes())\n        return digest.toHexString()\n    }\n}"
  },
  {
    "path": "buildSrc/src/main/kotlin/buildlogic/versioning/VersionUtil.kt",
    "content": "package buildlogic.versioning\n\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.util.platform.Platform\nimport org.gradle.api.Project\nimport org.jetbrains.compose.desktop.application.dsl.TargetFormat\n\nfun Project.getAppVersion(): Version {\n    return rootProject.version as Version\n}\n\nfun Project.getAppVersionString(): String {\n    return rootProject.version.toString()\n}\nfun Version.convertToVersionCode(): Int {\n    require(major in 0..1023) { \"Major must be 0..1023\" }\n    require(minor in 0..1023) { \"Minor must be 0..1023\" }\n    require(patch in 0..511) { \"Patch must be 0..511\" }\n\n    return (major shl 19) or\n            (minor shl 9)  or\n            patch\n}\n\nfun Project.getAppName(): String {\n    return rootProject.name\n}\n\nfun Project.getPrettifiedAppName(): String {\n    return \"AB Download Manager\"\n}\nfun Project.getAppDataDirName(): String {\n    return \".abdm\"\n}\n\nfun Project.getApplicationPackageName(): String {\n    return \"com.abdownloadmanager\"\n}\n\nprivate fun guessTargetFormatBasedOnCurrentOs()= when (Platform.getCurrentPlatform()) {\n    Platform.Desktop.Linux -> TargetFormat.Deb\n    Platform.Desktop.MacOS -> TargetFormat.Dmg\n    Platform.Desktop.Windows -> TargetFormat.Msi\n    Platform.Android -> error(\"we are executing gradle in desktop :D\")\n}\n\nfun Project.getAppVersionStringForPackaging(targetFormat: TargetFormat? = null): String {\n    val v = getAppVersion()\n    val simple = { v.run { \"$major.$minor.$patch\" } }\n    val semantic = { v.toString() }\n    val forRpm = { semantic().replace(\"-\", \"_\") }\n    return when (targetFormat?: guessTargetFormatBasedOnCurrentOs()) {\n        TargetFormat.Rpm -> forRpm()\n        TargetFormat.Deb, TargetFormat.AppImage -> semantic()\n        TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Dmg, TargetFormat.Pkg -> simple()\n    }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/composeAndroid.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    id(\"myPlugins.kotlinAndroid\")\n    id(\"myPlugins.composeBase\")\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/composeBase.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    kotlin(\"plugin.compose\")\n    id(\"org.jetbrains.compose\")\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/composeDesktop.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    id(\"myPlugins.kotlin\")\n    id(\"myPlugins.composeBase\")\n}\n\ndependencies {\n    api(compose.desktop.currentOs){\n        exclude(\"org.jetbrains.compose.material\")\n    }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/kotlin.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    kotlin(\"jvm\")\n}\nrepositories {\n    mavenCentral()\n    google()\n    maven(\"https://jitpack.io\")\n}\n\nfun getOptIns(): Set<String> = setOf(\n    \"androidx.compose.animation.ExperimentalAnimationApi\",\n    \"androidx.compose.foundation.ExperimentalFoundationApi\",\n    \"androidx.compose.ui.ExperimentalComposeUiApi\",\n)\n\nfun getFeatures(): Set<String> = setOf(\n    \"context-parameters\",\n)\n\nkotlin {\n    compilerOptions {\n        val optIns = getOptIns().map { \"-Xopt-in=$it\" }\n        val features = getFeatures().map { \"-X$it\" }\n        freeCompilerArgs.set(optIns + features)\n    }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/kotlinAndroid.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    kotlin(\"android\")\n}\nrepositories {\n    mavenCentral()\n    google()\n    maven(\"https://jitpack.io\")\n}\n\nfun getOptIns(): Set<String> = setOf(\n    \"androidx.compose.animation.ExperimentalAnimationApi\",\n    \"androidx.compose.foundation.ExperimentalFoundationApi\",\n    \"androidx.compose.ui.ExperimentalComposeUiApi\",\n)\n\nfun getFeatures(): Set<String> = setOf(\n    \"context-parameters\",\n)\n\nkotlin {\n    compilerOptions {\n        val optIns = getOptIns().map { \"-Xopt-in=$it\" }\n        val features = getFeatures().map { \"-X$it\" }\n        freeCompilerArgs.set(optIns + features)\n    }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/kotlinMultiplatform.gradle.kts",
    "content": "package myPlugins\n\nplugins {\n    kotlin(\"multiplatform\")\n}\n\nrepositories {\n    mavenCentral()\n    google()\n    maven(\"https://jitpack.io\")\n}\n\nfun getOptIns(): Set<String> = setOf(\n    \"androidx.compose.animation.ExperimentalAnimationApi\",\n    \"androidx.compose.foundation.ExperimentalFoundationApi\",\n    \"androidx.compose.ui.ExperimentalComposeUiApi\",\n)\n\nfun getFeatures(): Set<String> = setOf(\n    \"context-parameters\",\n)\n\nkotlin {\n    compilerOptions {\n        val optIns = getOptIns().map { \"-Xopt-in=$it\" }\n        val features = getFeatures().map { \"-X$it\" }\n        freeCompilerArgs.set(optIns + features)\n    }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/myPlugins/proguardDesktop.gradle.kts",
    "content": "package myPlugins\n\nimport org.jetbrains.compose.desktop.application.tasks.AbstractProguardTask\nimport java.util.zip.ZipFile\n\nplugins {\n    id(\"org.jetbrains.compose\")\n}\n\nfun getProguardFileContent(file: File): List<Pair<String, String>> {\n    val list = ArrayList<Pair<String, String>>()\n    if (file.isFile) {\n        if (file.name.endsWith(\".jar\", true)) {\n            return runCatching {\n                val zipFile = ZipFile(file)\n                list.apply {\n                    addAll(zipFile.use { zFile ->\n                        zFile.entries().toList().filter {\n                            it.name.run {\n                                endsWith(\".pro\")\n//                                        && (startsWith(\"META-INF/proguard\") || !contains(\"/\"))\n                            }\n                        }.map {\n                            val name = it.name.split(\"/\").last()\n                            val content = zFile.getInputStream(it).reader().use { it.readText() }\n                            name to \"\"\"\n# - rules applied from $name                                    \n$content                                \n                            \"\"\".trimIndent()\n                        }\n                    })\n                }\n            }.getOrThrow()\n        }\n    }\n    return list\n}\nval compileClasspathProvider = configurations.named(\"compileClasspath\")\nval getProguardConfigurations by tasks.registering {\n    dependsOn(compileClasspathProvider)\n    val folder = layout.buildDirectory.map {\n        it.dir(\"resolvedProguards\")\n    }\n    inputs.files(compileClasspathProvider)\n    outputs.dir(folder)\n    doLast {\n        val outputFolder = folder.get().asFile\n        outputFolder.deleteRecursively()\n        compileClasspathProvider.get().files.forEach { file ->\n            val outPutOfPackage = outputFolder.resolve(\"${file.name}\")\n            for ((name, content) in getProguardFileContent(file)) {\n                outPutOfPackage\n                    .resolve(name).also {\n                        it.parentFile.mkdirs()\n                    }\n                    .writeText(content)\n            }\n        }\n    }\n}\ntasks.withType<AbstractProguardTask> {\n    dependsOn(getProguardConfigurations)\n}"
  },
  {
    "path": "compositeBuilds/plugins/common-android/build.gradle.kts",
    "content": "plugins {\n    `kotlin-dsl`\n}\nrepositories {\n    mavenCentral()\n    google()\n}\nversion = 1\ngroup = \"ir.amirab.plugin\"\ndependencies {\n    implementation(libs.pluginAndroidGradle)\n    implementation(libs.handlebarsJava)\n    implementation(libs.okio.okio)\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/common-android/src/main/kotlin/ir/amirab/plugin/common_android/task/EnableFileTypesGeneratorForManifest.kt",
    "content": "package ir.amirab.plugin.common_android.task\n\nimport org.gradle.api.Action\nimport org.gradle.api.Project\nimport org.gradle.api.file.RegularFile\nimport org.gradle.api.plugins.ExtensionAware\nimport org.gradle.internal.extensions.stdlib.capitalized\nimport org.gradle.kotlin.dsl.register\n\nprivate fun Project.androidComponents(configure: Action<com.android.build.api.variant.ApplicationAndroidComponentsExtension>): Unit =\n    (this as ExtensionAware).extensions.configure(\"androidComponents\", configure)\n\n\nfun Project.androidEnableFileTypesGeneratorForManifest(\n    targetActivityClass: String,\n    fileTypesFile: RegularFile,\n) {\n    androidComponents {\n        onVariants{variant ->\n            val taskName = \"generate${variant.name.capitalized()}FileTypesManifest\"\n            val task = tasks.register<GenerateFileTypesManifest>(taskName) {\n                extensionsFile.set(fileTypesFile)\n                targetActivity.set(targetActivityClass)\n            }\n            variant.sources.manifests.addGeneratedManifestFile(task, GenerateFileTypesManifest::outputFile)\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/common-android/src/main/kotlin/ir/amirab/plugin/common_android/task/FileTypesIntentFilterGenerator.kt",
    "content": "package ir.amirab.plugin.common_android.task\n\nimport com.github.jknack.handlebars.Context\nimport com.github.jknack.handlebars.Handlebars\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.file.RegularFileProperty\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputFile\nimport org.gradle.api.tasks.OutputFile\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.kotlin.dsl.property\n\ninternal abstract class GenerateFileTypesManifest : DefaultTask() {\n    @get:InputFile\n    val extensionsFile: RegularFileProperty = project.objects.fileProperty()\n\n    @get:Input\n    val targetActivity = project.objects.property<String>()\n\n    @get:OutputFile\n    val outputFile: RegularFileProperty = project.objects.fileProperty()\n\n    @TaskAction\n    fun generate() {\n        val output = outputFile.get().asFile\n        output.parentFile.mkdirs()\n        val extensionList = extensionsFile.asFile.get()\n            .bufferedReader()\n            .lineSequence()\n            .map { it.trim() }\n            .filterNot { it.isEmpty() }\n            .filterNot { it.startsWith(\"#\") }\n            .toList()\n        val templateFile = \"ir/amirab/plugin/common_android/AndroidManifest.xml.hbs\".toPath()\n        val templateContent = FileSystem.RESOURCES.read(templateFile) {\n            readUtf8()\n        }\n        val patterns = generatePatterns(extensionList)\n        val handlebars = Handlebars()\n        val manifestContent = handlebars.compileInline(templateContent)\n            .apply(\n                Context.newContext(\n                    mapOf(\n                        \"activityName\" to targetActivity.get(),\n                        \"patterns\" to patterns,\n                    )\n                )\n            )\n        output.writeText(manifestContent)\n    }\n\n    private fun generatePatterns(extensions: List<String>): List<String> {\n        return extensions.flatMap { generatePatternsForExtension(it) }\n    }\n    private fun generatePatternsForExtension(\n        extension: String,\n        repeat: Int = 4\n    ): List<String> {\n        val first = \"\"\"\\\\.$extension\"\"\"\n        return buildList{\n            add(\".*$first\")\n            repeat(repeat){\n                add(\".*${\"\"\"\\\\..*\"\"\".repeat(it)}$first.*\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/common-android/src/main/kotlin/ir/amirab/plugin/common_android/task/SignApkTask.kt",
    "content": "package ir.amirab.plugin.common_android.task\n\nimport okio.Path.Companion.toPath\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.GradleException\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputDirectory\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.internal.os.OperatingSystem\nimport org.gradle.process.ExecOperations\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.util.Base64\nimport javax.inject.Inject\n\nsealed interface KeystoreContent {\n    companion object {\n        fun fromUri(\n            uriString: String,\n        ): KeystoreContent {\n            val splitIndex = uriString.indexOf(':')\n            if (splitIndex == -1) {\n                throw GradleException(\"Invalid KeystoreContent it should be <type>:<data>\")\n            }\n            val type = uriString.substring(0, splitIndex)\n            val data = uriString.substring(splitIndex + 1)\n            return when (type) {\n                \"file\" -> {\n                    FromFile(File(data))\n                }\n\n                \"base64\" -> {\n                    FromBase64(data)\n                }\n\n                else -> error(\"please provide file or Base64 ( base64:abcdefg or file:/path/to/file )\")\n            }\n        }\n    }\n\n    fun getContent(): ByteArray\n    data class FromFile(private val file: File) : KeystoreContent {\n        override fun getContent(): ByteArray {\n            return file.readBytes()\n        }\n    }\n\n    data class FromBase64(private val base64Content: String) : KeystoreContent {\n        override fun getContent(): ByteArray {\n            return Base64.getDecoder().decode(base64Content)\n        }\n    }\n\n}\n\nabstract class SignApkTask : DefaultTask() {\n    @get:Inject\n    internal abstract val execOps: ExecOperations\n\n    @get:InputDirectory\n    abstract val inputDir: DirectoryProperty\n\n    @get:OutputDirectory\n    abstract val outputDIr: DirectoryProperty\n\n    @get:Input\n    abstract val keystoreUri: Property<String>\n\n    @get:Input\n    abstract val keystorePassword: Property<String>\n\n    @get:Input\n    abstract val keyAlias: Property<String>\n\n    @get:Input\n    abstract val keyPassword: Property<String>\n\n    @get:Input\n    abstract val platformToolsVersion: Property<String>\n\n    private fun getApkSignerFile(): String {\n        val androidHome = System.getenv(\"ANDROID_HOME\")?.toPath()\n            ?: throw GradleException(\"ANDROID HOME environment variable is not set.\")\n        val dir = androidHome / \"build-tools\" / platformToolsVersion.get()\n        val name = if (OperatingSystem.current().isWindows) {\n            \"apksigner.bat\"\n        } else {\n            \"apksigner\"\n        }\n        return (dir / name).toString()\n    }\n\n    @TaskAction\n    fun sign() {\n        val inputDir = inputDir.get().asFile\n        if (!inputDir.exists()) {\n            throw IllegalArgumentException(\"Input APK does not exist: ${inputDir.absolutePath}\")\n        }\n        val outputDir = outputDIr.get().asFile\n        val keystorePassword: String = keystorePassword.get()\n        val keyPassword: String = keyPassword.get()\n        val keyAlias: String = keyAlias.get()\n        val tempKeyStoreFile = getKeyStoreFile()\n        try {\n            inputDir.listFiles().filter {\n                it.name.endsWith(\".apk\")\n            }.forEach {\n                signSingleApk(\n                    inputApk = it,\n                    outputApk = outputDir.resolve(it.name),\n                    keystoreFile = tempKeyStoreFile,\n                    keystorePassword = keystorePassword,\n                    keyAlias = keyAlias,\n                    keyPassword = keyPassword\n                )\n            }\n        } finally {\n            tempKeyStoreFile.delete()\n        }\n    }\n\n    private fun getKeyStoreFile(): File {\n        val keystoreContent = KeystoreContent.fromUri(\n            keystoreUri.get(),\n        )\n        // Decode base64 keystore to temp file\n        val tempKeystore = File.createTempFile(\"keystore\", \".jks\")\n        FileOutputStream(tempKeystore).use { fos ->\n            fos.write(keystoreContent.getContent())\n        }\n        return tempKeystore\n    }\n\n    fun signSingleApk(\n        inputApk: File,\n        outputApk: File,\n        keystoreFile: File,\n        keystorePassword: String,\n        keyAlias: String,\n        keyPassword: String,\n    ) {\n        logger.lifecycle(\"Signing APK: $inputApk\")\n        logger.lifecycle(\"Output APK: $outputApk\")\n\n        execOps.exec {\n            commandLine(\n                getApkSignerFile(),\n                \"sign\",\n                \"--ks\", keystoreFile,\n                \"--ks-pass\", \"pass:$keystorePassword\",\n                \"--key-pass\", \"pass:$keyPassword\",\n                \"--ks-key-alias\", keyAlias,\n                \"--out\", outputApk.absolutePath,\n                inputApk.absolutePath,\n            )\n        }\n\n        logger.lifecycle(\"Signed APK generated at: $outputApk\")\n\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/common-android/src/main/resources/ir/amirab/plugin/common_android/AndroidManifest.xml.hbs",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application>\n        <activity android:name=\"{{activityName}}\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\"/>\n                <category android:name=\"android.intent.category.DEFAULT\"/>\n                <data android:mimeType=\"*/*\"/>\n            </intent-filter>\n            <intent-filter>\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                <data android:scheme=\"http\"/>\n                <data android:scheme=\"https\"/>\n                <data android:mimeType=\"*/*\"/>\n            </intent-filter>\n            <intent-filter>\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                <data android:scheme=\"http\"/>\n                <data android:scheme=\"https\"/>\n                <data android:host=\"*\"/>\n                {{~#each patterns}}\n                <data android:pathPattern=\"{{this}}\" />\n                {{~/each}}\n            </intent-filter>\n        </activity>\n    </application>\n</manifest>\n"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/build.gradle.kts",
    "content": "plugins {\n    `kotlin-dsl`\n}\nrepositories {\n    mavenCentral()\n}\nversion = 1\ngroup = \"ir.amirab.plugin\"\ndependencies {\n    implementation(libs.semver)\n    implementation(libs.jgit)\n}\ngradlePlugin {\n    plugins {\n        create(\"git-version-plugin\") {\n            id = \"ir.amirab.git-version-plugin\"\n            implementationClass = \"ir.amirab.git_version.GitVersionPlugin\"\n        }\n    }\n}"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/GitVersionPlugin.kt",
    "content": "package ir.amirab.git_version\n\nimport ir.amirab.git_version.core.GitVersionExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.slf4j.Logger\n\nclass GitVersionPlugin: Plugin<Project> {\n    override fun apply(target: Project) {\n        val gitVersionExtension = GitVersionExtension()\n        target.extensions.add(\"gitVersion\", gitVersionExtension)\n        gitVersionExtension.currentWorkingDirectory = target.rootDir\n        gitVersionExtension.setLogger(target.logger)\n    }\n}"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/CiReferenceProvider.kt",
    "content": "package ir.amirab.git_version.core\n\nimport ir.amirab.git_version.core.CiReferenceProvider.Companion.getEnv\nimport ir.amirab.git_version.core.CiReferenceProvider.Companion.safeEnv\n\n\ninterface CiReferenceProvider {\n    fun isAvailable(): Boolean\n    fun getSha(): String?\n    fun getRef(): String?\n    fun getReference(): GitReference? {\n        return refOrNull(getRef(), getSha())\n    }\n\n    companion object {\n        var envProvider: (String) -> String? = { System.getenv(it) }\n        fun getEnv(string: String): String? {\n            return envProvider(string)\n        }\n        fun safeEnv(string: String): String? {\n            return getEnv(string)?.takeIf {\n                it.isNotBlank()\n            }\n        }\n\n\n        private val registeredItems = linkedSetOf<CiReferenceProvider>()\n        private fun builtIns(): Set<CiReferenceProvider> {\n            return setOf(\n                GithubCiReferenceProvider,\n                GitlabCiReferenceProvider,\n                CircleCiReferenceProvider,\n                JenkinsCiReferenceProvider,\n            )\n        }\n\n        init {\n            builtIns().forEach { add(it) }\n        }\n\n        fun add(ciReferenceProvider: CiReferenceProvider) {\n            registeredItems.add(ciReferenceProvider)\n        }\n\n        fun isInCi() = getAll().any { it.isAvailable() }\n        fun getAll(): Set<CiReferenceProvider> {\n            return registeredItems\n        }\n    }\n}\n\nprivate fun refOrNull(ref: String?, sha: String? = null): GitReference? {\n    return ref?.let {\n        GitReference.of(ref, sha)\n    }\n}\n\nobject GithubCiReferenceProvider : CiReferenceProvider {\n    override fun isAvailable(): Boolean {\n        return getEnv(\"GITHUB_CI\")?.lowercase() == \"true\"\n    }\n\n    override fun getRef(): String? {\n        return safeEnv(\"GITHUB_REF\")\n    }\n\n    override fun getSha(): String? {\n        return safeEnv(\"GITHUB_SHA\")\n    }\n}\n\nobject GitlabCiReferenceProvider : CiReferenceProvider {\n    override fun isAvailable(): Boolean {\n        return getEnv(\"GITLAB_CI\")?.lowercase() == \"true\"\n    }\n\n    override fun getRef(): String? {\n        return safeEnv(\"CI_COMMIT_BRANCH\")\n                ?: safeEnv(\"CI_COMMIT_TAG\")\n                ?: safeEnv(\"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME\")\n    }\n\n    override fun getSha(): String? {\n        return null\n    }\n}\n\nobject CircleCiReferenceProvider : CiReferenceProvider {\n    override fun isAvailable(): Boolean {\n        return getEnv(\"CIRCLECI\")?.lowercase() == \"true\"\n    }\n\n    override fun getRef(): String? {\n        return safeEnv(\"CIRCLE_BRANCH\")\n                ?: safeEnv(\"CIRCLE_TAG\")\n    }\n\n    override fun getSha(): String? {\n        return null\n    }\n}\n\nobject JenkinsCiReferenceProvider : CiReferenceProvider {\n    override fun isAvailable(): Boolean {\n        return safeEnv(\"JENKINS_HOME\") != null\n    }\n\n    override fun getRef(): String? {\n        return safeEnv(\"BRANCH_NAME\")\n                ?: safeEnv(\"TAG_NAME\")\n    }\n\n    override fun getSha(): String? {\n        return null\n    }\n}\n\n"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/GitStatus.kt",
    "content": "package ir.amirab.git_version.core\n\nimport org.eclipse.jgit.lib.Constants.*\nimport org.eclipse.jgit.lib.ObjectId\nimport org.eclipse.jgit.lib.Ref\nimport org.eclipse.jgit.lib.Repository\nimport org.eclipse.jgit.lib.Repository.shortenRefName\nimport org.eclipse.jgit.revwalk.RevTag\nimport org.eclipse.jgit.revwalk.RevWalk\n\n\nprivate fun Repository.fullBranchOrNull(): String? {\n    return fullBranch.takeIf { !ObjectId.isId(it) }\n}\n\nprivate fun Repository.tagsPointAt(pointAtThisObject: ObjectId): List<Ref> {\n    return refDatabase.getRefsByPrefix(R_TAGS).filter {\n        it.toString()\n        refDatabase.peel(it).run {\n            peeledObjectId ?: objectId\n        } == pointAtThisObject\n    }\n}\n\nclass GitStatus(\n    val repository: Repository,\n) {\n    val head = repository.resolve(HEAD)\n\n    val branch: GitReference.BranchInfo? by lazy {\n        repository.fullBranchOrNull()?.let {\n            GitReference.BranchInfo(it, head.name)\n        }\n    }\n\n    val tags by lazy {\n        RevWalk(repository).use { revWalk ->\n            repository.tagsPointAt(head).map {\n                val f = revWalk.parseAny(it.objectId)\n                GitReference.TagInfo(\n                    fullName = it.name,\n                    commitHash = head.name,\n                    createdAt = (f as? RevTag)?.taggerIdent?.`when`?.time,\n                )\n            }\n        }\n    }\n\n    fun isDetached() = branch == null\n}\n\nsealed interface GitReference {\n    val commitHash: String?\n\n    sealed class SymbolicReference : GitReference {\n        abstract val fullName: String\n        val shortenName by lazy { shortenRefName(fullName) }\n    }\n\n    data class TagInfo(\n        override val fullName: String,\n        override val commitHash: String? = null,\n        val createdAt: Long? = null,\n    ) : SymbolicReference()\n\n    data class BranchInfo(\n        override val fullName: String,\n        override val commitHash: String? = null,\n    ) : SymbolicReference()\n\n    data class ShaReference(\n        override val commitHash: String,\n    ) : GitReference\n\n    companion object {\n        fun of(\n            ref: String,\n            sha: String?,\n        ): GitReference {\n            return when {\n                ref.startsWith(R_TAGS) -> TagInfo(\n                    fullName = ref,\n                    commitHash = sha,\n                )\n\n                ref.startsWith(R_REMOTES)\n                        || ref.startsWith(R_HEADS)\n                -> BranchInfo(ref, sha)\n\n                else -> error(\"'$ref' is not a valid ref name\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/SemanticVersionSelector.kt",
    "content": "package ir.amirab.git_version.core\n\nimport io.github.z4kn4fein.semver.Version\nimport io.github.z4kn4fein.semver.toVersionOrNull\nimport org.intellij.lang.annotations.Language\n\n\nclass SelectBestSemanticVersion : TagSelector {\n    private val regex by lazy {\n        \"$semanticVersionRegex$$\".toRegex()\n    }\n\n    private fun GitReference.TagInfo.toVersionOrNull(): Version? {\n        return regex.find(shortenName)?.groups?.get(\"version\")?.value?.toVersionOrNull()\n    }\n\n    override fun select(\n        tags: List<GitReference.TagInfo>\n    ): GitReference.TagInfo? {\n        return tags\n            .map { it to it.toVersionOrNull() }\n            .sortedByDescending { it.second }\n//            .also {\n//                println(it.map { it.second })\n//            }\n            .firstOrNull()?.first\n    }\n}\n\n/**\n * Note: add ending manually\n */\n@Language(\"RegExp\")\nval semanticVersionRegex = \"\"\"(?<version>(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(?:-(?<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)\"\"\"\n"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/TagSelector.kt",
    "content": "package ir.amirab.git_version.core\n\n\nfun interface TagSelector {\n    fun select(tags: List<GitReference.TagInfo>): GitReference.TagInfo?\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/Utils.kt",
    "content": "package ir.amirab.git_version.core\n\nfun String.toSlug() = replace(\"/\", \"-\")"
  },
  {
    "path": "compositeBuilds/plugins/git-version-plugin/src/main/kotlin/ir/amirab/git_version/core/extension.kt",
    "content": "package ir.amirab.git_version.core\n\nimport org.eclipse.jgit.lib.RepositoryBuilder\nimport org.intellij.lang.annotations.Language\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport org.slf4j.helpers.NOPLogger\nimport java.io.File\nimport kotlin.math.log\n\nopen class MatchedRef<T : GitReference>(\n    val refInfo: T\n)\n\nclass MatchedRefWithResult<T : GitReference>(\n    refInfo: T,\n    val matchResult: MatchResult,\n) : MatchedRef<T>(refInfo)\n\nclass ResolvedScope() {\n    private val tagFilter = linkedMapOf<String, (MatchedRefWithResult<GitReference.TagInfo>) -> String?>()\n    private val branchFilter = linkedMapOf<String, (MatchedRefWithResult<GitReference.BranchInfo>) -> String?>()\n    private var _commit: (MatchedRef<GitReference.ShaReference>) -> String? = { null }\n\n    //should match entire tag name\n    fun branch(\n        @Language(\"RegExp\") regex: String, function: (MatchedRefWithResult<GitReference.BranchInfo>) -> String?\n    ) {\n        branchFilter[regex] = function\n    }\n\n    //should match entire tag name\n    fun tag(\n        @Language(\"RegExp\") regex: String,\n        function: (MatchedRefWithResult<GitReference.TagInfo>) -> String?\n    ) {\n        tagFilter[regex] = function\n    }\n\n    fun commit(function: (MatchedRef<GitReference.ShaReference>) -> String?) {\n        _commit = function\n    }\n\n    private fun matchTag(it: GitReference.TagInfo): String? {\n        for ((regex, matchedRef) in tagFilter) {\n            val matched = regex.toRegex().matchEntire(it.shortenName)\n            if (matched != null) {\n                matchedRef(MatchedRefWithResult(it, matched))?.let { it ->\n                    return it\n                } ?: continue\n            }\n        }\n        return null\n    }\n\n    private fun matchBranch(it: GitReference.BranchInfo): String? {\n        for ((regex, matchedRef) in branchFilter) {\n            val matched = regex.toRegex().matchEntire(it.shortenName)\n            if (matched != null) {\n                matchedRef(MatchedRefWithResult(it, matched))?.let { it ->\n                    return it\n                } ?: continue\n            }\n        }\n        return null\n    }\n\n    private fun matchCommit(it: GitReference.ShaReference): String? {\n        return _commit(MatchedRef(it))\n    }\n\n    fun match(gitReference: GitReference): String? {\n        return when (gitReference) {\n            is GitReference.BranchInfo -> matchBranch(gitReference)\n            is GitReference.TagInfo -> matchTag(gitReference)\n            is GitReference.ShaReference -> matchCommit(gitReference)\n        }\n    }\n}\n\nclass GitVersionExtension {\n    var preferTag: Boolean = false\n    var preferCi: Boolean = true\n    var checkCi: Boolean = true\n    var tagSelector: TagSelector = SelectBestSemanticVersion()\n    val transform: (String) -> String = { it.toSlug() }\n\n    var currentWorkingDirectory: File = File(\".\")\n    private var _logger:Logger?=null\n    fun setLogger(logger: Logger){\n        _logger= logger\n    }\n    fun getLogger():Logger{\n        if (_logger==null){\n            _logger=object :NOPLogger(){}\n        }\n        return _logger!!\n    }\n\n    private val repository by lazy {\n        RepositoryBuilder()\n            .findGitDir(currentWorkingDirectory)\n            .build()\n    }\n    private val refHandlers = ResolvedScope()\n    fun on(block: ResolvedScope.() -> Unit) {\n        refHandlers.apply(block)\n    }\n\n\n    operator fun invoke(block: GitVersionExtension.() -> Unit) = apply { block() }\n\n    private fun tryGetVersionFromCi(): GitReference? {\n        return CiReferenceProvider.getAll().firstOrNull {\n            it.isAvailable()\n        }?.getReference()\n    }\n\n    private fun tryGetVersionFromGit(\n        status: GitStatus = GitStatus(repository)\n    ): GitReference? {\n        val getTag = {\n            status.tags\n                .also { getLogger().info(\"${it.count()} tags found. ${it.map { it.shortenName }.joinToString(\" , \")}\") }\n                .let { tagSelector.select(it) }\n        }\n        val getBranch = {\n            status.branch?.also {\n                getLogger().info(\"branch ${it.shortenName} detected\")\n            }\n        }\n\n        return if (preferTag) {\n            getTag() ?: getBranch()\n        } else {\n            getBranch() ?: getTag()\n        }\n    }\n\n    fun getBestReference(): GitReference? {\n        val ci = { if (checkCi) tryGetVersionFromCi() else null }\n        val gitStatus by lazy {\n            GitStatus(repository)\n        }\n        val git = { tryGetVersionFromGit(gitStatus) }\n        return when {\n            preferCi -> ci() ?: git()\n            else -> git() ?: ci()\n        } ?: GitReference.ShaReference(gitStatus.head.name)\n    }\n\n    fun getVersion() = getBestReference()\n        ?.let(refHandlers::match)\n        ?.let(transform)\n}"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/build.gradle.kts",
    "content": "plugins {\n    `kotlin-dsl`\n}\nrepositories {\n    mavenCentral()\n}\nversion = 1\ngroup = \"ir.amirab.plugin\"\ndependencies {\n    implementation(\"ir.amirab.util:platform:1\")\n    implementation(libs.handlebarsJava)\n}\ngradlePlugin {\n    plugins {\n        create(\"installer-plugin\") {\n            id = \"ir.amirab.installer-plugin\"\n            implementationClass = \"ir.amirab.installer.InstallerPlugin\"\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerPlugin.kt",
    "content": "package ir.amirab.installer\n\nimport ir.amirab.installer.extensiion.InstallerPluginExtension\nimport ir.amirab.installer.tasks.macos.CreateDmgTask\nimport ir.amirab.installer.tasks.windows.NsisTask\nimport ir.amirab.installer.utils.Constants\nimport ir.amirab.util.platform.Platform\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.create\nimport org.gradle.kotlin.dsl.register\n\nclass InstallerPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        val extension = target.extensions.create(\"installerPlugin\", InstallerPluginExtension::class)\n        target.afterEvaluate {\n            registerTasks(target, extension)\n        }\n    }\n\n    private fun registerTasks(\n        project: Project,\n        extension: InstallerPluginExtension\n    ) {\n        val windowConfig = extension.windowsConfig\n        val macosConfig = extension.macosConfig\n        val createInstallerTaskName = Constants.CREATE_INSTALLER_TASK_NAME\n        val createInstallerNsisTaskName = \"${createInstallerTaskName}Nsis\"\n        val createInstallerDmgTaskName = \"${createInstallerTaskName}Dmg\"\n        if (windowConfig != null) {\n            project.tasks\n                .register<NsisTask>(createInstallerNsisTaskName)\n                .configure {\n                    dependsOn(extension.taskDependencies.toTypedArray())\n                    this.nsisTemplate.set(requireNotNull(windowConfig.nsisTemplate) { \"Nsis Template not provided\" })\n                    this.commonParams.set(windowConfig)\n                    this.extraParams.set(windowConfig.extraParams)\n                    this.destFolder.set(extension.outputFolder.get().asFile)\n                    this.outputFileName.set(requireNotNull(windowConfig.outputFileName) { \" outputFileName not provided \" })\n                    this.sourceFolder.set(requireNotNull(windowConfig.inputDir) { \"inputDir not provided\" })\n                }\n        }\n        if (macosConfig != null) {\n            project.tasks\n                .register<CreateDmgTask>(createInstallerDmgTaskName)\n                .configure {\n                    dependsOn(extension.taskDependencies.toTypedArray())\n                    this.appName.set(requireNotNull(macosConfig.appName) { \"appName not provided\" })\n                    this.appFileName.set(requireNotNull(macosConfig.appFileName) { \"iconFile not provided\" })\n                    this.backgroundImage.set(requireNotNull(macosConfig.backgroundImage) { \"backgroundImage not provided\" })\n                    this.outputFileName.set(requireNotNull(macosConfig.outputFileName) { \"outputFileName not provided\" })\n                    this.inputDir.set(requireNotNull(macosConfig.inputDir) { \"inputDir not provided\" })\n                    this.licenseFile.set(requireNotNull(macosConfig.licenseFile) { \"licenseFile not provided\" })\n                    this.destFolder.set(requireNotNull(extension.outputFolder.get().asFile) { \"outputFolder not provided\" })\n                    this.volumeIcon.set(requireNotNull(macosConfig.volumeIcon) { \"volumeIcon not provided\" })\n                    this.iconSize.set(macosConfig.iconSize)\n                    this.windowHeight.set(macosConfig.windowHeight)\n                    this.windowWidth.set(macosConfig.windowWidth)\n                    this.folderOffsetX.set(macosConfig.folderOffsetX)\n                    this.appOffsetX.set(macosConfig.appOffsetX)\n                    this.iconsY.set(macosConfig.iconsY)\n                    this.windowX.set(macosConfig.windowX)\n                    this.windowY.set(macosConfig.windowY)\n                }\n        }\n        project.tasks.register(createInstallerTaskName) {\n            // when we want to create installer we need to prepare its input first!\n            when (val platform = Platform.getCurrentPlatform()) {\n                Platform.Desktop.Linux -> {\n                    // nothing yet\n                }\n\n                Platform.Desktop.MacOS -> {\n                    if (macosConfig != null) {\n                        dependsOn(createInstallerDmgTaskName)\n                    }\n                }\n\n                Platform.Desktop.Windows -> {\n                    if (windowConfig != null) {\n                        dependsOn(createInstallerNsisTaskName)\n                    }\n                }\n\n                else -> error(\"unsupported platform: $platform\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/InstallerTargetFormat.kt",
    "content": "package ir.amirab.installer\n\nimport ir.amirab.util.platform.Platform\n\n\nenum class InstallerTargetFormat(\n    val id: String,\n    val targetOS: Platform,\n) {\n    Deb(\"deb\", Platform.Desktop.Linux),\n    Rpm(\"rpm\", Platform.Desktop.Linux),\n    Dmg(\"dmg\", Platform.Desktop.MacOS),\n    Pkg(\"pkg\", Platform.Desktop.MacOS),\n    Exe(\"exe\", Platform.Desktop.Windows),\n    Msi(\"msi\", Platform.Desktop.Windows),\n    Apk(\"apk\", Platform.Android);\n\n    val isCompatibleWithCurrentOS: Boolean by lazy { isCompatibleWith(Platform.getCurrentPlatform()) }\n\n    fun isCompatibleWith(os: Platform): Boolean = os == targetOS\n\n    val outputDirName: String\n        get() = id\n\n    val fileExt: String\n        get() {\n            return \".$id\"\n        }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/extensiion/InstallerPluginExtension.kt",
    "content": "package ir.amirab.installer.extensiion\n\nimport ir.amirab.installer.InstallerTargetFormat\nimport ir.amirab.installer.utils.Constants\nimport ir.amirab.util.platform.Platform\nimport org.gradle.api.Project\nimport org.gradle.api.Task\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.tasks.TaskProvider\nimport java.io.File\nimport java.io.Serializable\nimport javax.inject.Inject\n\nabstract class InstallerPluginExtension {\n    @get:Inject\n    internal abstract val project: Project\n\n    abstract val outputFolder: DirectoryProperty\n\n    internal val taskDependencies = mutableListOf<Any>()\n\n    fun dependsOn(vararg tasks: Any) {\n        taskDependencies.addAll(tasks)\n    }\n\n    internal var windowsConfig: WindowsConfig? = null\n        private set\n\n    internal var macosConfig: MacosConfig? = null\n        private set\n\n    fun windows(\n        config: WindowsConfig.() -> Unit\n    ) {\n        if (Platform.getCurrentPlatform() != Platform.Desktop.Windows) return\n        val windowsConfig = if (this.windowsConfig == null) {\n            WindowsConfig().also {\n                this.windowsConfig = it\n            }\n        } else {\n            this.windowsConfig!!\n        }\n        windowsConfig.config()\n    }\n\n    fun macos(\n        config: MacosConfig.() -> Unit\n    ) {\n        if (Platform.getCurrentPlatform() != Platform.Desktop.MacOS) return\n        val macosConfig = if (this.macosConfig == null) {\n            MacosConfig().also {\n                this.macosConfig = it\n            }\n        } else {\n            this.macosConfig!!\n        }\n        macosConfig.config()\n    }\n\n    val createInstallerTask: TaskProvider<Task> by lazy {\n        project.tasks.named(Constants.CREATE_INSTALLER_TASK_NAME)\n    }\n\n    fun isThisPlatformSupported() = when (Platform.getCurrentPlatform()) {\n        Platform.Desktop.Windows -> windowsConfig != null\n        Platform.Desktop.MacOS -> macosConfig != null\n        else -> false\n    }\n\n    fun getCreatedInstallerTargetFormats(): List<InstallerTargetFormat> {\n        return buildList {\n            when (Platform.getCurrentPlatform()) {\n                Platform.Desktop.Windows -> {\n                    if (windowsConfig != null) {\n                        add(InstallerTargetFormat.Exe)\n                    }\n                }\n\n                Platform.Desktop.MacOS -> {\n                    if (macosConfig != null) {\n                        add(InstallerTargetFormat.Dmg)\n                    }\n                }\n\n                else -> {}\n            }\n        }\n    }\n}\n\ndata class WindowsConfig(\n    var appName: String? = null,\n    var appDisplayName: String? = null,\n    var appVersion: String? = null,\n    var appDisplayVersion: String? = null,\n    var appDataDirName: String? = null,\n    var iconFile: File? = null,\n    var licenceFile: File? = null,\n\n    var outputFileName: String? = null,\n\n    var inputDir: File? = null,\n\n    var nsisTemplate: File? = null,\n\n    var extraParams: Map<String, Any> = emptyMap()\n) : Serializable\n\n\ndata class MacosConfig(\n    var appName: String? = null,\n    var appFileName: String? = null,\n    var outputFileName: String? = null,\n    var inputDir: File? = null,\n    /**\n     * Displays an image larger than the window size with proper scaling.\n     *\n     * **Important:** Ensure the image’s aspect ratio is preserved exactly.\n     * Standard scaling methods can cause the background to render larger than expected\n     * when the window is resized, breaking the intended alignment.\n     *\n     * **Recommended approach:** Use the original image as the base layer when creating a new one.\n     * This helps maintain correct scaling and positioning across different window sizes.\n     */\n    var backgroundImage: File? = null,\n    var volumeIcon: File? = null,\n    var iconSize: Int = 100,\n    var licenseFile: File? = null,\n    var windowWidth: Int = 600,\n    var windowHeight: Int = 400,\n    var iconsY: Int = 150,\n    var appOffsetX: Int = 100,\n    var folderOffsetX: Int = 450,\n    var windowX: Int = 150,\n    var windowY: Int = 200,\n) : Serializable"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/tasks/macos/CreateDmgTask.kt",
    "content": "package ir.amirab.installer.tasks.macos\n\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.GradleException\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputDirectory\nimport org.gradle.api.tasks.InputFile\nimport org.gradle.api.tasks.Internal\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.process.ExecOperations\nimport java.io.File\nimport javax.inject.Inject\n\nabstract class CreateDmgTask : DefaultTask() {\n    @get:Inject\n    abstract val execOps: ExecOperations\n\n    @get:InputDirectory\n    abstract val inputDir: DirectoryProperty\n\n    @get:OutputDirectory\n    abstract val destFolder: DirectoryProperty\n\n    @get:Input\n    abstract val appName: Property<String>\n\n    @get:Input\n    abstract val iconSize: Property<Int>\n\n    @get:Input\n    abstract val windowWidth: Property<Int>\n\n    @get:Input\n    abstract val windowHeight: Property<Int>\n\n    @get:Input\n    abstract val iconsY: Property<Int>\n\n    @get:Input\n    abstract val appOffsetX: Property<Int>\n\n    @get:Input\n    abstract val folderOffsetX: Property<Int>\n\n    @get:Input\n    abstract val windowX: Property<Int>\n\n    @get:Input\n    abstract val windowY: Property<Int>\n\n    @get:Input\n    abstract val appFileName: Property<String>\n\n    @get:InputFile\n    abstract val backgroundImage: Property<File>\n\n    @get:InputFile\n    abstract val volumeIcon: Property<File>\n\n    @get:InputFile\n    abstract val licenseFile: Property<File>\n\n    @get:Input\n    abstract val outputFileName: Property<String>\n\n    @get:Internal\n    abstract val dmgExecutable: Property<File>\n\n    init {\n        dmgExecutable.convention(\n            project.provider {\n                val process = ProcessBuilder(\"which\", \"create-dmg\")\n                    .redirectErrorStream(true)\n                    .start()\n                val path = process.inputStream.bufferedReader().readText().trim()\n                if (path.isBlank()) {\n                    throw GradleException(\"create-dmg not found in PATH. Please install it or check your PATH.\")\n                }\n                File(path)\n            }\n        )\n    }\n\n    private fun createDmgContext(): Map<String, Any> {\n        val outputFileNameWithExt = outputFileName.get() + \".dmg\"\n        return mapOf(\n            \"input_dir\" to inputDir.get().asFile.absolutePath.asQuoted(),\n            \"output_file\" to destFolder.file(outputFileNameWithExt)\n                .get().asFile.absolutePath.asQuoted(),\n            \"background_image\" to backgroundImage.get().absolutePath.asQuoted(),\n            \"volume_icon\" to volumeIcon.get().absolutePath.asQuoted(),\n            \"icon_file\" to appFileName.get().asQuoted(),\n            \"app_name\" to appName.get().asQuoted(),\n            \"icon_size\" to iconSize.get(),\n            \"window_width\" to windowWidth.get(),\n            \"window_height\" to windowHeight.get(),\n            \"icons_y\" to iconsY.get(),\n            \"app_offset_x\" to appOffsetX.get(),\n            \"folder_offset_x\" to folderOffsetX.get(),\n            \"window_x\" to windowX.get(),\n            \"window_y\" to windowY.get(),\n            \"license_file\" to licenseFile.get().absolutePath\n        )\n    }\n\n    private fun String.asQuoted() = \"\\\"${this}\\\"\"\n\n    @TaskAction\n    fun run() {\n        val executable = dmgExecutable.get()\n        val context = createDmgContext()\n\n        // Use launchctl to run in the user's GUI session.\n        // This is required because create-dmg uses AppleScript to manipulate Finder,\n        // which only works inside an active user session with GUI access.\n        val fullCommand = buildString {\n            append(\"launchctl asuser $(id -u) \")\n            append(\"${executable.absolutePath.asQuoted()} \")\n            append(\"--volname ${context[\"app_name\"]} \")\n            append(\"--background ${context[\"background_image\"]} \")\n            append(\"--window-size ${context[\"window_width\"]} ${context[\"window_height\"]} \")\n            append(\"--icon-size ${context[\"icon_size\"]} \")\n            append(\"--icon ${context[\"icon_file\"]} ${context[\"app_offset_x\"]} ${context[\"icons_y\"]} \")\n            append(\"--app-drop-link ${context[\"folder_offset_x\"]} ${context[\"icons_y\"]} \")\n            append(\"--eula ${context[\"license_file\"]} \")\n            append(\"--volicon ${context[\"volume_icon\"]} \")\n            append(\"--window-pos ${context[\"window_x\"]} ${context[\"window_y\"]} \")\n            append(\"${context[\"output_file\"]} \")\n            append(\"${context[\"input_dir\"]}\")\n        }\n\n        logger.debug(\"Creating DMG with shell command: {}\", fullCommand)\n\n        execOps.exec {\n            commandLine(\"sh\", \"-c\", fullCommand)\n            isIgnoreExitValue = false\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/tasks/windows/NsisTask.kt",
    "content": "package ir.amirab.installer.tasks.windows\n\nimport com.github.jknack.handlebars.Context\nimport com.github.jknack.handlebars.Handlebars\nimport ir.amirab.installer.extensiion.WindowsConfig\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.MapProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputDirectory\nimport org.gradle.api.tasks.InputFile\nimport org.gradle.api.tasks.Internal\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.kotlin.dsl.mapProperty\nimport org.gradle.process.ExecOperations\nimport java.io.ByteArrayInputStream\nimport java.io.File\nimport javax.inject.Inject\n\nabstract class NsisTask : DefaultTask() {\n    @get:Inject\n    abstract val execOps: ExecOperations\n\n    @get:InputDirectory\n    abstract val sourceFolder: DirectoryProperty\n\n    @get:OutputDirectory\n    abstract val destFolder: DirectoryProperty\n\n    @get:Input\n    abstract val outputFileName: Property<String>\n\n    @get:InputFile\n    abstract val nsisTemplate: Property<File>\n\n    @get:Input\n    abstract val commonParams: Property<WindowsConfig>\n\n    @get:Input\n    val extraParams: MapProperty<String, Any> = project.objects.mapProperty<String, Any>()\n\n    @get:Internal\n    abstract val nsisExecutable: Property<File>\n\n    init {\n        nsisExecutable.convention(\n            project.provider { File(\"C:\\\\Program Files (x86)\\\\NSIS\\\\makensis.exe\") }\n        )\n    }\n\n    private fun createHandleBarContext(): Context {\n        val commonParams = commonParams.get()\n        val common = mapOf(\n            \"app_name\" to commonParams.appName!!,\n            \"app_display_name\" to commonParams.appDisplayName!!,\n            \"app_version\" to commonParams.appVersion!!,\n            \"app_display_version\" to commonParams.appDisplayVersion!!,\n            \"app_data_dir_name\" to commonParams.appDataDirName!!,\n            \"license_file\" to commonParams.licenceFile!!,\n            \"icon_file\" to commonParams.iconFile!!,\n        )\n        val overrides = mapOf(\n            \"input_dir\" to sourceFolder.get().asFile.absolutePath,\n            \"output_file\" to \"${destFolder.file(outputFileName).get().asFile.path}.exe\",\n        )\n        return Context.newContext(\n            extraParams\n                .get()\n                .plus(common)\n                .plus(overrides)\n        )\n    }\n\n    @TaskAction\n    fun run() {\n        val executable = nsisExecutable.get()\n        val scriptTemplate = nsisTemplate.get()\n        val handlebars = Handlebars()\n        val context = createHandleBarContext()\n        val script = handlebars.compileInline(\n            scriptTemplate.readText()\n        ).apply(context)\n        logger.debug(\"NSIS Script:\")\n        logger.debug(script)\n        execOps.exec {\n            executable(\n                executable,\n            )\n            args(\"-\")\n            standardInput = ByteArrayInputStream(script.toByteArray())\n\n        }\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/plugins/installer-plugin/src/main/kotlin/ir/amirab/installer/utils/Contants.kt",
    "content": "package ir.amirab.installer.utils\n\ninternal object Constants {\n    const val CREATE_INSTALLER_TASK_NAME = \"createInstaller\"\n}"
  },
  {
    "path": "compositeBuilds/plugins/settings.gradle.kts",
    "content": "dependencyResolutionManagement{\n    versionCatalogs {\n        create(\"libs\"){\n            from(files(\"../../gradle/libs.versions.toml\"))\n        }\n    }\n}\ninclude(\"git-version-plugin\")\ninclude(\"installer-plugin\")\ninclude(\"common-android\")\n"
  },
  {
    "path": "compositeBuilds/shared/README.md",
    "content": "### Note\nthis is a shared module that used in both `buildSrc` and my main build\nhere how I can add dependency to each of my composite modules\n```kts\nimplementation(\"$definedGroupId:$definedProjectName:$projectVersion\")\n```\nthe benefit of this solution is two things\n1. I can use shared code in both `buildSrc` and `root project's mainBuild`\n2. I can move this module in separate repository without any modifications "
  },
  {
    "path": "compositeBuilds/shared/build.gradle.kts",
    "content": "plugins {\n    // this project is used in both gradle and main project\n    // the gradle version usually is lower than our project kotlin version\n    // so we use the version that is used in gradle to fix version compatibility\n    kotlin(\"jvm\") version embeddedKotlinVersion apply false\n}\n"
  },
  {
    "path": "compositeBuilds/shared/platform/build.gradle.kts",
    "content": "plugins{\n    kotlin(\"multiplatform\")\n}\nrepositories{\n    mavenCentral()\n}\nkotlin {\n    jvm(\"desktop\")\n}\nversion=1\ngroup=\"ir.amirab.util\"\n"
  },
  {
    "path": "compositeBuilds/shared/platform/src/commonMain/kotlin/ir/amirab/util/platform/Arch.kt",
    "content": "package ir.amirab.util.platform\n\nsealed class Arch(val name: String) {\n    data object X64 : Arch(\"x64\")\n    data object Arm64 : Arch(\"arm64\")\n    data object X32 : Arch(\"x32\")\n    data object Arm32 : Arch(\"arm32\")\n\n    override fun toString(): String {\n        return name\n    }\n\n    companion object : ArchFinder by JvmArchFinder() {\n        private val DefinedArchStrings = mapOf(\n            X64 to listOf(\n                \"amd64\", \"x64\", \"x86_64\"\n            ),\n            Arm64 to listOf(\n                \"arm64\", \"aarch64\"\n            ),\n            X32 to listOf(\n                \"x86\", \"i686\"\n            ),\n            Arm32 to listOf(\n                \"armv8l\", \"armv7l\", \"arm\"\n            ),\n        )\n\n        fun fromString(archName: String): Arch? {\n            val a = archName.lowercase()\n            return DefinedArchStrings.entries.firstOrNull {\n                a in it.value\n            }?.key\n        }\n    }\n}\n\ninterface ArchFinder {\n    fun getCurrentArch(): Arch\n}\n\nprivate class JvmArchFinder : ArchFinder {\n    private val _arch by lazy {\n        getCurrentArchFromJVMProperty()\n    }\n\n    private fun getCurrentArchFromJVMProperty(): Arch {\n        val osString = System.getProperty(\"os.arch\").lowercase()\n        return requireNotNull(Arch.fromString(osString)) {\n            \"this arch is not recognized: $osString\"\n        }\n    }\n\n    override fun getCurrentArch(): Arch {\n        return _arch\n    }\n}\n"
  },
  {
    "path": "compositeBuilds/shared/platform/src/commonMain/kotlin/ir/amirab/util/platform/Platform.kt",
    "content": "package ir.amirab.util.platform\n\nimport ir.amirab.util.platform.Platform.Android\nimport ir.amirab.util.platform.Platform.Desktop\n\nsealed class Platform(val name: String) {\n    data object Android : Platform(\"Android\")\n    sealed class Desktop(name: String) : DesktopPlatform,\n        Platform(name) {\n        data object Windows : Desktop(\"Windows\")\n        data object Linux : Desktop(\"Linux\")\n        data object MacOS : Desktop(\"Mac\")\n    }\n\n    override fun toString(): String {\n        return name\n    }\n\n    companion object : PlatformFInder by JvmPlatformFinder() {\n        fun fromString(platformName: String): Platform? {\n            return when (platformName.lowercase()) {\n                \"windows\" -> Desktop.Windows\n                \"linux\" -> Desktop.Linux\n                \"mac\" -> Desktop.MacOS\n                \"android\" -> Android\n                else -> null\n            }\n        }\n\n        fun fromExecutableFileExtension(fileExtension: String): Platform? {\n            return when (fileExtension.lowercase()) {\n                \"exe\", \"msi\" -> Desktop.Windows\n                \"deb\", \"rpm\" -> Desktop.Linux\n                \"dmg\", \"pkg\" -> Desktop.MacOS\n                \"apk\" -> Android\n                else -> null\n            }\n        }\n    }\n}\n\ninterface PlatformFInder {\n    fun getCurrentPlatform(): Platform\n}\n\nprivate class JvmPlatformFinder : PlatformFInder {\n    private val _platform by lazy {\n        getCurrentPlatformFromJVMProperty()\n    }\n\n    private fun isAndroid(): Boolean {\n        val vm = System.getProperty(\"java.vm.name\")?.lowercase().orEmpty()\n        val vendor = System.getProperty(\"java.vendor\")?.lowercase().orEmpty()\n        val isAndroid = \"android\" in vendor || \"dalvik\" in vm\n        return isAndroid\n    }\n\n    private fun getCurrentPlatformFromJVMProperty(): Platform {\n        val osString = System.getProperty(\"os.name\").orEmpty().lowercase()\n        if (isAndroid()) {\n            return Android\n        }\n        return when {\n            osString.contains(\"windows\") -> Desktop.Windows\n            osString.contains(\"linux\") -> Desktop.Linux\n            osString.contains(\"mac\") || osString.contains(\"darwin\") -> Desktop.MacOS\n            else -> error(\"this platform is not detected: $osString\")\n        }\n    }\n\n    override fun getCurrentPlatform(): Platform {\n        return _platform\n    }\n}\n\nsealed interface DesktopPlatform\n\n\n/**\n * use this only in desktop environments\n */\nfun PlatformFInder.asDesktop(): Desktop {\n    val platform = getCurrentPlatform()\n    if (platform is Desktop) {\n        return platform\n    } else {\n        error(\"Current platform is not a desktop platform\")\n    }\n}\n\nfun PlatformFInder.isWindows(): Boolean {\n    return getCurrentPlatform() == Desktop.Windows\n}\n\nfun PlatformFInder.isMac(): Boolean {\n    return getCurrentPlatform() == Desktop.MacOS\n}\n\nfun PlatformFInder.isLinux(): Boolean {\n    return getCurrentPlatform() == Desktop.Linux\n}\n\nfun PlatformFInder.isAndroid(): Boolean {\n    return getCurrentPlatform() == Android\n}\n"
  },
  {
    "path": "compositeBuilds/shared/settings.gradle.kts",
    "content": "dependencyResolutionManagement{\n    versionCatalogs {\n        create(\"libs\"){\n            from(files(\"../../gradle/libs.versions.toml\"))\n        }\n    }\n}\nrootProject.name = \"shared-code-between-gradle-and-app\"\ninclude(\"platform\")"
  },
  {
    "path": "crowdin.yml",
    "content": "\"project_id_env\": \"CROWDIN_PROJECT_ID\"\n\"api_token_env\": \"CROWDIN_PERSONAL_TOKEN\"\n\"base_path\": \".\"\n\"base_url\": \"https://api.crowdin.com\"\n\"preserve_hierarchy\": true\nfiles: [\n  {\n      \"source\": \"/shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/en_US.properties\",\n      \"translation\": \"/shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/%locale_with_underscore%.properties\",\n  }\n]\n"
  },
  {
    "path": "desktop/app/build.gradle.kts",
    "content": "import buildlogic.*\nimport buildlogic.versioning.*\nimport ir.amirab.installer.InstallerTargetFormat\nimport org.jetbrains.changelog.Changelog\nimport org.jetbrains.compose.desktop.application.dsl.TargetFormat\nimport ir.amirab.util.platform.Platform\nimport org.jetbrains.compose.desktop.application.dsl.TargetFormat.*\nimport com.mikepenz.aboutlibraries.plugin.DuplicateMode\nimport com.mikepenz.aboutlibraries.plugin.DuplicateRule\nimport ir.amirab.util.platform.Arch\n\nplugins {\n    id(MyPlugins.kotlin)\n    id(MyPlugins.composeDesktop)\n    id(Plugins.Kotlin.serialization)\n    id(Plugins.ksp)\n    id(Plugins.aboutLibraries)\n    id(\"ir.amirab.installer-plugin\")\n//    id(MyPlugins.proguardDesktop)\n}\n\n\ndependencies {\n    implementation(libs.decompose)\n    implementation(libs.decompose.jbCompose)\n\n    implementation(libs.koin.core)\n\n    implementation(libs.kotlin.serialization.json)\n\n    implementation(libs.kotlin.coroutines.core)\n    implementation(libs.kotlin.coroutines.swing)\n\n    implementation(libs.kotlin.datetime)\n\n    implementation(libs.compose.reorderable)\n\n    implementation(libs.http4k.core)\n    implementation(libs.http4k.client.okhttp)\n\n    implementation(libs.arrow.core)\n    implementation(libs.arrow.optics)\n    ksp(libs.arrow.opticKsp)\n\n    implementation(libs.androidx.datastore)\n\n    implementation(libs.aboutLibraries.core)\n    implementation(libs.markdownRenderer.core)\n    implementation(libs.composeFileKit) {\n        exclude(group = \"net.java.dev.jna\")\n    }\n    implementation(libs.proxyVole) {\n        exclude(group = \"net.java.dev.jna\")\n    }\n    implementation(libs.jna.core)\n    implementation(libs.jna.platform)\n\n    implementation(project(\":downloader:core\"))\n    implementation(project(\":downloader:monitor\"))\n\n    implementation(project(\":integration:server\"))\n    implementation(project(\":desktop:shared\"))\n    implementation(project(\":desktop:app-utils\"))\n\n    implementation(libs.composeNativeTray)\n\n    implementation(project(\":shared:app\"))\n    implementation(project(\":shared:utils\"))\n    implementation(project(\":shared:updater\"))\n    implementation(project(\":shared:nanohttp4k\"))\n    implementation(project(\":desktop:mac_utils\"))\n}\n\naboutLibraries {\n    export {\n        prettyPrint = true\n    }\n    library {\n        duplicationMode = DuplicateMode.MERGE\n        duplicationRule = DuplicateRule.SIMPLE\n    }\n}\n\ntasks.processResources {\n    from(tasks.named(\"exportLibraryDefinitions\"))\n}\n\nval desktopPackageName = \"com.abdownloadmanager.desktop\"\ncompose {\n    desktop {\n        application {\n//            val getProguardConfigurationsTask = tasks.getProguardConfigurations.get()\n            buildTypes.release.proguard {\n                isEnabled.set(false)\n//                obfuscate.set(false)\n//                optimize.set(true)\n//                configurationFiles.from(\n//                    project.fileTree(\"proguard\"),\n//                    getProguardConfigurationsTask.outputs.files.asFileTree.filter {\n//                        !it.name.contains(\"r8\")\n//                    },\n//                )\n            }\n\n            // Define the main class for the application.\n            mainClass = \"$desktopPackageName.AppKt\"\n            nativeDistributions {\n                modules(\n                    \"java.instrument\",\n                    \"jdk.unsupported\",\n                    \"jdk.accessibility\",\n                )\n                targetFormats(Msi, Deb)\n                if (Platform.getCurrentPlatform() == Platform.Desktop.Linux) {\n                    // filekit library requires this module in linux.\n                    modules(\"jdk.security.auth\")\n                }\n                packageVersion = getAppVersionStringForPackaging()\n                packageName = getAppName()\n                vendor = \"abdownloadmanager.com\"\n                appResourcesRootDir.set(project.layout.projectDirectory.dir(\"resources\"))\n                val menuGroupName = getPrettifiedAppName()\n                licenseFile.set(rootProject.file(\"LICENSE\"))\n                linux {\n                    debPackageVersion = getAppVersionStringForPackaging(Deb)\n                    rpmPackageVersion = getAppVersionStringForPackaging(Rpm)\n                    appCategory = \"Network\"\n                    iconFile = project.file(\"icons/icon.png\")\n                    menuGroup = menuGroupName\n                    shortcut = true\n                }\n                macOS {\n                    pkgPackageVersion = getAppVersionStringForPackaging(Pkg)\n                    dmgPackageVersion = getAppVersionStringForPackaging(Dmg)\n                    iconFile = project.file(\"icons/icon.icns\")\n                    infoPlist {\n                        extraKeysRawXml = \"\"\"\n                            <key>LSUIElement</key>\n                            <string>true</string>\n                        \"\"\".trimIndent()\n                    }\n                    jvmArgs(\"-Dapple.awt.enableTemplateImages=true\")\n                }\n                windows {\n                    exePackageVersion = getAppVersionStringForPackaging(Exe)\n                    msiPackageVersion = getAppVersionStringForPackaging(Msi)\n                    upgradeUuid = properties[\"INSTALLER.WINDOWS.UPGRADE_UUID\"]?.toString()\n                    iconFile = project.file(\"icons/icon.ico\")\n                    console = false\n                    dirChooser = true\n                    shortcut = true\n                    menuGroup = menuGroupName\n                    menu = true\n                }\n            }\n        }\n    }\n}\n\ninstallerPlugin {\n    dependsOn(\"createReleaseDistributable\")\n    outputFolder.set(layout.buildDirectory.dir(\"custom-installer\"))\n    windows {\n        appName = getAppName()\n        appDisplayName = getPrettifiedAppName()\n        appVersion = getAppVersionStringForPackaging(Exe)\n        appDisplayVersion = getAppVersionString()\n        appDataDirName = getAppDataDirName()\n        inputDir = project.file(\"build/compose/binaries/main-release/app/${getAppName()}\")\n        outputFileName = getAppName()\n        licenceFile = rootProject.file(\"LICENSE\")\n        iconFile = project.file(\"icons/icon.ico\")\n        nsisTemplate = project.file(\"resources/installer/nsis-script-template.nsi\")\n        extraParams = mapOf(\n            \"app_publisher\" to \"abdownloadmanager.com\",\n            \"app_version_with_build\" to \"${getAppVersionStringForPackaging(Exe)}.0\",\n            \"source_code_url\" to \"https://github.com/amir1376/ab-download-manager\",\n            \"project_website\" to \"www.abdownloadmanager.com\",\n            \"copyright\" to \"© 2024-present AB Download Manager App\",\n            \"header_image_file\" to project.file(\"resources/installer/abdm-header-image.bmp\"),\n            \"sidebar_image_file\" to project.file(\"resources/installer/abdm-sidebar-image.bmp\")\n        )\n    }\n    macos {\n        appName = getAppName()\n        inputDir = project.file(\"build/compose/binaries/main-release/app/\")\n        appFileName = \"${getAppName()}.app\"\n        backgroundImage = project.file(\"resources/installer/dmg_background.png\")\n        outputFileName = getAppName()\n        licenseFile = rootProject.file(\"LICENSE\")\n        volumeIcon = project.file(\"icons/icon.icns\")\n    }\n}\n\n\n// ======= begin of GitHub action stuff\nval ciDir = CiUtils.getCiDir(project)\n\nval appPackageNameByComposePlugin\n    get() = requireNotNull(compose.desktop.application.nativeDistributions.packageName) {\n        \"compose.desktop.application.nativeDistributions.packageName must not be null!\"\n    }\n\nval distributableAppArchiveDir: Provider<Directory> =\n    project.layout.buildDirectory.dir(\"dist/archives\")\n\nfun AbstractArchiveTask.fromAppImagePath() {\n    from(tasks.named(\"createReleaseDistributable\"))\n    destinationDirectory.set(distributableAppArchiveDir)\n}\n\n/**\n * gradle 9 removes file permissions and timestamp by default in archive tasks!. but we want them!\n */\nfun AbstractArchiveTask.preserveFileAttributes() {\n    // Make file order based on the file system\n    isReproducibleFileOrder = false\n    // Use file timestamps from the file system\n    isPreserveFileTimestamps = true\n    // Use permissions from the file system\n    useFileSystemPermissions()\n}\n\nval createDistributableAppArchiveTar by tasks.registering(Tar::class) {\n    preserveFileAttributes()\n    archiveFileName.set(\"app.tar.gz\")\n    compression = Compression.GZIP\n    fromAppImagePath()\n}\nval createDistributableAppArchiveZip by tasks.registering(Zip::class) {\n    preserveFileAttributes()\n    archiveFileName.set(\"app.zip\")\n    fromAppImagePath()\n}\nval createDistributableAppArchive by tasks.registering {\n    when (Platform.getCurrentPlatform()) {\n        Platform.Desktop.Linux,\n        Platform.Desktop.MacOS -> dependsOn(createDistributableAppArchiveTar)\n\n        Platform.Desktop.Windows -> dependsOn(createDistributableAppArchiveZip)\n        Platform.Android -> error(\"this task is used for desktop only\")\n    }\n}\n\ntasks.register(CiUtils.getCreateBinaryFolderForCiTaskName()) {\n    if (installerPlugin.isThisPlatformSupported()) {\n        dependsOn(installerPlugin.createInstallerTask)\n        inputs.dir(installerPlugin.outputFolder)\n    }\n    dependsOn(createDistributableAppArchive)\n    inputs.property(\"appVersion\", getAppVersionString())\n    inputs.dir(distributableAppArchiveDir)\n    outputs.dir(ciDir.binariesDir)\n    doLast {\n        val output = ciDir.binariesDir.get().asFile\n        val packageName = appPackageNameByComposePlugin\n\n        if (installerPlugin.isThisPlatformSupported()) {\n            val targets = installerPlugin.getCreatedInstallerTargetFormats()\n            for (target in targets) {\n                CiUtils.movePackagedAndCreateSignature(\n                    appVersion = getAppVersion(),\n                    packageName = packageName,\n                    target = target,\n                    basePackagedAppsDir = installerPlugin.outputFolder.get().asFile,\n                    outputDir = output,\n                )\n            }\n            logger.lifecycle(\"app packages for '${targets.joinToString(\", \") { it.name }}' written in $output using the installer plugin\")\n        }\n        val appArchiveDistributableDir = distributableAppArchiveDir.get().asFile\n        CiUtils.copyAndHashToDestination(\n            distributableAppArchiveDir.get().asFile.resolve(\n                CiUtils.getFileOfDistributedArchivedTarget(\n                    appArchiveDistributableDir,\n                )\n            ),\n            output,\n            CiUtils.getTargetFileName(\n                packageName,\n                getAppVersion(),\n                null, // this is not an installer (it will be automatically converted to current os name\n                Arch.getCurrentArch().name\n            )\n        )\n        logger.lifecycle(\"distributable app archive written in ${output}\")\n    }\n}\n// ======= end of GitHub action stuff\n\nfun TargetFormat.toInstallerTargetFormat(): InstallerTargetFormat {\n    return when (this) {\n        AppImage -> error(\"$this is not recognized as installer\")\n        Deb -> InstallerTargetFormat.Deb\n        Rpm -> InstallerTargetFormat.Rpm\n        Dmg -> InstallerTargetFormat.Dmg\n        Pkg -> InstallerTargetFormat.Pkg\n        Exe -> InstallerTargetFormat.Exe\n        Msi -> InstallerTargetFormat.Msi\n    }\n}\n"
  },
  {
    "path": "desktop/app/gradle.properties",
    "content": "INSTALLER.WINDOWS.UPGRADE_UUID=2b3aeac1-271a-4f05-b26e-8e0d2a81b215"
  },
  {
    "path": "desktop/app/proguard/decompose.pro",
    "content": "-dontwarn com.arkivanov.decompose.**\n-keep class * implements com.arkivanov.decompose.mainthread.MainThreadChecker"
  },
  {
    "path": "desktop/app/proguard/main.pro",
    "content": "#-dontobfuscate\n-keep class kotlinx.coroutines.swing.**\n\n"
  },
  {
    "path": "desktop/app/proguard/okhttp.pro",
    "content": "-dontwarn okhttp3.internal.platform.**\n-dontwarn okio.**"
  },
  {
    "path": "desktop/app/resources/common/app.properties",
    "content": "app.debug=\"false\"\n"
  },
  {
    "path": "desktop/app/resources/installer/nsis-script-template.nsi",
    "content": "Unicode True\nRequestExecutionLevel user\nSetCompressor /SOLID lzma\n!include \"LogicLib.nsh\"\n!include \"MUI2.nsh\"\n\n\n!define APP_PUBLISHER \"{{ app_publisher }}\"\n!define APP_NAME \"{{ app_name }}\"\n!define APP_DISPLAY_NAME \"{{ app_display_name }}\"\n!define APP_DATA_DIR_NAME \"{{ app_data_dir_name }}\"\n!define APP_VERSION \"{{ app_version }}\"\n!define APP_VERSION_WITH_BUILD \"{{ app_version_with_build }}\"\n!define APP_DISPLAY_VERSION \"{{ app_display_version }}\"\n!define SOURCE_CODE_URL \"{{ source_code_url }}\"\n!define PROJECT_WEBSITE \"{{ project_website }}\"\n!define COPYRIGHT \"{{ copyright }}\"\n\n!define INPUT_DIR \"{{ input_dir }}\"\n!define LICENSE_FILE \"{{ license_file }}\"\n!define MAIN_BINARY_NAME \"${APP_NAME}\"\n\n!define SIDEBAR_IMAGE \"{{ sidebar_image_file }}\"\n!define HEADER_IMAGE \"{{ header_image_file }}\"\n!define ICON_FILE \"{{ icon_file }}\"\n\n!define REG_UNINSTALL_KEY \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\"\n!define REG_RUN_KEY \"Software\\Microsoft\\Windows\\CurrentVersion\\Run\\${APP_NAME}\"\n!define REG_APP_KEY \"Software\\${APP_NAME}\"\n\n; icon for this installer!\n\nIcon \"${ICON_FILE}\"\n!define MUI_ICON \"${ICON_FILE}\"\n!define MUI_UNICON \"${ICON_FILE}\"\n\n!if \"${SIDEBAR_IMAGE}\" != \"\"\n  !define MUI_WELCOMEFINISHPAGE_BITMAP \"${SIDEBAR_IMAGE}\"\n\n  !define MUI_UNWELCOMEFINISHPAGE_BITMAP \"${SIDEBAR_IMAGE}\"\n!endif\n\n!if \"${HEADER_IMAGE}\" != \"\"\n  !define MUI_HEADERIMAGE\n  !define MUI_HEADERIMAGE_BITMAP  \"${HEADER_IMAGE}\"\n\n  !define MUI_UNHEADERIMAGE\n  !define MUI_UNHEADERIMAGE_BITMAP \"${HEADER_IMAGE}\"\n!endif\n\nVIProductVersion \"${APP_VERSION_WITH_BUILD}\"\nVIAddVersionKey \"ProductName\" \"${APP_DISPLAY_NAME}\"\nVIAddVersionKey \"FileDescription\" \"${APP_DISPLAY_NAME}\"\nVIAddVersionKey \"LegalCopyright\" \"${COPYRIGHT}\"\nVIAddVersionKey \"FileVersion\" \"${APP_VERSION_WITH_BUILD}\"\nVIAddVersionKey \"ProductVersion\" \"${APP_VERSION_WITH_BUILD}\"\n\nName \"${APP_DISPLAY_NAME}\"\nOutFile \"{{ output_file }}\"\n\nInstallDir \"$LOCALAPPDATA\\${APP_NAME}\"\n\n\n\n!define INSTALL_DIR `$INSTDIR`\nFunction .onInit\n\n    ; Call RestorePreviousInstallLocation\n\nFunctionEnd\n\n; configure instfiles page\n!define MUI_FINISHPAGE_NOAUTOCLOSE\n!define MUI_INSTFILESPAGE_NOAUTOCLOSE\n\n; configure finish page\n!define MUI_FINISHPAGE_LINK \"Open project in GitHub\"\n!define MUI_FINISHPAGE_LINK_LOCATION \"${SOURCE_CODE_URL}\"\n!define MUI_FINISHPAGE_RUN\n!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary\n\n;Installation Pages\n!insertmacro MUI_PAGE_WELCOME\n!insertmacro MUI_PAGE_LICENSE \"${LICENSE_FILE}\"\n!insertmacro MUI_PAGE_COMPONENTS\n!insertmacro MUI_PAGE_INSTFILES\n!insertmacro MUI_PAGE_FINISH\n\n;Uninstallation Pages\n!insertmacro MUI_UNPAGE_WELCOME\n!insertmacro MUI_UNPAGE_COMPONENTS\n!insertmacro MUI_UNPAGE_CONFIRM\n!insertmacro MUI_UNPAGE_INSTFILES\n!insertmacro MUI_UNPAGE_FINISH\n\n; set language\n!insertmacro MUI_LANGUAGE \"English\"\n\n; a macro clear files to cleanup installation folder\n!macro clearFiles\n    RmDir /r \"${INSTALL_DIR}\\app\"\n    RmDir /r \"${INSTALL_DIR}\\runtime\"\n    Delete \"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.exe\"\n    Delete \"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.ico\"\n    Delete \"${INSTALL_DIR}\\uninstall.exe\"\n    RmDir \"${INSTALL_DIR}\"\n!macroend\n\nFunction RunMainBinary\n   Exec \"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.exe\"\nFunctionEnd\n\n!macro GetBestExecutableName result\n    StrCpy ${result} \"${MAIN_BINARY_NAME}.exe\"\n!macroend\n\n; Function RestorePreviousInstallLocation\n;     ReadRegStr $4 SHCTX \"${REG_APP_KEY}\" \"InstallPath\"\n;     ${if} $4 != \"\"\n;         StrCpy $INSTDIR $4\n;     ${endif}\n; FunctionEnd\n\n; I should improve this.\n!macro closeApp\n    !insertmacro GetBestExecutableName $1\n    DetailPrint \"Stopping Executable $1\"\n    ; I don't wanna kill myself!\n    ${If} \"$EXEFILE\" != \"$1\"\n        ExecWait 'taskkill /F /IM \"$1\"' $0\n    ${Else}\n        DetailPrint \"It seems that installer file name is same as app executable name\"\n        DetailPrint \"Please close app manually\"\n        ; don't sleep the script for nothing.\n        StrCpy $0 \"1\"\n    ${EndIf}\n    ${If} $0 == \"0\"\n        Sleep 500\n        BringToFront ; when we sleep it seems that window goes down\n        DetailPrint \"Current app stopped successfully\"\n    ${Endif}\n!macroend\n\n!macro CreateStartMenu\n\tcreateDirectory \"$SMPROGRAMS\\${APP_DISPLAY_NAME}\"\n\tcreateShortCut \"$SMPROGRAMS\\${APP_DISPLAY_NAME}\\${APP_DISPLAY_NAME}.lnk\" \"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.exe\"\n!macroend\n\n!macro RemoveStartMenu\n\tRmDir /r \"$SMPROGRAMS\\${APP_DISPLAY_NAME}\"\n!macroend\n\n!macro RemoveUserData\n\tRMDir /r \"$PROFILE\\${APP_DATA_DIR_NAME}\"\n\tRmDir /r \"${INSTALL_DIR}\\${APP_DATA_DIR_NAME}\"\n!macroend\n\n!macro CreateDesktopShortcut\n    CreateShortcut \"$DESKTOP\\${APP_DISPLAY_NAME}.lnk\" \"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.exe\"\n!macroend\n\n!macro RemoveDesktopShortCut\n\tDelete \"$DESKTOP\\${APP_DISPLAY_NAME}.lnk\"\n!macroend\n\nFunction .onInstSuccess\n    ; Check if the installer is running in silent mode\n    ${If} ${Silent}\n        ; In silent mode, always run the app\n        Call RunMainBinary\n    ${Endif}\nFunctionEnd\n\nSection \"${APP_DISPLAY_NAME}\"\n    SectionInstType RO\n\n    DetailPrint \"Closing app (if any)\"\n    !insertmacro closeApp\n    DetailPrint \"clearing old app (if any)\"\n    !insertmacro clearFiles\n    DetailPrint \"writing new data\"\n    SetOutPath \"${INSTALL_DIR}\"\n    CreateDirectory \"${INSTALL_DIR}\"\n\n    WriteUninstaller \"${INSTALL_DIR}\\uninstall.exe\"\n\n    File /nonfatal /r \"${INPUT_DIR}\\\"\n\n\n    ; Registry information for add/remove programs\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"DisplayName\" \"${APP_DISPLAY_NAME}\"\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"DisplayIcon\" \"$\\\"${INSTALL_DIR}\\${MAIN_BINARY_NAME}.exe$\\\"\"\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"DisplayVersion\" \"${APP_VERSION}\"\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"Publisher\" \"${APP_PUBLISHER}\"\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"InstallLocation\" \"$\\\"${INSTALL_DIR}$\\\"\"\n    WriteRegStr SHCTX \"${REG_UNINSTALL_KEY}\" \"UninstallString\" \"$\\\"${INSTALL_DIR}\\uninstall.exe$\\\"\"\n    WriteRegDWORD SHCTX \"${REG_UNINSTALL_KEY}\" \"NoModify\" \"1\"\n    WriteRegDWORD SHCTX \"${REG_UNINSTALL_KEY}\" \"NoRepair\" \"1\"\n\n    ; Registry keys for app installation path and version\n    WriteRegStr SHCTX \"${REG_APP_KEY}\" \"InstallPath\" \"${INSTALL_DIR}\"\n    WriteRegStr SHCTX \"${REG_APP_KEY}\" \"Version\" \"${APP_VERSION}\"\nSectionEnd\n\nSection \"Start Menu\"\n    !insertmacro CreateStartMenu\nSectionEnd\n\nSection \"Desktop Shortcut\"\n    !insertmacro CreateDesktopShortcut\nSectionEnd\n\nSection /o \"un.Remove User Data\"\n    !insertmacro RemoveUserData\nSectionEnd\n\nSection \"Uninstall\"\n    SectionInstType RO\n\n    !insertmacro closeApp\n    !insertmacro clearFiles\n\n    !insertmacro RemoveStartMenu\n    !insertmacro RemoveDesktopShortCut\n\n    DeleteRegKey SHCTX \"${REG_UNINSTALL_KEY}\"\n    DeleteRegKey SHCTX \"${REG_APP_KEY}\"\n\n    ; remove auto start on boot registry\n    DeleteRegValue SHCTX \"${REG_RUN_KEY}\" \"${APP_NAME}\"\nSectionEnd\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/App.kt",
    "content": "/*\n * This Kotlin source file was generated by the Gradle 'init' task.\n */\npackage com.abdownloadmanager.desktop\n\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.desktop.di.Di\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.desktop.ui.Ui\nimport com.abdownloadmanager.desktop.utils.*\nimport com.abdownloadmanager.desktop.utils.renderapi.CustomRenderApi\nimport com.abdownloadmanager.desktop.utils.singleInstance.AnotherInstanceIsRunning\nimport com.abdownloadmanager.desktop.utils.singleInstance.MutableSingleInstanceServerHandler\nimport com.abdownloadmanager.desktop.utils.singleInstance.SingleInstanceUtil\nimport com.abdownloadmanager.integration.Integration\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.appinfo.PreviousVersion\nimport kotlinx.coroutines.runBlocking\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.system.exitProcess\n\nclass App : AutoCloseable,\n    KoinComponent {\n    private val downloadSystem: DownloadSystem by inject()\n    private val appRepository: AppRepository by inject()\n    private val integration: Integration by inject()\n    private val previousVersion: PreviousVersion by inject()\n    private val updateManager: UpdateManager by inject()\n    private val keepAwakeManager: KeepAwakeManager by inject()\n    private val customRenderApi: CustomRenderApi by inject()\n\n    //TODO Setup Native Messaging Feature\n    //private val browserNativeMessaging: NativeMessaging by inject()\n    fun start(\n        appArguments: AppArguments,\n        singleInstanceServerHandler: MutableSingleInstanceServerHandler,\n        globalAppExceptionHandler: GlobalAppExceptionHandler,\n    ) {\n        try {\n            runBlocking {\n                //make sure to not get any dependency until boot the DI Container\n                Di.boot()\n                // it's better to organize these list of boot functions in a separate class\n\n                // boot configs from the storage so download manager can use them on boot!\n                customRenderApi.boot()\n                appRepository.boot()\n                integration.boot()\n                downloadSystem.boot()\n                previousVersion.boot()\n                keepAwakeManager.boot()\n                //TODO Setup Native Messaging Feature\n                //waiting for compose kmp to add multi launcher to nativeDistributions,the PR is already exists but not merger\n                //or maybe I should use a custom solution\n                //browserNativeMessaging.boot()\n                SingleInstanceServerInitializer.boot(singleInstanceServerHandler)\n                Ui.boot(appArguments, globalAppExceptionHandler)\n            }\n        } catch (e: Exception) {\n            globalAppExceptionHandler.onProcessIsUseless()\n            throw e\n        }\n    }\n\n    override fun close() {\n        //nothing yet!\n    }\n}\n\n\nfun main(args: Array<String>) {\n    try {\n        AppArguments.init(args)\n        AppProperties.boot()\n        val appArguments = AppArguments.get()\n        if (appArguments.version) {\n            dispatchVersionAndExit()\n        }\n        val singleInstance = SingleInstanceUtil(AppInfo.definedPaths.configDir)\n        if (appArguments.exit) {\n            exitExistingProcessAndExit(singleInstance)\n        }\n        if (appArguments.startIfNotStarted && !AppInfo.isInIDE()) {\n            startAndWaitForRunIfNotRunning(singleInstance)\n        }\n        if (appArguments.getIntegrationPort) {\n            dispatchIntegrationPortAndExit(singleInstance)\n        }\n        //going to start main app\n        defaultApp(\n            singleInstance = singleInstance,\n            appArguments = appArguments,\n        )\n\n    } catch (e: Throwable) {\n        System.err.println(\"Fail to start the ${AppInfo.displayName} app because:\")\n        e.printStackTrace()\n        exitProcess(-1)\n    }\n}\n\nprivate fun startAppInAnotherProcess() {\n    val exeFile = requireNotNull(AppInfo.exeFile)\n    val cmd = listOf(\n        exeFile,\n        AppArguments.Args.BACKGROUND\n    ).joinToString(\" \").also {\n//        println(\"executing $it\")\n    }\n    Runtime.getRuntime().exec(cmd)\n}\n\nprivate fun dispatchVersionAndExit(): Nothing {\n    print(AppInfo.version)\n    exitProcess(0)\n}\n\nprivate fun exitExistingProcessAndExit(singleInstance: SingleInstanceUtil): Nothing {\n    singleInstance.sendToInstance(Commands.exit)\n    exitProcess(0)\n}\n\nprivate fun dispatchIntegrationPortAndExit(singleInstance: SingleInstanceUtil): Nothing {\n    val port =\n        singleInstance.sendToInstance(Commands.getIntegrationPort)\n            .orElse { IntegrationPortBroadcaster.INTEGRATION_UNKNOWN }\n    print(port)\n    exitProcess(0)\n}\n\nprivate fun startAndWaitForRunIfNotRunning(\n    singleInstance: SingleInstanceUtil,\n    howMuchWait: Long = 10_000,\n    initialDelay: Long = 0,\n    eachTimeDelay: Long = 500L,\n) {\n    val deadLine = System.currentTimeMillis() + howMuchWait\n    if (initialDelay > 0) {\n        Thread.sleep(initialDelay)\n    }\n    var firstLoop = true\n    while (true) {\n        val isReady: Boolean = singleInstance\n            .sendToInstance(Commands.isReady)\n            .orElse {\n//                println(\"or else $it\")\n                false\n            }\n//        println(\"isReady: $isReady\")\n        if (isReady) {\n            return\n        }\n        if (firstLoop) {\n            startAppInAnotherProcess()\n//            println(\"send start signal\")\n        }\n        if (System.currentTimeMillis() >= deadLine) {\n//            println(\"dead line reached\")\n            //deadline reached exiting now\n            exitProcess(1)\n        }\n        Thread.sleep(eachTimeDelay)\n        firstLoop = false\n    }\n}\n\nprivate fun defaultApp(\n    appArguments: AppArguments,\n    singleInstance: SingleInstanceUtil,\n) {\n    val singleInstanceServerHandler by lazy { MutableSingleInstanceServerHandler() }\n    try {\n        singleInstance.lockInstance { singleInstanceServerHandler }\n    } catch (e: AnotherInstanceIsRunning) {\n        println(\"instance already running\")\n        singleInstance.sendToInstance(Commands.showUserThatAppIsRunning)\n        return\n    }\n    if (AppInfo.isInIDE()) {\n        println(\"app version ${AppVersion.get()} is started\")\n        println(\"it seems we are in ide\")\n    }\n\n    val globalExceptionHandler = createAndSetGlobalExceptionHandler()\n    App().use {\n        it.start(\n            appArguments = appArguments,\n            globalAppExceptionHandler = globalExceptionHandler,\n            singleInstanceServerHandler = singleInstanceServerHandler,\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppArguments.kt",
    "content": "package com.abdownloadmanager.desktop\n\ndata class AppArguments(\n    val getIntegrationPort: Boolean,\n    val startIfNotStarted: Boolean,\n    val startSilent: Boolean,\n    val debug: Boolean,\n    val version: Boolean,\n    val exit: Boolean,\n) {\n    companion object {\n        private lateinit var instance: AppArguments\n        fun get() = instance\n\n        /**\n         * Initial me on app startup\n         */\n        fun init(args: Array<String>) {\n            instance = create(args)\n        }\n\n        private fun create(args: Array<String>): AppArguments {\n            return AppArguments(\n                getIntegrationPort = args.contains(Args.GET_INTEGRATION_PORT),\n                startIfNotStarted = args.contains(Args.START_IF_NOT_STARTED),\n                startSilent = args.contains(Args.BACKGROUND),\n                debug = args.contains(Args.DEBUG),\n                version = args.contains(Args.VERSION),\n                exit = args.contains(Args.EXIT),\n            )\n        }\n    }\n\n    object Args {\n        const val START_IF_NOT_STARTED = \"--start-if-not-started\"\n        const val BACKGROUND = \"--background\"\n        const val GET_INTEGRATION_PORT = \"--get-integration-port\"\n        const val DEBUG = \"--debug\"\n        const val VERSION = \"--version\"\n        const val EXIT = \"--exit\"\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt",
    "content": "package com.abdownloadmanager.desktop\n\nimport com.abdownloadmanager.UpdateManager\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadConfig\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.desktop.pages.addDownload.multiple.DesktopAddMultiDownloadComponent\nimport com.abdownloadmanager.desktop.pages.addDownload.single.DesktopAddSingleDownloadComponent\nimport com.abdownloadmanager.desktop.pages.batchdownload.DesktopBatchDownloadComponent\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.desktop.pages.category.DesktopCategoryDialogManager\nimport com.abdownloadmanager.desktop.pages.editdownload.DesktopEditDownloadComponent\nimport com.abdownloadmanager.desktop.pages.enterurl.DesktopEnterNewURLComponent\nimport com.abdownloadmanager.desktop.pages.checksum.DesktopFileChecksumComponent\nimport com.abdownloadmanager.desktop.pages.home.HomeComponent\nimport com.abdownloadmanager.desktop.pages.perhostsettings.DesktopPerHostSettingsComponent\nimport com.abdownloadmanager.desktop.pages.queue.QueuesComponent\nimport com.abdownloadmanager.desktop.pages.settings.DesktopSettingsComponent\nimport com.abdownloadmanager.desktop.pages.poweractionalert.PowerActionComponent\nimport com.abdownloadmanager.desktop.pages.singleDownloadPage.DesktopSingleDownloadComponent\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.desktop.storage.AppSettingsStorage\nimport com.abdownloadmanager.desktop.storage.DesktopExtraDownloadItemSettings\nimport com.abdownloadmanager.desktop.storage.PageStatesStorage\nimport com.abdownloadmanager.desktop.ui.widget.MessageDialogModel\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.ui.widget.NotificationModel\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.desktop.utils.*\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.children.ChildNavState\nimport com.arkivanov.decompose.router.pages.Pages\nimport com.arkivanov.decompose.router.pages.PagesNavigation\nimport com.arkivanov.decompose.router.pages.childPages\nimport com.arkivanov.decompose.router.pages.navigate\nimport com.arkivanov.decompose.router.slot.*\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.downloaditem.contexts.ResumedBy\nimport ir.amirab.downloader.downloaditem.contexts.User\nimport ir.amirab.downloader.queue.DefaultQueueInfo\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport com.abdownloadmanager.integration.Integration\nimport com.abdownloadmanager.integration.IntegrationResult\nimport com.abdownloadmanager.resources.*\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pagemanager.AboutPageManager\nimport com.abdownloadmanager.shared.pagemanager.AddDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.BatchDownloadPageManager\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EnterNewURLDialogManager\nimport com.abdownloadmanager.shared.pagemanager.ExitApplicationRequestManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.OpenSourceLibrariesPageManager\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pagemanager.SettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.TranslatorsPageManager\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.CategorySelectionMode\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.childContext\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.destination.IncompleteFileUtil\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.exception.TooManyErrorException\nimport ir.amirab.downloader.monitor.isDownloadActiveFlow\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.combineStringSources\nimport ir.amirab.util.coroutines.launchWithDeferred\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.osfileutil.FileUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.Serializable\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport java.awt.Toolkit\nimport kotlin.system.exitProcess\n\nsealed interface AppEffects {\n    data class SimpleNotificationNotification(\n        val notificationModel: NotificationModel,\n    ) : AppEffects\n}\n\nclass AppComponent(\n    ctx: ComponentContext,\n) : BaseComponent(ctx),\n    DesktopDownloadDialogManager,\n    DesktopAddDownloadDialogManager,\n    DesktopCategoryDialogManager,\n    EditDownloadDialogManager,\n    FileChecksumDialogManager,\n    QueuePageManager,\n    NotificationSender,\n    DownloadItemOpener,\n    PerHostSettingsPageManager,\n    PowerActionManager,\n    EnterNewURLDialogManager,\n    SettingsPageManager,\n    OpenSourceLibrariesPageManager,\n    TranslatorsPageManager,\n    AboutPageManager,\n    BatchDownloadPageManager,\n    ExitApplicationRequestManager,\n    ContainsEffects<AppEffects> by supportEffects(),\n    KoinComponent {\n    val applicationScope: CoroutineScope by inject()\n    val appRepository: AppRepository by inject()\n    val appSettings: AppSettingsStorage by inject()\n    val downloaderInUiRegistry: DownloaderInUiRegistry by inject()\n    private val queueManager: QueueManager by inject()\n    private val defaultCategories: DefaultCategories by inject()\n    private val integration: Integration by inject()\n    private val perHostSettingsManager: PerHostSettingsManager by inject()\n    val iconFromUriResolver: IIconResolver by inject()\n    val updaterManager: UpdateManager by inject()\n    val extraDownloadSettingStorage: ExtraDownloadSettingsStorage<DesktopExtraDownloadItemSettings> by inject()\n    val useSystemTray = appSettings.useSystemTray\n    fun openHome() {\n        scope.launch {\n            showHomeSlot.value.child?.instance.let {\n                if (it != null) {\n                    it.bringToFront()\n                } else {\n                    showHome.activate(HomePageConfig())\n                }\n            }\n        }\n    }\n\n    fun activateHomeIfNotOpen() {\n        scope.launch {\n            showHomeSlot.value.child?.instance.let {\n                if (it == null) {\n                    showHome.activate(HomePageConfig())\n                }\n            }\n        }\n    }\n\n    fun closeHome() {\n        scope.launch {\n            showHome.dismiss()\n        }\n    }\n\n    @Serializable\n    class HomePageConfig\n\n    private val showHome = SlotNavigation<HomePageConfig>()\n    val showHomeSlot = childSlot(\n        showHome,\n        serializer = null,\n        key = \"home\",\n        childFactory = { _: HomePageConfig, componentContext: ComponentContext ->\n            HomeComponent(\n                ctx = componentContext,\n                downloadItemOpener = this,\n                downloadDialogManager = this,\n                enterNewURLDialogManager = this,\n                desktopAddDownloadDialogManager = this,\n                fileChecksumDialogManager = this,\n                categoryDialogManager = this,\n                notificationSender = this,\n                editDownloadDialogManager = this,\n                queuePageManager = this,\n                categoryManager = categoryManager,\n                downloadSystem = downloadSystem,\n                queueManager = queueManager,\n                defaultCategories = defaultCategories,\n                fileIconProvider = fileIconProvider,\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    class QueuePageConfig(\n        val selectedQueue: Long? = null\n    )\n\n    private val showQueues = SlotNavigation<QueuePageConfig>()\n    val showQueuesSlot = childSlot(\n        showQueues,\n        serializer = null,\n        key = \"queues\",\n        childFactory = { config: QueuePageConfig, componentContext: ComponentContext ->\n            QueuesComponent(componentContext, this::closeQueues).apply {\n                config.selectedQueue?.let {\n                    onQueueSelected(it)\n                }\n            }\n        }\n    ).subscribeAsStateFlow()\n\n    class BatchDownloadConfig\n\n    private val batchDownload = SlotNavigation<BatchDownloadConfig>()\n    val batchDownloadSlot = childSlot(\n        batchDownload,\n        serializer = null,\n        key = \"batchDownload\",\n        childFactory = { _: BatchDownloadConfig, componentContext: ComponentContext ->\n            DesktopBatchDownloadComponent(\n                ctx = componentContext,\n                onClose = this::closeBatchDownload,\n                importLinks = {\n                    openAddDownloadDialog(\n                        it.mapNotNull {\n                            downloaderInUiRegistry\n                                .bestMatchForThisLink(it)\n                                ?.createMinimumCredentials(it)\n                                ?.let { credentials ->\n                                    AddDownloadCredentialsInUiProps(\n                                        credentials = credentials,\n                                    )\n                                }\n                        }\n\n                    )\n                }\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    private val editDownload = SlotNavigation<Long>()\n    val editDownloadSlot = childSlot(\n        editDownload,\n        serializer = null,\n        key = \"editDownload\",\n        childFactory = { editDownloadConfig: Long, componentContext: ComponentContext ->\n            DesktopEditDownloadComponent(\n                ctx = componentContext,\n                onRequestClose = {\n                    closeEditDownloadDialog()\n                },\n                onEdited = { updater, downloadJobExtraConfig ->\n                    scope.launch {\n                        downloadSystem.editDownload(\n                            id = editDownloadConfig,\n                            applyUpdate = updater,\n                            downloadJobExtraConfig = downloadJobExtraConfig\n                        )\n                        closeEditDownloadDialog()\n                    }\n                },\n                downloadId = editDownloadConfig,\n                acceptEdit = downloadSystem.downloadMonitor\n                    .isDownloadActiveFlow(editDownloadConfig)\n                    .mapStateFlow { !it },\n                downloadSystem = downloadSystem,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n                iconProvider = fileIconProvider,\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override fun openEditDownloadDialog(id: Long) {\n        val currentComponent = editDownloadSlot.value.child?.instance\n        if (currentComponent != null && currentComponent.downloadId == id) {\n            currentComponent.bringToFront()\n        } else {\n            editDownload.activate(id)\n        }\n    }\n\n    override fun closeEditDownloadDialog() {\n        editDownload.dismiss()\n    }\n\n    override fun openSettings() {\n        scope.launch {\n            showSettingSlot.value.child?.instance.let {\n                if (it != null) {\n                    it.toFront()\n                } else {\n                    showSettingWindow.activate(AppSettingPageConfig())\n                }\n\n            }\n        }\n    }\n\n    override fun closeSettings() {\n        scope.launch {\n            showSettingWindow.dismiss()\n        }\n    }\n\n    class AppSettingPageConfig\n\n    val showSettingWindow = SlotNavigation<AppSettingPageConfig>()\n    val showSettingSlot = childSlot(\n        showSettingWindow,\n        serializer = null,\n        key = \"settings\",\n        childFactory = { configuration: AppSettingPageConfig, componentContext: ComponentContext ->\n            DesktopSettingsComponent(\n                componentContext,\n                this\n            )\n        }\n    ).subscribeAsStateFlow()\n    private val pageStatesStorage: PageStatesStorage by inject()\n\n    val downloadSystem: DownloadSystem by inject()\n    private val fileIconProvider: FileIconProvider by inject()\n    private val addDownloadPageControl = PagesNavigation<AddDownloadConfig>()\n    val _openedAddDownloadDialogs = childPages(\n        key = \"openedAddDownloadDialogs\",\n        source = addDownloadPageControl,\n        serializer = null,\n        initialPages = { Pages() },\n\n        pageStatus = { _, _ ->\n            ChildNavState.Status.RESUMED\n        },\n        childFactory = { config, ctx ->\n            val component: AddDownloadComponent = when (config) {\n                is AddDownloadConfig.SingleAddConfig -> {\n                    DesktopAddSingleDownloadComponent(\n                        ctx = ctx,\n                        onRequestClose = {\n                            closeAddDownloadDialog(config.id)\n                        },\n                        onRequestAddToQueue = { item, queueId, categoryId ->\n                            addDownload(\n                                item = item,\n                                queueId = queueId,\n                                categoryId = categoryId,\n                            )\n                        },\n                        categoryDialogManager = this,\n                        onRequestDownload = { item, categoryId ->\n                            startNewDownload(\n                                item = item,\n                                categoryId = categoryId,\n                            )\n                        },\n                        openExistingDownload = {\n                            openDownloadDialog(it)\n                        },\n                        downloadItemOpener = this,\n                        updateExistingDownloadCredentials = { id, newCredentials, downloadJobExtraConfig ->\n                            scope.launch {\n                                downloadSystem.downloadManager.updateDownloadItem(\n                                    id = id,\n                                    downloadJobExtraConfig = downloadJobExtraConfig,\n                                    updater = {\n                                        it.withCredentials(newCredentials)\n                                    }\n                                )\n                                openDownloadDialog(id)\n                            }\n                        },\n\n                        id = config.id,\n                        importOptions = config.importOptions,\n                        initialCredentials = config.newDownload,\n                        downloaderInUi = requireNotNull(\n                            downloaderInUiRegistry.getDownloaderOf(config.newDownload.credentials)\n                        ),\n                        lastSavedLocationsStorage = pageStatesStorage,\n                        appScope = applicationScope,\n                        appSettings = appSettings,\n                        appRepository = appRepository,\n                        perHostSettingsManager = perHostSettingsManager,\n                        downloadSystem = downloadSystem,\n                        iconProvider = fileIconProvider,\n                        categoryManager = categoryManager,\n                        queueManager = queueManager,\n                    )\n                }\n\n                is AddDownloadConfig.MultipleAddConfig -> {\n                    DesktopAddMultiDownloadComponent(\n                        ctx = ctx,\n                        id = config.id,\n                        onRequestClose = { closeAddDownloadDialog(config.id) },\n                        onRequestAdd = { items, queueId, categorySelectionMode ->\n                            addDownloads(\n                                items = items,\n                                queueId = queueId,\n                                categorySelectionMode = categorySelectionMode\n                            )\n                        },\n                        lastSavedLocationsStorage = pageStatesStorage,\n                        perHostSettingsManager = perHostSettingsManager,\n                        downloadSystem = downloadSystem,\n                        fileIconProvider = fileIconProvider,\n                        appRepository = appRepository,\n                        downloaderInUiRegistry = downloaderInUiRegistry,\n                        queueManager = queueManager,\n                        categoryManager = categoryManager,\n                        categoryDialogManager = this,\n                    ).apply { addItems(config.newDownloads) }\n                }\n\n                else -> error(\"should not happened\")\n            }\n            component\n        }\n    ).subscribeAsStateFlow()\n    override val openedAddDownloadDialogs = _openedAddDownloadDialogs.map {\n        it.items.mapNotNull { it.instance }\n    }\n        .stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    private val downloadDialogControl = PagesNavigation<DesktopSingleDownloadComponent.Config>()\n\n    private val _openedDownloadDialogs = childPages(\n        key = \"openedDownloadDialogs\",\n        source = downloadDialogControl,\n        serializer = null,\n        initialPages = { Pages() },\n        pageStatus = { _, _ ->\n            ChildNavState.Status.RESUMED\n        },\n        childFactory = { cfg, ctx ->\n            DesktopSingleDownloadComponent(\n                ctx = ctx,\n                downloadItemOpener = this,\n                onDismiss = {\n                    closeDownloadDialog(listOf(cfg.id))\n                },\n                downloadId = cfg.id,\n                downloadSystem = downloadSystem,\n                appSettings = appSettings,\n                appRepository = appRepository,\n                applicationScope = applicationScope,\n                fileIconProvider = fileIconProvider,\n                extraDownloadSettingsStorage = extraDownloadSettingStorage,\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override val openedDownloadDialogs = _openedDownloadDialogs\n        .map { it.items.mapNotNull { it.instance } }\n        .stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    private val categoryManager: CategoryManager by inject()\n\n    private val categoryPageControl = PagesNavigation<Long>()\n    private val _openedCategoryDialogs = childPages(\n        key = \"openedCategoryDialogs\",\n        source = categoryPageControl,\n        serializer = null,\n        initialPages = { Pages() },\n        pageStatus = { _, _ ->\n            ChildNavState.Status.RESUMED\n        },\n        childFactory = { cfg, ctx ->\n            CategoryComponent(\n                ctx = ctx,\n                close = {\n                    closeCategoryDialog(cfg)\n                },\n                submit = { submittedCategory ->\n                    if (submittedCategory.id < 0) {\n                        categoryManager.addCustomCategory(submittedCategory)\n                    } else {\n                        categoryManager.updateCategory(\n                            submittedCategory.id\n                        ) {\n                            submittedCategory.copy(\n                                items = it.items\n                            )\n                        }\n                    }\n                    closeCategoryDialog(cfg)\n                },\n                id = cfg\n            )\n        }\n    ).subscribeAsStateFlow()\n    override val openedCategoryDialogs: StateFlow<List<CategoryComponent>> = _openedCategoryDialogs\n        .map {\n            it.items.mapNotNull { it.instance }\n        }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    override fun openCategoryDialog(categoryId: Long) {\n        scope.launch {\n            val component = openedCategoryDialogs.value.find {\n                it.id == categoryId\n            }\n            if (component != null) {\n//                component.bringToFront()\n            } else {\n                categoryPageControl.navigate {\n                    val newItems = (it.items.toSet() + categoryId).toList()\n                    val copy = it.copy(\n                        items = newItems,\n                        selectedIndex = newItems.lastIndex\n                    )\n                    copy\n                }\n            }\n        }\n    }\n\n    override fun closeCategoryDialog(categoryId: Long) {\n        scope.launch {\n            categoryPageControl.navigate {\n                val newItems = it.items.filter { config ->\n                    config != categoryId\n                }\n                it.copy(items = newItems, selectedIndex = newItems.lastIndex)\n            }\n        }\n    }\n    override fun closeCategoryDialog() {\n        scope.launch {\n            categoryPageControl.navigate {\n                Pages()\n            }\n        }\n    }\n\n    init {\n        downloadSystem.downloadEvents\n            .filterIsInstance<DownloadManagerEvents.OnJobRemoved>()\n            .onEach {\n                closeDownloadDialog(listOf(it.downloadItem.id))\n            }.launchIn(scope)\n    }\n\n    override fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType) {\n        beep()\n        showNotification(tag = tag, title = title, description = description, type = type)\n    }\n\n    override fun sendDialogNotification(\n        title: StringSource,\n        description: StringSource,\n        type: MessageDialogType,\n    ) {\n        beep()\n        newDialogMessage(MessageDialogModel(title = title, description = description, type = type))\n    }\n\n    private fun beep() {\n        if (appSettings.notificationSound.value) {\n            Toolkit.getDefaultToolkit().beep()\n        }\n    }\n\n    private fun showNotification(\n        tag: Any,\n        title: StringSource,\n        description: StringSource,\n        type: NotificationType = NotificationType.Info,\n    ) {\n        sendEffect(\n            AppEffects.SimpleNotificationNotification(\n                NotificationModel(\n                    tag = tag,\n                    initialTitle = title,\n                    initialDescription = description,\n                    initialNotificationType = type\n                )\n            )\n        )\n    }\n\n    init {\n        downloadSystem\n            .downloadEvents\n            .onEach {\n                onNewDownloadEvent(it)\n            }\n            .launchIn(scope)\n//        IntegrationPortBroadcaster.cleanOnClose()\n        integration\n            .integrationStatus\n            .onEach {\n                when (it) {\n                    is IntegrationResult.Fail -> {\n                        IntegrationPortBroadcaster.setIntegrationPortInFile(null)\n                        sendDialogNotification(\n                            title = Res.string.cant_run_browser_integration.asStringSource(),\n                            type = MessageDialogType.Error,\n                            description = it.throwable.localizedMessage.asStringSource()\n                        )\n                    }\n\n                    IntegrationResult.Inactive -> {\n                        IntegrationPortBroadcaster.setIntegrationPortInFile(null)\n                    }\n\n                    is IntegrationResult.Success -> {\n                        IntegrationPortBroadcaster.setIntegrationPortInFile(it.port)\n                    }\n                }\n            }.launchIn(scope)\n    }\n\n    private fun onNewDownloadEvent(it: DownloadManagerEvents) {\n        if (it.context[ResumedBy]?.by !is User) {\n            //only notify events that is started by user\n            return\n        }\n//                or\n//                val qm = downloadSystem.queueManager\n//                val queueId = qm.findItemInQueue(it.downloadItem.id)\n//                if (queueId != null) {\n//                    return@onEach\n//                    // skip download events when download is triggered by queue\n////                    if (qm.getQueue(queue).isQueueActive){\n////                      return@onEach\n////                    }\n//                }\n        if (it is DownloadManagerEvents.OnJobCanceled) {\n            val exception = it.e\n            if (ExceptionUtils.isNormalCancellation(exception)) {\n                return\n            }\n            var isMaxTryReachedError = false\n            val actualCause = if (exception is TooManyErrorException) {\n                isMaxTryReachedError = true\n                exception.findActualDownloadErrorCause()\n            } else exception\n            if (ExceptionUtils.isNormalCancellation(actualCause)) {\n                return\n            }\n            val prefix = if (isMaxTryReachedError) {\n                \"Too Many Error: \"\n            } else {\n                \"Error: \"\n            }.asStringSource()\n            val reason = actualCause.message?.asStringSource() ?: Res.string.unknown.asStringSource()\n            sendNotification(\n                \"downloadId=${it.downloadItem.id}\",\n                title = it.downloadItem.name.asStringSource(),\n                description = listOf(prefix, reason).combineStringSources(),\n                type = NotificationType.Error,\n            )\n        }\n        if (it is DownloadManagerEvents.OnJobCompleted) {\n            sendNotification(\n                tag = \"downloadId=${it.downloadItem.id}\",\n                title = it.downloadItem.name.asStringSource(),\n                description = Res.string.finished.asStringSource(),\n                type = NotificationType.Success,\n            )\n            if (appSettings.showDownloadCompletionDialog.value) {\n                openDownloadDialog(it.downloadItem.id)\n            }\n        }\n        if (it is DownloadManagerEvents.OnJobStarting) {\n            if (appSettings.showDownloadProgressDialog.value) {\n                openDownloadDialog(it.downloadItem.id)\n            }\n        }\n    }\n\n    override suspend fun openDownloadItem(id: Long) {\n        val item = downloadSystem.getDownloadItemById(id)\n        if (item == null) {\n            sendNotification(\n                Res.string.open_file,\n                Res.string.cant_open_file.asStringSource(),\n                Res.string.download_item_not_found.asStringSource(),\n                NotificationType.Error,\n            )\n            return\n        }\n        openDownloadItem(item)\n    }\n\n    override suspend fun openDownloadItem(downloadItem: IDownloadItem) {\n        runCatching {\n            withContext(Dispatchers.IO) {\n                FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem))\n            }\n        }.onFailure {\n            sendNotification(\n                Res.string.open_file,\n                Res.string.cant_open_file.asStringSource(),\n                it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),\n                NotificationType.Error,\n            )\n            println(\"Can't open file:${it.message}\")\n        }\n    }\n\n    override suspend fun openDownloadItemFolder(id: Long) {\n        val item = downloadSystem.getDownloadItemById(id)\n        if (item == null) {\n            sendNotification(\n                Res.string.open_folder,\n                Res.string.cant_open_folder.asStringSource(),\n                Res.string.download_item_not_found.asStringSource(),\n                NotificationType.Error,\n            )\n            return\n        }\n        openDownloadItemFolder(item)\n    }\n\n    override suspend fun openDownloadItemFolder(downloadItem: IDownloadItem) {\n        runCatching {\n            withContext(Dispatchers.IO) {\n                val file = downloadSystem.getDownloadFile(downloadItem)\n                if (file.exists()) {\n                    FileUtils.openFolderOfFile(file)\n                } else {\n                    val incompleteFile = IncompleteFileUtil.addIncompleteIndicator(file, downloadItem.id)\n                    if (incompleteFile.exists() && downloadItem.status != DownloadStatus.Completed) {\n                        FileUtils.openFolderOfFile(incompleteFile)\n                    } else {\n                        FileUtils.openFolder(file.parentFile)\n                    }\n                }\n            }\n        }.onFailure {\n            sendNotification(\n                Res.string.open_folder,\n                Res.string.cant_open_folder.asStringSource(),\n                it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),\n                NotificationType.Error,\n            )\n            println(\"Can't open folder:${it.message}\")\n        }\n    }\n\n    fun externalCredentialComingIntoApp(\n        list: List<AddDownloadCredentialsInUiProps>,\n        options: ImportOptions\n    ) {\n        val editDownloadComponent = editDownloadSlot.value.child?.instance\n        if (editDownloadComponent != null) {\n            list.firstOrNull()?.let {\n                editDownloadComponent.importCredential(\n                    it.credentials\n                )\n                editDownloadComponent.bringToFront()\n            }\n        } else {\n            openAddDownloadDialog(list, options)\n        }\n    }\n\n    override fun openAddDownloadDialog(\n        links: List<AddDownloadCredentialsInUiProps>,\n        importOptions: ImportOptions,\n    ) {\n        scope.launch {\n            //remove duplicates\n            val addDownloadCredentialsProps = links.distinctBy {\n                it.credentials\n            }\n            addDownloadPageControl.navigate {\n                val newItems = buildList {\n                    addAll(it.items)\n                    if (addDownloadCredentialsProps.size > 1) {\n                        add(\n                            AddDownloadConfig.MultipleAddConfig(\n                                addDownloadCredentialsProps,\n                                importOptions,\n                            )\n                        )\n                    } else {\n                        add(\n                            AddDownloadConfig.SingleAddConfig(\n                                addDownloadCredentialsProps.first(),\n                                importOptions,\n                            )\n                        )\n                    }\n                }\n                val copy = it.copy(\n                    items = newItems,\n                    selectedIndex = newItems.lastIndex\n                )\n                copy\n            }\n        }\n    }\n\n    override fun closeAddDownloadDialog(dialogId: String) {\n        scope.launch {\n            addDownloadPageControl.navigate {\n                val newItems = it.items.filter { config ->\n                    config.id != dialogId\n                }\n                it.copy(items = newItems, selectedIndex = newItems.lastIndex)\n            }\n        }\n    }\n    override fun closeAddDownloadDialog() {\n        scope.launch {\n            addDownloadPageControl.navigate {\n                Pages()\n            }\n        }\n    }\n\n    override fun openDownloadDialog(id: Long) {\n        scope.launch {\n            val component = openedDownloadDialogs.value.find {\n                it.downloadId == id\n            }\n            if (component != null) {\n                component.bringToFront()\n            } else {\n                downloadDialogControl.navigate {\n                    val newItems = (it.items.toSet() + DesktopSingleDownloadComponent.Config(id)).toList()\n                    val copy = it.copy(\n                        items = newItems,\n                        selectedIndex = newItems.lastIndex\n                    )\n                    copy\n                }\n            }\n\n        }\n    }\n\n    override fun closeDownloadDialog(ids: List<Long>) {\n        scope.launch {\n            downloadDialogControl.navigate {\n                val newItems = it.items.filter { config ->\n                    config.id !in ids\n                }\n                it.copy(items = newItems, selectedIndex = newItems.lastIndex)\n            }\n        }\n    }\n\n    override fun closeDownloadDialog() {\n        scope.launch {\n            downloadDialogControl.navigate {\n                Pages()\n            }\n        }\n    }\n\n    private val fileChecksumPagesControl = SlotNavigation<DesktopFileChecksumComponent.Config>()\n    val openedFileChecksumDialog = childSlot(\n        key = \"openedFileChecksumPage\",\n        source = fileChecksumPagesControl,\n        serializer = null,\n        childFactory = { config, ctx ->\n            DesktopFileChecksumComponent(\n                ctx = ctx,\n                id = config.id,\n                itemIds = config.itemIds,\n                closeComponent = {\n                    closeFileChecksumPage(config.id)\n                },\n                downloadSystem = downloadSystem,\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override fun openFileChecksumPage(ids: List<Long>) {\n        scope.launch {\n            val instance = openedFileChecksumDialog.value.child?.instance\n            if (instance?.itemIds == ids) {\n                instance.bringToFront()\n            } else {\n                fileChecksumPagesControl.navigate {\n                    DesktopFileChecksumComponent.Config(itemIds = ids)\n                }\n            }\n        }\n    }\n\n    override fun closeFileChecksumPage(dialogId: String) {\n        scope.launch {\n            fileChecksumPagesControl.dismiss()\n        }\n    }\n\n    fun addDownloads(\n        items: List<NewDownloadItemProps>,\n        categorySelectionMode: CategorySelectionMode?,\n        queueId: Long?,\n    ): Deferred<List<Long>> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newItemsToAdd = items,\n                queueId = queueId,\n                categorySelectionMode = categorySelectionMode,\n            )\n        }\n    }\n\n    fun addDownload(\n        item: NewDownloadItemProps,\n        queueId: Long?,\n        categoryId: Long?,\n    ): Deferred<Long> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newDownload = item,\n                queueId = queueId,\n                categoryId = categoryId,\n            )\n        }\n    }\n\n    fun startNewDownload(\n        item: NewDownloadItemProps,\n        categoryId: Long?,\n    ): Deferred<Long> {\n        return scope.launchWithDeferred {\n            downloadSystem.addDownload(\n                newDownload = item,\n                queueId = DefaultQueueInfo.ID,\n                categoryId = categoryId,\n            ).also {\n                downloadSystem.userManualResume(it)\n            }\n        }\n    }\n\n    private val _showConfirmExitDialog = MutableStateFlow(false)\n    val showConfirmExitDialog = _showConfirmExitDialog.asStateFlow()\n\n    fun exitAppAsync() {\n        scope.launch { exitApp() }\n    }\n\n    suspend fun exitApp() {\n        downloadSystem.stopAnything()\n        exitProcess(0)\n    }\n\n    fun closeConfirmExit() {\n        _showConfirmExitDialog.value = false\n    }\n\n    override suspend fun requestExitApp() {\n        val hasActiveDownloads = downloadSystem.downloadMonitor.activeDownloadCount.value > 0\n        if (hasActiveDownloads) {\n            _showConfirmExitDialog.value = true\n            return\n        }\n        exitApp()\n    }\n\n    override fun openAboutPage() {\n        showAboutPage.update { true }\n    }\n\n    fun closeAbout() {\n        showAboutPage.update { false }\n    }\n\n    override fun openOpenSourceLibrariesPage() {\n        showOpenSourceLibraries.update { true }\n    }\n\n    fun closeOpenSourceLibraries() {\n        showOpenSourceLibraries.update { false }\n    }\n\n    override fun openTranslatorsPage() {\n        showTranslators.update { true }\n    }\n\n    override fun closeTranslatorsPage() {\n        showTranslators.update { false }\n    }\n\n    override fun openQueues(\n        openQueueId: Long?,\n    ) {\n        scope.launch {\n            showQueuesSlot.value.child?.instance.let {\n                if (it != null) {\n                    it.bringToFront()\n                    if (openQueueId != null) {\n                        it.onQueueSelected(openQueueId)\n                    }\n                } else {\n                    showQueues.activate(\n                        QueuePageConfig(\n                            selectedQueue = openQueueId\n                        )\n                    )\n                }\n            }\n        }\n    }\n\n    override fun closeQueues() {\n        showQueues.dismiss()\n    }\n\n    var showCreateQueueDialog = MutableStateFlow(false)\n        private set\n\n    override fun closeNewQueueDialog() {\n        showCreateQueueDialog.update { false }\n    }\n\n    override fun openNewQueueDialog() {\n        showCreateQueueDialog.update { true }\n    }\n\n    fun createNewQueue(name: String) {\n        scope.launch {\n            downloadSystem.addQueue(name)\n        }\n    }\n\n    override fun openBatchDownloadPage() {\n        scope.launch {\n\n            batchDownloadSlot.value.child?.instance.let {\n                if (it != null) {\n                    it.bringToFront()\n                } else {\n                    batchDownload.activate(BatchDownloadConfig())\n                }\n            }\n        }\n    }\n\n    override fun closeBatchDownload() {\n        batchDownload.dismiss()\n    }\n\n    val enterNewURLWindow = SlotNavigation<DesktopEnterNewURLComponent.Config>()\n    val enterNewURLWindowSlot = childSlot(\n        enterNewURLWindow,\n        serializer = null,\n        key = \"enterNewURLWindow\",\n        childFactory = { configuration: DesktopEnterNewURLComponent.Config, componentContext: ComponentContext ->\n            DesktopEnterNewURLComponent(\n                ctx = componentContext,\n                config = configuration,\n                downloaderInUiRegistry = downloaderInUiRegistry,\n                onCloseRequest = {\n                    closeEnterNewURLWindow()\n                },\n                onRequestFinished = { credentials ->\n                    scope.launch {\n                        openAddDownloadDialog(\n                            links = listOf(\n                                AddDownloadCredentialsInUiProps(\n                                    credentials = credentials\n                                )\n                            ),\n                        )\n                    }\n                }\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override fun openEnterNewURLWindow() {\n        scope.launch {\n            enterNewURLWindowSlot.value.child?.instance.let {\n                if (it != null) {\n                    it.bringToFront()\n                } else {\n                    enterNewURLWindow.activate(\n                        DesktopEnterNewURLComponent.Config\n                    )\n                }\n            }\n        }\n    }\n\n    override fun closeEnterNewURLWindow() {\n        scope.launch {\n            enterNewURLWindow.dismiss()\n        }\n    }\n\n    val dialogMessages: MutableStateFlow<List<MessageDialogModel>> = MutableStateFlow(emptyList())\n    private fun newDialogMessage(msgDialogModel: MessageDialogModel) {\n        dialogMessages.update {\n            it\n                .filter { item -> item.id != msgDialogModel.id }\n                .plus(msgDialogModel)\n        }\n    }\n\n    fun onDismissDialogMessage(msgDialogModel: MessageDialogModel) {\n        dialogMessages.update {\n            it.filter { item ->\n                msgDialogModel.id != item.id\n            }\n        }\n    }\n\n    fun isReady(): Boolean {\n        return listOf(\n            IntegrationPortBroadcaster.isInitialized(),\n        ).all { it }\n    }\n\n    val powerActionNavigation = SlotNavigation<PowerActionComponent.Config>()\n    val openedPowerAction = childSlot(\n        source = powerActionNavigation,\n        key = \"powerAction\",\n        serializer = null,\n        childFactory = { config, ctx ->\n            PowerActionComponent(\n                ctx = ctx,\n                powerActionConfig = config.powerActionConfig,\n                powerActionDelay = config.powerActionDelay,\n                powerActionReason = config.powerActionReason,\n                close = ::dismissPowerAction,\n                onBeforePowerAction = {\n                    downloadSystem.stopAnything()\n                },\n            )\n        }\n    ).subscribeAsStateFlow()\n\n    override fun initiatePowerAction(\n        powerActionConfig: PowerActionConfig,\n        reason: PowerActionComponent.PowerActionReason,\n    ) {\n        scope.launch {\n            powerActionNavigation.activate(\n                PowerActionComponent.Config(\n                    powerActionConfig = powerActionConfig,\n                    powerActionReason = reason,\n                )\n            )\n        }\n    }\n\n    override fun dismissPowerAction() {\n        scope.launch {\n            powerActionNavigation.dismiss()\n        }\n    }\n\n    val updater = UpdateComponent(\n        childContext(\"updater\"),\n        this,\n        updaterManager,\n    )\n\n\n    private val perHostSettings = SlotNavigation<DesktopPerHostSettingsComponent.Config>()\n    val perHostSettingsSlot = childSlot(\n        perHostSettings,\n        serializer = null,\n        key = \"perHostSettings\",\n        childFactory = { cfg: DesktopPerHostSettingsComponent.Config, componentContext: ComponentContext ->\n            DesktopPerHostSettingsComponent(\n                ctx = componentContext,\n                closeRequested = this::closePerHostSettings,\n                appScope = applicationScope,\n                perHostSettingsManager = perHostSettingsManager,\n                appRepository = appRepository,\n            ).apply {\n                cfg.openedHost?.let(this::onHostSelected)\n            }\n        }\n    ).subscribeAsStateFlow()\n\n    override fun openPerHostSettings(\n        openedHost: String?\n    ) {\n        scope.launch {\n            perHostSettingsSlot.value.child?.instance.let { component ->\n                if (component != null) {\n                    component.bringToFront()\n                    openedHost?.let {\n                        component.onHostSelected(it)\n                    }\n                } else {\n                    perHostSettings.activate(DesktopPerHostSettingsComponent.Config(openedHost))\n                }\n            }\n        }\n    }\n\n    override fun closePerHostSettings() {\n        perHostSettings.dismiss { }\n    }\n\n\n    val showAboutPage = MutableStateFlow(false)\n    val showOpenSourceLibraries = MutableStateFlow(false)\n    val showTranslators = MutableStateFlow(false)\n    val theme = appRepository.theme\n    val uiScale = appRepository.uiScale\n}\n\ninterface DesktopDownloadDialogManager : DownloadDialogManager {\n    val openedDownloadDialogs: StateFlow<List<DesktopSingleDownloadComponent>>\n    fun closeDownloadDialog(ids: List<Long>)\n}\n\ninterface DesktopAddDownloadDialogManager : AddDownloadDialogManager {\n    val openedAddDownloadDialogs: StateFlow<List<AddDownloadComponent>>\n    fun closeAddDownloadDialog(dialogId: String)\n}\n\ninterface PowerActionManager {\n    fun initiatePowerAction(\n        powerActionConfig: PowerActionConfig,\n        reason: PowerActionComponent.PowerActionReason,\n    )\n\n    fun dismissPowerAction()\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/SingleInstanceServerInitializer.kt",
    "content": "package com.abdownloadmanager.desktop\n\nimport com.abdownloadmanager.desktop.utils.IntegrationPortBroadcaster\nimport com.abdownloadmanager.desktop.utils.singleInstance.Command\nimport com.abdownloadmanager.desktop.utils.singleInstance.MutableSingleInstanceServerHandler\nimport kotlinx.coroutines.runBlocking\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nobject Commands {\n    val isReady = Command<Boolean>(\"isReady\")\n    val showUserThatAppIsRunning = Command<Unit>(\"showUserThatAppIsRunning\")\n    val getIntegrationPort = Command<Int>(\"getIntegrationPort\")\n    val exit = Command<Unit>(\"exit\")\n}\nobject SingleInstanceServerInitializer:KoinComponent {\n    private val appComponent by inject<AppComponent> ()\n    fun boot(mutableHandler: MutableSingleInstanceServerHandler){\n        mutableHandler.add(Commands.showUserThatAppIsRunning){\n            kotlin.runCatching { appComponent.openHome() }\n        }\n        mutableHandler.add(Commands.getIntegrationPort){\n            IntegrationPortBroadcaster\n                .getIntegrationPort().let { it?:-1 }\n        }\n        mutableHandler.add(Commands.isReady){\n            appComponent.isReady()\n        }\n        mutableHandler.add(Commands.exit) {\n            runBlocking {\n                appComponent.exitApp()\n            }\n        }\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/DesktopActionFactories.kt",
    "content": "package com.abdownloadmanager.desktop.actions\n\nimport com.abdownloadmanager.desktop.DesktopDownloadDialogManager\nimport com.abdownloadmanager.shared.action.createStopAllAction\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.action.AnAction\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.StateFlow\n\n\nfun createDesktopStopAllAction(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    desktopDownloadDialogManager: DesktopDownloadDialogManager,\n    activeQueuesFlow: StateFlow<List<DownloadQueue>>\n): AnAction {\n    return createStopAllAction(\n        scope = scope,\n        downloadSystem = downloadSystem,\n        activeQueuesFlow = activeQueuesFlow,\n        extraJobs = {\n            val activeDownloadIds = downloadSystem.downloadMonitor.activeDownloadListFlow.value.map { it.id }\n            desktopDownloadDialogManager.closeDownloadDialog(activeDownloadIds)\n        }\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/dev.kt",
    "content": "package com.abdownloadmanager.desktop.actions\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.di.Di\nimport com.abdownloadmanager.desktop.pages.poweractionalert.PowerActionComponent\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.action.createDummyExceptionAction\nimport com.abdownloadmanager.shared.action.createDummyMessageAction\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport org.koin.core.component.get\n\nprivate val appComponent = Di.get<AppComponent>()\nval dummyMessage = createDummyMessageAction(appComponent)\nval dummyException = createDummyExceptionAction()\nval shutdown = simpleAction(\n    Res.string.shutdown_now.asStringSource(),\n    MyIcons.exit,\n) {\n    appComponent.initiatePowerAction(\n        PowerActionConfig(PowerActionConfig.Type.Shutdown, false),\n        PowerActionComponent.PowerActionReason.Unknown\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/main.kt",
    "content": "package com.abdownloadmanager.desktop.actions\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.desktop.di.Di\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.desktop.utils.DesktopEntryCreator\nimport com.abdownloadmanager.desktop.utils.isAppInstalled\nimport com.abdownloadmanager.desktop.window.Browser\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport com.abdownloadmanager.shared.util.getIcon\nimport com.abdownloadmanager.shared.util.getName\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.action.createCheckForUpdateAction\nimport com.abdownloadmanager.shared.action.createDownloadFromClipboardAction\nimport com.abdownloadmanager.shared.action.createNewDownloadAction\nimport com.abdownloadmanager.shared.action.createNewQueueAction\nimport com.abdownloadmanager.shared.action.createOpenAboutPage\nimport com.abdownloadmanager.shared.action.createOpenBatchDownloadAction\nimport com.abdownloadmanager.shared.action.createOpenOpenSourceThirdPartyLibrariesPage\nimport com.abdownloadmanager.shared.action.createOpenQueuesAction\nimport com.abdownloadmanager.shared.action.createOpenSettingsAction\nimport com.abdownloadmanager.shared.action.createOpenTranslatorsPageAction\nimport com.abdownloadmanager.shared.action.createPerHostSettingsPage\nimport com.abdownloadmanager.shared.action.createRequestExitAction\nimport com.abdownloadmanager.shared.action.createStartQueueGroupAction\nimport com.abdownloadmanager.shared.action.createStopQueueGroupAction\nimport ir.amirab.downloader.queue.activeQueuesFlow\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.PlatformAppActivator\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\nimport org.koin.core.component.get\n\nprivate val appComponent = Di.get<AppComponent>()\nprivate val scope = Di.get<CoroutineScope>()\nprivate val downloadSystem = appComponent.downloadSystem\n\nprivate val activeQueuesFlow = downloadSystem\n    .queueManager\n    .activeQueuesFlow()\n    .stateIn(\n        scope,\n        SharingStarted.WhileSubscribed(),\n        emptyList()\n    )\n\n// desktop\nval stopAllAction = createDesktopStopAllAction(scope, downloadSystem, appComponent, activeQueuesFlow)\nval newDownloadAction = createNewDownloadAction(appComponent)\nval newDownloadFromClipboardAction = createDownloadFromClipboardAction(appComponent)\nval createDesktopEntryAction = simpleAction(\n    Res.string.create_desktop_entry.asStringSource(),\n    MyIcons.applicationFile,\n    checkEnable = MutableStateFlow(AppInfo.isAppInstalled())\n) {\n    DesktopEntryCreator.createLinuxDesktopEntry()\n}\nval showDownloadList = simpleAction(\n    Res.string.show_downloads.asStringSource(),\n    MyIcons.download,\n) {\n    PlatformAppActivator.active()\n    appComponent.openHome()\n}\nval browserIntegrations = MenuItem.SubMenu(\n    title = Res.string.download_browser_integration.asStringSource(),\n    icon = MyIcons.download,\n    items = buildMenu {\n        for (browserExtension in SharedConstants.browserIntegrations) {\n            item(\n                title = browserExtension.type.getName().asStringSource(),\n                icon = browserExtension.type.getIcon(),\n                onClick = {\n                    val browser = Browser.getBrowserByType(browserExtension.type)\n                    val success = browser?.openLink(browserExtension.url) == true\n                    if (!success) {\n                        URLOpener.openUrl(browserExtension.url)\n                    }\n                }\n            )\n        }\n    }\n)\n\n\n// commonUsage but with desktop implementations\nval newQueueAction = createNewQueueAction(scope, appComponent)\nval openQueuesAction = createOpenQueuesAction(appComponent)\nval openTranslators = createOpenTranslatorsPageAction(appComponent)\nval openAboutAction = createOpenAboutPage(appComponent)\nval checkForUpdateAction = createCheckForUpdateAction(appComponent.updater)\nval gotoSettingsAction = createOpenSettingsAction(appComponent)\nval perHostSettings = createPerHostSettingsPage(appComponent)\nval requestExitAction = createRequestExitAction(scope, appComponent)\nval startQueueGroupAction = createStartQueueGroupAction(scope, appComponent.downloadSystem.queueManager)\nval stopQueueGroupAction = createStopQueueGroupAction(scope, activeQueuesFlow)\nval batchDownloadAction = createOpenBatchDownloadAction(appComponent)\nval openOpenSourceThirdPartyLibraries = createOpenOpenSourceThirdPartyLibrariesPage(appComponent)\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/onevennts/CleanExtraSettingsOnDownloadFinish.kt",
    "content": "package com.abdownloadmanager.desktop.actions.onevennts\n\nimport com.abdownloadmanager.shared.storage.IExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionAction\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\nclass CleanExtraSettingsOnDownloadFinish(\n    private val storage: IExtraDownloadSettingsStorage<*>\n) : OnDownloadCompletionAction {\n    override suspend fun onDownloadCompleted(downloadItem: IDownloadItem) {\n        storage.deleteExtraDownloadItemSettings(downloadItem.id)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/onevennts/getOnDownloadCompletionAction.kt",
    "content": "package com.abdownloadmanager.desktop.actions.onevennts\n\nimport com.abdownloadmanager.desktop.PowerActionManager\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport com.abdownloadmanager.desktop.pages.poweractionalert.PowerActionComponent\nimport com.abdownloadmanager.desktop.storage.DesktopExtraDownloadItemSettings\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionAction\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionProvider\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.getValue\n\nclass DesktopOnDownloadCompletionActionProvider(\n    private val extraDownloadSettingsStorage: ExtraDownloadSettingsStorage<DesktopExtraDownloadItemSettings>,\n) : OnDownloadCompletionActionProvider, KoinComponent {\n    // TODO: BUG\n    // at the moment if I move this to constructor the DI halts\n    // probably due to Circular Dependency Exception\n    // I need to redesign the dependency graph to prevent these sorts of issues!\n    private val powerActionManager: PowerActionManager by inject()\n\n    override suspend fun getOnDownloadCompletionAction(downloadItem: IDownloadItem): List<OnDownloadCompletionAction> {\n        val downloadId = downloadItem.id\n        val extraDownloadItemSettings = extraDownloadSettingsStorage.getExtraDownloadItemSettings(downloadId)\n        return buildList {\n            extraDownloadItemSettings.getPowerActionConfigOnFinish()?.let {\n                add(PowerActionOnDownloadFinish(powerActionManager, it))\n            }\n            add(\n                CleanExtraSettingsOnDownloadFinish(extraDownloadSettingsStorage)\n            )\n        }\n    }\n}\n\nclass PowerActionOnDownloadFinish(\n    val powerActionManager: PowerActionManager,\n    val powerActionConfig: PowerActionConfig,\n) : OnDownloadCompletionAction {\n    override suspend fun onDownloadCompleted(downloadItem: IDownloadItem) {\n        powerActionManager.initiatePowerAction(\n            powerActionConfig,\n            PowerActionComponent.PowerActionReason.DownloadFinished,\n        )\n    }\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/onevennts/getOnQueueEventActions.kt",
    "content": "package com.abdownloadmanager.desktop.actions.onevennts\n\nimport com.abdownloadmanager.desktop.PowerActionManager\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport com.abdownloadmanager.desktop.pages.poweractionalert.PowerActionComponent\nimport com.abdownloadmanager.desktop.storage.DesktopExtraQueueSettings\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueCompletionActionProvider\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueEventAction\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.getValue\n\nclass DesktopOnQueueEventActionProvider(\n    private val desktopExtraQueueSettingsStorage: IExtraQueueSettingsStorage<DesktopExtraQueueSettings>,\n) : OnQueueCompletionActionProvider, KoinComponent {\n    // TODO: BUG\n    // at the moment if I move this to constructor the DI halts\n    // probably due to Circular Dependency but no exception is thrown\n    // I need to redesign the dependency graph to prevent these sorts of issues!\n    private val powerActionManager: PowerActionManager by inject()\n\n    override suspend fun getOnQueueEventActions(queueId: Long): List<OnQueueEventAction> {\n        return desktopExtraQueueSettingsStorage.getExtraQueueSettings(queueId).let {\n            buildList {\n                it.getPowerActionConfigOnFinish()?.let { powerAction ->\n                    add(\n                        PowerActionOnQueueFinishOrTimeEnd(\n                            powerActionManager,\n                            powerAction,\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\nclass PowerActionOnQueueFinishOrTimeEnd(\n    private val powerActionManager: PowerActionManager,\n    private val powerActionConfig: PowerActionConfig,\n) : OnQueueEventAction {\n    override suspend fun onQueueCompleted(queueId: Long) {\n        powerActionManager.initiatePowerAction(\n            powerActionConfig,\n            PowerActionComponent.PowerActionReason.QueueWorkFinished\n        )\n    }\n\n    override suspend fun onQueueEndTimeReached(queueId: Long) {\n        powerActionManager.initiatePowerAction(\n            powerActionConfig,\n            PowerActionComponent.PowerActionReason.QueueEndTimeReached\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt",
    "content": "package com.abdownloadmanager.desktop.di\n\nimport com.abdownloadmanager.github.GithubApi\nimport com.abdownloadmanager.UpdateDownloadLocationProvider\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.desktop.DesktopAddDownloadDialogManager\nimport com.abdownloadmanager.desktop.AppArguments\nimport com.abdownloadmanager.integration.IntegrationHandler\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.DesktopDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.desktop.PowerActionManager\nimport com.abdownloadmanager.desktop.actions.onevennts.DesktopOnDownloadCompletionActionProvider\nimport com.abdownloadmanager.desktop.actions.onevennts.DesktopOnQueueEventActionProvider\nimport com.abdownloadmanager.desktop.integration.IntegrationHandlerImp\nimport com.abdownloadmanager.desktop.pages.category.DesktopCategoryDialogManager\nimport com.abdownloadmanager.desktop.pages.settings.FontManager\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport ir.amirab.downloader.queue.QueueManager\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.desktop.storage.*\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\nimport com.abdownloadmanager.desktop.utils.*\nimport com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging\nimport com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier\nimport com.abdownloadmanager.desktop.utils.proxy.AutoConfigurableProxyProviderForDesktop\nimport com.abdownloadmanager.desktop.utils.proxy.DesktopSystemProxySelectorProvider\nimport com.abdownloadmanager.desktop.utils.proxy.ProxyCachingConfig\nimport com.abdownloadmanager.desktop.utils.renderapi.CustomRenderApi\nimport com.abdownloadmanager.integration.HLSDownloadCredentialsFromIntegration\nimport com.abdownloadmanager.integration.HttpDownloadCredentialsFromIntegration\nimport com.abdownloadmanager.integration.IDownloadCredentialsFromIntegration\nimport com.arkivanov.decompose.DefaultComponentContext\nimport com.arkivanov.essenty.lifecycle.LifecycleRegistry\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.DownloadSettings\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.OkHttpHttpDownloaderClient\nimport ir.amirab.downloader.db.*\nimport ir.amirab.downloader.monitor.DownloadMonitor\nimport ir.amirab.downloader.utils.IDiskStat\nimport com.abdownloadmanager.integration.Integration\nimport com.abdownloadmanager.resources.ABDMLanguageResources\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSDownloaderInUi\nimport com.abdownloadmanager.shared.downloaderinui.http.HttpDownloaderInUi\nimport com.abdownloadmanager.shared.pagemanager.SettingsPageManager\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.storage.PerHostSettingsDatastoreStorage\nimport com.abdownloadmanager.shared.storage.ProxyDatastoreStorage\nimport com.abdownloadmanager.shared.ui.theme.ThemeSettingsStorage\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.updater.UpdateDownloaderViaDownloadSystem\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.DefinedPaths\nimport com.abdownloadmanager.shared.util.DesktopDiskStat\nimport com.abdownloadmanager.shared.util.DesktopSystemThemeDetector\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.UserAgentProviderFromSettings\nimport com.abdownloadmanager.shared.util.*\nimport com.abdownloadmanager.updateapplier.DesktopDirectLinkUpdateApplier\nimport com.abdownloadmanager.updateapplier.UpdateApplier\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.util.config.datastore.createMapConfigDatastore\nimport kotlinx.coroutines.*\nimport kotlinx.serialization.json.Json\nimport okhttp3.Dispatcher\nimport okhttp3.OkHttpClient\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.context.startKoin\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\nimport com.abdownloadmanager.updatechecker.GithubUpdateChecker\nimport com.abdownloadmanager.updatechecker.UpdateChecker\nimport ir.amirab.util.AppVersionTracker\nimport com.abdownloadmanager.shared.util.appinfo.PreviousVersion\nimport com.abdownloadmanager.shared.util.autoremove.RemovedDownloadsFromDiskTracker\nimport com.abdownloadmanager.shared.util.category.*\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionProvider\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionRunner\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueEventActionRunner\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueCompletionActionProvider\nimport com.abdownloadmanager.shared.util.perhostsettings.IPerHostSettingsStorage\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.ui.IMyIcons\nimport com.abdownloadmanager.shared.util.proxy.IProxyStorage\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport com.arkivanov.essenty.lifecycle.Lifecycle\nimport ir.amirab.downloader.DownloaderRegistry\nimport ir.amirab.downloader.connection.UserAgentProvider\nimport ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider\nimport ir.amirab.downloader.connection.proxy.ProxyStrategyProvider\nimport ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloader\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloader\nimport ir.amirab.downloader.monitor.DownloadItemStateFactory\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.queue.ManualDownloadQueue\nimport ir.amirab.downloader.utils.EmptyFileCreator\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.compose.localizationmanager.LanguageSourceProvider\nimport ir.amirab.util.compose.localizationmanager.LanguageStorage\nimport ir.amirab.util.config.datastore.kotlinxSerializationDataStore\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.startup.AbstractStartupManager\nimport ir.amirab.util.startup.Startup\nimport kotlinx.serialization.modules.SerializersModule\nimport kotlinx.serialization.modules.polymorphic\nimport okhttp3.Protocol\nimport okhttp3.internal.tls.OkHostnameVerifier\n\nval downloaderModule = module {\n    single<IDownloadQueueDatabase> {\n        val definedPaths = get<DefinedPaths>()\n\n        DownloadQueueFileStorageDatabase(\n            queueFolder = get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.queuesDir\n            ),\n            fileSaver = get(),\n        )\n    }\n    single<IDownloadListDb> {\n        val definedPaths = get<DefinedPaths>()\n        DownloadListFileStorage(\n            downloadListFolder = get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.downloadListDir\n            ),\n            fileSaver = get(),\n        )\n    }\n    single {\n        TransactionalFileSaver(get())\n    }\n    single<IDownloadPartListDb> {\n        val definedPaths = get<DefinedPaths>()\n        PartListFileStorage(\n            get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.partsDir\n            ),\n            get()\n        )\n    }\n    single<IDiskStat> {\n        DesktopDiskStat()\n    }\n    single<ISystemThemeDetector> {\n        DesktopSystemThemeDetector()\n    }\n    single {\n        QueueManager(get(), get())\n    }\n    single {\n        DownloadFoldersRegistry()\n    }\n    single {\n        DownloadSettings(\n            8,\n        )\n    }\n    single {\n        ProxyManager(\n            get()\n        )\n    }.bind<ProxyStrategyProvider>()\n    single {\n        ProxyCachingConfig.default()\n    }\n    single<AutoConfigurableProxyProvider> {\n        AutoConfigurableProxyProviderForDesktop(get())\n    }\n    single<SystemProxySelectorProvider> {\n        DesktopSystemProxySelectorProvider(get())\n    }\n    single<UserAgentProvider> {\n        UserAgentProviderFromSettings(get())\n    }\n    single<HttpDownloaderClient> {\n        OkHttpHttpDownloaderClient(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }\n    single {\n        val downloadSettings: DownloadSettings = get()\n        EmptyFileCreator(\n            diskStat = get(),\n            useSparseFile = { downloadSettings.useSparseFileAllocation }\n        )\n    }\n    single {\n        HLSDownloader(inject())\n    }\n    single {\n        HLSDownloaderInUi(get(), get())\n    }\n    single {\n        HttpDownloader(inject())\n    }\n    single {\n        HttpDownloaderInUi(get(), get())\n    }\n    single {\n        DownloaderInUiRegistry().apply {\n            add(get<HttpDownloaderInUi>())\n            add(get<HLSDownloaderInUi>())\n        }\n    }.bind<DownloadItemStateFactory<IDownloadItem, DownloadJob>>()\n    single {\n        DownloaderRegistry().apply {\n            add(get<HttpDownloader>())\n            add(get<HLSDownloader>())\n        }\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        DownloadManager(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get<DownloadFoldersRegistry>().registerAndGet(\n                definedPaths.downloadDataDir\n            )\n        )\n    }.bind(DownloadManagerMinimalControl::class)\n    single {\n        ManualDownloadQueue(get(), get())\n    }\n    single<IDownloadMonitor> {\n        DownloadMonitor(\n            downloadManager = get(),\n            manualDownloadQueue = get(),\n            downloadItemStateFactory = inject(),\n        )\n    }\n}\nval downloadSystemModule = module {\n    single {\n        val definedPaths = get<DefinedPaths>()\n        get<DownloadFoldersRegistry>().registerAndGet(definedPaths.categoriesDir)\n        CategoryFileStorage(\n            file = definedPaths.categoriesFile.toFile(),\n            fileSaver = get()\n        )\n    }.bind<CategoryStorage>()\n    single {\n        FileIconProviderUsingCategoryIcons(\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }.bind<FileIconProvider>()\n    single {\n        DefaultCategories(\n            icons = get(),\n            getDefaultDownloadFolder = {\n                get<AppSettingsStorage>().defaultDownloadFolder.value\n            }\n        )\n    }\n    single {\n        DownloadManagerCategoryItemProvider(get())\n    }.bind<ICategoryItemProvider>()\n    single {\n        CategoryManager(\n            categoryStorage = get(),\n            scope = get(),\n            defaultCategoriesFactory = get(),\n            categoryItemProvider = get(),\n        )\n    }\n\n    single {\n        DownloadSystem(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        val extraDownloadSettingsStorageFolder = get<DownloadFoldersRegistry>().registerAndGet(\n            definedPaths.extraDownloadSettings\n        )\n        ExtraDownloadSettingsStorage(\n            extraDownloadSettingsStorageFolder,\n            get(),\n            DesktopExtraDownloadItemSettings\n        )\n    }.bind<IExtraDownloadSettingsStorage<*>>()\n    single {\n        val definedPaths = get<DefinedPaths>()\n        val extraQueueSettingsStorageFolder = get<DownloadFoldersRegistry>().registerAndGet(\n            definedPaths.extraQueueSettings\n        )\n        ExtraQueueSettingsStorage(\n            extraQueueSettingsStorageFolder,\n            get(),\n            DesktopExtraQueueSettings\n        )\n    }.apply {\n        bind<IExtraQueueSettingsStorage<*>>()\n    }\n    single<OnDownloadCompletionActionProvider> {\n        DesktopOnDownloadCompletionActionProvider(get())\n    }\n    single<OnQueueCompletionActionProvider> {\n        DesktopOnQueueEventActionProvider(get())\n    }\n    single {\n        OnDownloadCompletionActionRunner(\n            downloadManagerMinimalControl = get(),\n            scope = get(),\n            onDownloadCompletionActionProvider = get(),\n        )\n    }\n    single {\n        OnQueueEventActionRunner(\n            queueManager = get(),\n            scope = get(),\n            onQueueCompletionActionProvider = get(),\n        )\n    }\n}\nval coroutineModule = module {\n    single {\n        CoroutineScope(SupervisorJob())\n    }\n}\nval jsonModule = module {\n    single {\n        val downloaderRegistry: DownloaderRegistry by inject()\n        Json {\n            this.encodeDefaults = true\n            this.prettyPrint = true\n            this.ignoreUnknownKeys = true\n            this.serializersModule = SerializersModule {\n                polymorphic(IDownloadItem::class) {\n                    downloaderRegistry.getAll().forEach {\n                        subclass(it.downloadItemClass, it.downloadItemSerializer)\n                    }\n                    defaultDeserializer {\n                        HttpDownloadItem.serializer()\n                    }\n                }\n                polymorphic(IDownloadCredentials::class) {\n                    downloaderRegistry.getAll().forEach {\n                        subclass(it.downloadCredentialsClass, it.downloadCredentialsSerializer)\n                    }\n                    defaultDeserializer {\n                        HttpDownloadCredentials.serializer()\n                    }\n                }\n                // TODO remove this later\n                polymorphic(IDownloadCredentialsFromIntegration::class) {\n                    subclass(\n                        HttpDownloadCredentialsFromIntegration::class,\n                        HttpDownloadCredentialsFromIntegration.serializer()\n                    )\n                    subclass(\n                        HLSDownloadCredentialsFromIntegration::class,\n                        HLSDownloadCredentialsFromIntegration.serializer()\n                    )\n                    defaultDeserializer {\n                        HttpDownloadCredentialsFromIntegration.serializer()\n                    }\n                }\n            }\n        }\n    }\n}\nval integrationModule = module {\n    single<IntegrationHandler> {\n        IntegrationHandlerImp()\n    }\n    single {\n        Integration(get(), get(), get(), AppInfo.isInDebugMode())\n    }\n}\nval updaterModule = module {\n    single {\n        val definedPaths = get<DefinedPaths>()\n        UpdateDownloadLocationProvider {\n            definedPaths.updateDownloadLocation.toFile()\n        }\n    }\n    single<UpdateApplier> {\n        val definedPaths = get<DefinedPaths>()\n        definedPaths.updateDownloadLocation\n        DesktopDirectLinkUpdateApplier(\n            installationFolder = AppInfo.installationFolder,\n            updateFolder = definedPaths.updateDir.toString(),\n            logDir = definedPaths.logDir.toString(),\n            appName = AppInfo.name,\n            updatePreparer = UpdateDownloaderViaDownloadSystem(\n                get(),\n                get(),\n            ),\n        )\n    }\n    single<UpdateChecker> {\n        GithubUpdateChecker(\n            AppVersion.get(),\n            githubApi = GithubApi(\n                owner = SharedConstants.projectGithubOwner,\n                repo = SharedConstants.projectGithubRepo,\n                client = OkHttpClient\n                    .Builder()\n                    .build()\n            )\n        )\n    }\n    single {\n        UpdateManager(\n            updateChecker = get(),\n            updateApplier = get(),\n            appVersionTracker = get(),\n        )\n    }\n}\nval startUpModule = module {\n    single {\n        Startup.getStartUpManagerForDesktop(\n            name = AppInfo.displayName,\n            path = AppInfo.exeFile,\n            args = listOf(AppArguments.Args.BACKGROUND),\n            packageName = AppInfo.packageName,\n        )\n    }.apply {\n        bind<AbstractStartupManager>()\n    }\n}\nval nativeMessagingModule = module {\n    single<NativeMessaging> {\n        NativeMessaging(NativeMessagingManifestApplier.getForCurrentPlatform())\n    }\n}\n\nval appModule = module {\n    includes(downloaderModule)\n    includes(downloadSystemModule)\n    includes(coroutineModule)\n    includes(jsonModule)\n    includes(integrationModule)\n    includes(updaterModule)\n    includes(startUpModule)\n    includes(nativeMessagingModule)\n//    single {\n//        NetworkChecker(get())\n//    }\n    single {\n        AppInfo.definedPaths\n    }.bind<DefinedPaths>()\n    single {\n        AppRepository(\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n            get(),\n        )\n    }.apply {\n        bind<BaseAppRepository>()\n        bind<SizeAndSpeedUnitProvider>()\n    }\n    single {\n        ThemeManager(get(), get(), get())\n    }\n    single {\n        FontManager(get())\n    }\n    single {\n        LanguageManager(\n            get(),\n            LanguageSourceProvider(\n                ABDMLanguageResources.defaultLanguageResource,\n                ABDMLanguageResources.languages,\n            )\n        )\n    }\n    single {\n        MyIcons\n    }.apply {\n        bind<IMyIcons>()\n        bind<IIconResolver>()\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        ProxyDatastoreStorage(\n            kotlinxSerializationDataStore(\n                definedPaths.proxySettingsFile.toFile(),\n                get(),\n                ProxyData::default,\n            )\n        )\n    }.bind<IProxyStorage>()\n    single {\n        val definedPaths = get<DefinedPaths>()\n        AppSettingsStorage(\n            createMapConfigDatastore(\n                definedPaths.appSettingsFile.toFile(),\n                get(),\n            )\n        )\n    }.apply {\n        bind<BaseAppSettingsStorage>()\n        bind<LanguageStorage>()\n        bind<ThemeSettingsStorage>()\n    }\n    single {\n        val definedPaths = get<DesktopDefinedPaths>()\n        PageStatesStorage(\n            createMapConfigDatastore(\n                definedPaths.pageStatesStorageFile.toFile(),\n                get(),\n            )\n        )\n    }\n    single {\n        val lifecycle = LifecycleRegistry(\n            Lifecycle.State.RESUMED\n        )\n        val context = DefaultComponentContext(lifecycle)\n        runBlocking {\n            withContext(Dispatchers.Main) {\n                AppComponent(context)\n            }\n        }\n    }.apply {\n        bind<DesktopDownloadDialogManager>()\n        bind<DesktopAddDownloadDialogManager>()\n        bind<DesktopCategoryDialogManager>()\n        bind<EditDownloadDialogManager>()\n        bind<FileChecksumDialogManager>()\n        bind<QueuePageManager>()\n        bind<NotificationSender>()\n        bind<DownloadItemOpener>()\n        bind<PerHostSettingsPageManager>()\n        bind<PowerActionManager>()\n        bind<SettingsPageManager>()\n    }\n    single {\n        RemovedDownloadsFromDiskTracker(\n            get(), get(), get(),\n        )\n    }\n    single {\n        val definedPaths = get<DefinedPaths>()\n        PreviousVersion(\n            systemPath = definedPaths.systemDir.toFile(),\n            currentVersion = AppInfo.version,\n        )\n    }\n    single {\n        AppVersionTracker(\n            previousVersion = {\n                // it MUST be booted first\n                get<PreviousVersion>().get()\n            },\n            currentVersion = AppInfo.version,\n        )\n    }\n\n    single {\n        val appSettingsStorage: AppSettingsStorage = get()\n        AppSSLFactoryProvider(\n            ignoreSSLCertificates = appSettingsStorage.ignoreSSLCertificates\n        )\n    }\n    single {\n        val appSettingsStorage: AppSettingsStorage = get()\n        AppHostNameVerifier(\n            delegateHostnameVerifier = OkHostnameVerifier,\n            ignoreHostNameVerification = appSettingsStorage.ignoreSSLCertificates\n        )\n    }\n    single<OkHttpClient> {\n        val appSSLFactoryProvider: AppSSLFactoryProvider = get()\n        val appHostNameVerifier: AppHostNameVerifier = get()\n        OkHttpClient\n            .Builder()\n            .protocols(listOf(Protocol.HTTP_1_1))\n            .dispatcher(Dispatcher().apply {\n                //bypass limit on concurrent connections!\n                maxRequests = Int.MAX_VALUE\n                maxRequestsPerHost = Int.MAX_VALUE\n            })\n            .sslSocketFactory(\n                appSSLFactoryProvider.createSSLSocketFactory(),\n                appSSLFactoryProvider.trustManager,\n            )\n            .hostnameVerifier(appHostNameVerifier)\n            .build()\n    }\n    single {\n        KeepAwakeManager(\n            DesktopUtils.keepAwakeService(),\n            get(),\n            get(),\n        )\n    }\n    single<IPerHostSettingsStorage> {\n        val definedPaths = get<DefinedPaths>()\n        PerHostSettingsDatastoreStorage(\n            kotlinxSerializationDataStore<List<PerHostSettingsItem>>(\n                definedPaths.perHostSettingsFile.toFile(),\n                get(),\n                ::emptyList,\n            )\n        )\n    }\n    single {\n        PerHostSettingsManager(get())\n    }\n    single { NotificationManager() }\n\n    single {\n        val definedPaths = get<DesktopDefinedPaths>()\n        CustomRenderApi(definedPaths.renderApiFile)\n    }\n}\n\n\nobject Di : KoinComponent {\n    fun boot() {\n        startKoin {\n            modules(appModule)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/integration/IntegrationHandlerImp.kt",
    "content": "package com.abdownloadmanager.desktop.integration\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.pages.adddownload.SilentImportOptions\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.integration.IntegrationHandler\nimport com.abdownloadmanager.integration.HttpDownloadCredentialsFromIntegration\nimport com.abdownloadmanager.integration.NewDownloadTask\nimport com.abdownloadmanager.integration.ApiQueueModel\nimport com.abdownloadmanager.integration.AddDownloadOptionsFromIntegration\nimport com.abdownloadmanager.integration.HLSDownloadCredentialsFromIntegration\nimport com.abdownloadmanager.integration.IDownloadCredentialsFromIntegration\nimport com.abdownloadmanager.shared.downloaderinui.BasicDownloadItem\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.downloaditem.EmptyContext\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass IntegrationHandlerImp : IntegrationHandler, KoinComponent {\n    val appComponent by inject<AppComponent>()\n    val downloadSystem by inject<DownloadSystem>()\n    val queueManager by inject<QueueManager>()\n    val appSettings by inject<AppRepository>()\n    private val downloaderInUiRegistry by inject<DownloaderInUiRegistry>()\n\n    override suspend fun addDownload(\n        list: List<IDownloadCredentialsFromIntegration>,\n        options: AddDownloadOptionsFromIntegration,\n    ) {\n        appComponent.externalCredentialComingIntoApp(\n            list.map {\n                convertToDownloadSystemCredentials(it)\n            },\n            options = ImportOptions(\n                silentImport = if (options.silentAdd) {\n                    SilentImportOptions(\n                        silentDownload = options.silentStart\n                    )\n                } else null\n            )\n        )\n    }\n\n    override fun listQueues(): List<ApiQueueModel> {\n        return queueManager.getAll().map { downloadQueue ->\n            val queueModel = downloadQueue.getQueueModel()\n            ApiQueueModel(id = queueModel.id, name = queueModel.name)\n        }\n    }\n    override suspend fun addDownloadTask(task: NewDownloadTask) {\n        val addDownloaderInUiProps = convertToDownloadSystemCredentials(task.downloadSource)\n        val downloaderInUi = downloaderInUiRegistry.getDownloaderOf(\n            addDownloaderInUiProps.credentials\n        ) ?: error(\"Downloader for ${addDownloaderInUiProps.credentials::class.qualifiedName} not found\")\n        val downloadItem = downloaderInUi.createBareDownloadItem(\n            addDownloaderInUiProps.credentials,\n            basicDownloadItem = BasicDownloadItem(\n                folder = task.folder ?: appSettings.saveLocation.value,\n                name = task.name ?: addDownloaderInUiProps.extraConfig.suggestedName\n                ?: task.downloadSource.link.substringAfterLast(\"/\"),\n            ),\n        )\n        val id =\n            downloadSystem.addDownload(\n                newDownload = NewDownloadItemProps(\n                    downloadItem = downloadItem,\n                    onDuplicateStrategy = OnDuplicateStrategy.default(),\n                    extraConfig = null,\n                    context = EmptyContext,\n                ),\n                queueId = task.queueId,\n                categoryId = null\n            )\n        if (task.queueId != null) {\n            val queue = queueManager.getQueue(task.queueId!!)\n            queue.start()\n        } else {\n            downloadSystem.userManualResume(id)\n        }\n    }\n\n    companion object {\n        private fun convertToDownloadSystemCredentials(it: IDownloadCredentialsFromIntegration): AddDownloadCredentialsInUiProps {\n            val credentials = when (it) {\n                is HttpDownloadCredentialsFromIntegration -> {\n                    HttpDownloadCredentials(\n                        link = it.link,\n                        headers = it.headers,\n                        downloadPage = it.downloadPage,\n                    )\n                }\n\n                is HLSDownloadCredentialsFromIntegration -> {\n                    HLSDownloadCredentials(\n                        link = it.link,\n                        headers = it.headers,\n                        downloadPage = it.downloadPage,\n                    )\n                }\n            }\n            return AddDownloadCredentialsInUiProps(\n                credentials = credentials,\n                extraConfig = AddDownloadCredentialsInUiProps.Configs(\n                    suggestedName = it.suggestedName,\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/about/AboutDialog.kt",
    "content": "package com.abdownloadmanager.desktop.pages.about\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.window.custom.WindowIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun ShowAboutDialog(appComponent: AppComponent) {\n    if (appComponent.showAboutPage.collectAsState().value) {\n        AboutDialog(\n            onClose = {\n                appComponent.closeAbout()\n            },\n            onRequestShowOpenSourceLibraries = {\n                appComponent.openOpenSourceLibrariesPage()\n            },\n            onRequestShowTranslators = {\n                appComponent.openTranslatorsPage()\n            }\n        )\n    }\n}\n\n@Composable\nfun AboutDialog(\n    onClose: () -> Unit,\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    CustomWindow(\n        resizable = false,\n        onRequestToggleMaximize = null,\n        alwaysOnTop = false,\n        onRequestMinimize = null,\n        state = rememberWindowState(\n            position = WindowPosition.Aligned(Alignment.Center),\n            size = DpSize(600.dp, 310.dp)\n                .applyUiScale(LocalUiScale.current)\n        ),\n        onCloseRequest = onClose\n    ) {\n        WindowTitle(myStringResource(Res.string.about))\n        WindowIcon(MyIcons.info)\n        AboutPage(\n            onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries,\n            onRequestShowTranslators = onRequestShowTranslators\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/about/AboutPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.about\n\nimport androidx.compose.foundation.*\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.*\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Tooltip\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.LinkText\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun AboutPage(\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    Box {\n        BackgroundEffects()\n        RenderAppInfo(\n            modifier = Modifier,\n            onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries,\n            onRequestShowTranslators = onRequestShowTranslators,\n        )\n    }\n}\n\n@Composable\nprivate fun AppIconAndVersion(\n    modifier: Modifier,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = modifier.padding(\n            horizontal = 24.dp,\n            vertical = 8.dp,\n        )\n    ) {\n        val shape = RoundedCornerShape(16.dp)\n        Image(\n            MyIcons.appIcon.rememberPainter(),\n            null,\n            Modifier\n                .shadow(12.dp, shape, spotColor = myColors.primary)\n                .clip(shape)\n                .border(\n                    1.dp,\n                    Brush.linearGradient(\n                        listOf(myColors.primary, myColors.secondary)\n                    ),\n                    shape\n                )\n                .background(myColors.surface)\n                .padding(16.dp)\n                .size(52.dp)\n        )\n        Spacer(Modifier.size(16.dp))\n        Column(\n            horizontalAlignment = Alignment.Start\n        ) {\n            Text(\n                AppInfo.displayName,\n                fontSize = myTextSizes.lg,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(Modifier.height(2.dp))\n            WithContentAlpha(0.75f) {\n                Text(\n                    myStringResource(\n                        Res.string.version_n,\n                        Res.string.version_n_createArgs(\n                            value = AppInfo.version.toString(),\n                        )\n                    ),\n                    fontSize = myTextSizes.base,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderAppInfo(\n    modifier: Modifier,\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    Row(\n        modifier.fillMaxSize(),\n    ) {\n        Column(\n            Modifier.width(250.dp),\n            verticalArrangement = Arrangement.SpaceBetween,\n        ) {\n            AppIconAndVersion(Modifier.fillMaxWidth())\n            Spacer(Modifier.weight(1f))\n            Column(\n                Modifier\n                    .fillMaxWidth(),\n                horizontalAlignment = Alignment.CenterHorizontally,\n            ) {\n                Text(\n                    myStringResource(Res.string.developed_with_love_for_you),\n                )\n                Spacer(Modifier.height(8.dp))\n                DonateButton()\n                Spacer(Modifier.height(8.dp))\n                Spacer(\n                    Modifier\n                        .fillMaxWidth()\n                        .background(myColors.onBackground / 0.05f)\n                        .height(1.dp)\n                )\n                Spacer(Modifier.height(8.dp))\n                val websiteUrl = SharedConstants.projectWebsite\n                val websiteDisplayName = remember(websiteUrl) {\n                    HttpUrlUtils.getHost(websiteUrl) ?: websiteUrl\n                }\n                LinkText(\n                    text = websiteDisplayName,\n                    link = websiteUrl,\n                    showExternalIndicator = false,\n                )\n                Spacer(Modifier.height(16.dp))\n            }\n        }\n        Spacer(\n            Modifier\n                .fillMaxHeight()\n                .width(1.dp)\n                .background(myColors.onBackground.copy(0.15f))\n        )\n        Column(\n            Modifier.weight(1f)\n        ) {\n            CreditsSection(\n                modifier = Modifier.fillMaxWidth().weight(1f),\n                onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries,\n                onRequestShowTranslators = onRequestShowTranslators,\n            )\n            Spacer(Modifier.height(1.dp).fillMaxWidth().background(myColors.onBackground / 0.15f))\n            SocialAndLinks(\n                Modifier\n                    .fillMaxWidth()\n                    .background(myColors.surface / 0.5f)\n                    .padding(top = 12.dp)\n                    .padding(bottom = 16.dp)\n                    .wrapContentWidth(),\n                horizontalPadding = 8.dp,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SocialAndLinks(\n    modifier: Modifier = Modifier,\n    horizontalPadding: Dp,\n) {\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n        modifier = modifier\n            .padding(\n                horizontal = horizontalPadding,\n            )\n    ) {\n        SocialSmallButton(\n            MyIcons.earth,\n            Res.string.visit_the_project_website.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectWebsite)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.openSource,\n            Res.string.view_the_source_code.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectSourceCode)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.speaker,\n            Res.string.channel.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.telegramChannelUrl)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.group,\n            Res.string.group.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.telegramGroupUrl)\n            }\n        )\n        SocialSmallButton(\n            MyIcons.language,\n            Res.string.translators_contribute_title.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(SharedConstants.projectTranslations)\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun CreditsSection(\n    modifier: Modifier = Modifier,\n    onRequestShowOpenSourceLibraries: () -> Unit,\n    onRequestShowTranslators: () -> Unit,\n) {\n    Column(\n        modifier\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n            .padding(vertical = 8.dp),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        val itemModifier = Modifier.fillMaxWidth()\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.hearth,\n            title = Res.string.this_is_a_free_and_open_source_software.asStringSource(),\n            description = Res.string.view_the_source_code.asStringSource(),\n            onClick = {\n                URLOpener.openUrl(AppInfo.sourceCode)\n            }\n        )\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.openSource,\n            title = Res.string.powered_by_open_source_software.asStringSource(),\n            description = Res.string.view_the_open_source_licenses.asStringSource(),\n            onClick = {\n                onRequestShowOpenSourceLibraries()\n            }\n        )\n        AboutPageListItemButton(\n            itemModifier,\n            icon = MyIcons.language,\n            title = Res.string.localized_by_translators.asStringSource(),\n            description = Res.string.meet_the_translators.asStringSource(),\n            onClick = {\n                onRequestShowTranslators()\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun SocialSmallButton(\n    icon: IconSource,\n    title: StringSource,\n    onClick: () -> Unit,\n) {\n    Tooltip(title) {\n        IconActionButton(\n            icon,\n            contentDescription = title,\n            onClick = onClick,\n        )\n    }\n}\n\n@Composable\nprivate fun AboutPageListItemButton(\n    modifier: Modifier,\n    icon: IconSource,\n    title: StringSource,\n    description: StringSource,\n    onClick: () -> Unit,\n) {\n    val shape = myShapes.defaultRounded\n    Row(\n        modifier\n            .border(1.dp, myColors.onBackground / 0.15f, shape)\n            .clip(shape)\n            .clickable(onClick = onClick)\n            .background(myColors.surface / 0.5f)\n            .padding(\n                horizontal = 8.dp,\n                vertical = 8.dp,\n            ),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        MyIcon(\n            icon = icon,\n            contentDescription = null,\n            modifier = Modifier.size(24.dp)\n        )\n        Spacer(Modifier.width(8.dp))\n        Column {\n            Text(\n                title.rememberString(),\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(Modifier.height(2.dp))\n            Text(description.rememberString())\n        }\n    }\n}\n\n\n@Composable\nprivate fun BoxScope.BackgroundEffects() {\n    Box(\n        Modifier\n            .align(Alignment.TopCenter)\n            .offset(x = (-50).dp, y = (-148).dp)\n            .fillMaxWidth(0.5f)\n            .height(250.dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.primary / 0.15f\n            )\n    )\n    Box(\n        Modifier\n            .align(Alignment.BottomStart)\n            .size(220.dp)\n            .offset(x = (-64).dp, y = (+128).dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.secondaryVariant / 0.15f\n            )\n    )\n    Box(\n        Modifier\n            .align(Alignment.BottomEnd)\n            .size(220.dp)\n            .offset(x = 32.dp, y = (-32).dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.secondary / 0.15f\n            )\n    )\n}\n\n@Composable\nprivate fun DonateButton() {\n    ActionButton(\n        backgroundColor = SolidColor(LocalContentColor.current / 0.05f),\n        start = {\n            MyIcon(\n                MyIcons.hearth,\n                null,\n                modifier = Modifier.size(16.dp),\n                tint = myColors.error,\n            )\n            Spacer(Modifier.width(8.dp))\n        },\n        text = myStringResource(Res.string.donate),\n        onClick = {\n            URLOpener.openUrl(SharedConstants.donateLink)\n        }\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/ShowAddDownloadDialogs.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.DesktopAddDownloadDialogManager\nimport com.abdownloadmanager.desktop.pages.addDownload.multiple.DesktopAddMultiDownloadComponent\nimport com.abdownloadmanager.desktop.pages.addDownload.multiple.AddMultiItemPage\nimport com.abdownloadmanager.desktop.pages.addDownload.single.AddDownloadPage\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowIcon\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadComponent\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.PlatformAppActivator\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.Dimension\n\n@Composable\nfun ShowAddDownloadDialogs(component: DesktopAddDownloadDialogManager) {\n    val openedAddDownloadDialogs = component.openedAddDownloadDialogs.collectAsState().value\n    for (addDownloadComponent in openedAddDownloadDialogs) {\n        key(addDownloadComponent.id) {\n            AddDownloadWindow(\n                addDownloadComponent = addDownloadComponent,\n                onRequestClose = {\n                    component.closeAddDownloadDialog(addDownloadComponent.id)\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AddDownloadWindow(\n    addDownloadComponent: AddDownloadComponent,\n    onRequestClose: () -> Unit,\n) {\n    val shouldShowWindow by addDownloadComponent.shouldShowWindow.collectAsState()\n    if (!shouldShowWindow) return\n    val uiScale = LocalUiScale.current\n    when (addDownloadComponent) {\n        is BaseAddSingleDownloadComponent -> {\n            val h = 265.applyUiScale(uiScale)\n            val w = 500.applyUiScale(uiScale)\n            val size = remember {\n                DpSize(\n                    height = h.dp,\n                    width = w.dp,\n                )\n            }\n\n            val state = rememberWindowState(\n                size = size,\n                position = WindowPosition(Alignment.Center)\n            )\n            CustomWindow(\n                state = state,\n                onCloseRequest = onRequestClose,\n                alwaysOnTop = true,\n            ) {\n                LaunchedEffect(Unit) {\n                    window.minimumSize = Dimension(w, h)\n                    PlatformAppActivator.active()\n                }\n//                    BringToFront()\n                WindowTitle(myStringResource(Res.string.add_download))\n                WindowIcon(MyIcons.appIcon)\n                AddDownloadPage(addDownloadComponent)\n            }\n        }\n\n        is DesktopAddMultiDownloadComponent -> {\n            val h = 450\n            val w = 800\n            val state = rememberWindowState(\n                height = h.dp,\n                width = w.dp,\n                position = WindowPosition(Alignment.Center)\n            )\n            CustomWindow(\n                state = state,\n                onCloseRequest = onRequestClose,\n                alwaysOnTop = true,\n            ) {\n                LaunchedEffect(Unit) {\n                    window.minimumSize = Dimension(w, h)\n                    PlatformAppActivator.active()\n                }\n//                    BringToFront()\n                WindowTitle(myStringResource(Res.string.add_download))\n                WindowIcon(MyIcons.appIcon)\n                AddMultiItemPage(addDownloadComponent)\n            }\n        }\n    }\n}\n\n//it seems not affect at all\n//@Composable\n//private fun WindowScope.BringToFront() {\n//    LaunchedEffect(Unit) {\n//        window.toFront()\n//        window.requestFocus()\n//    }\n//}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.multiple\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.onClick\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.CategoryAddButton\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.CategorySelect\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.ExtraConfig\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.LocationTextField\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.ShowAddToQueueDialog\nimport com.abdownloadmanager.desktop.pages.home.sections.SearchBox\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.rememberString\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\nfun AddMultiItemPage(\n    addMultiDownloadComponent: DesktopAddMultiDownloadComponent,\n) {\n    Column(Modifier) {\n        Column(\n            Modifier\n                .padding(horizontal = 16.dp)\n                .padding(top = 8.dp)\n                .weight(1f)\n        ) {\n            WithContentAlpha(1f) {\n                Text(\n                    myStringResource(Res.string.add_multi_download_page_header),\n                    fontSize = myTextSizes.base\n                )\n            }\n            Spacer(Modifier.height(8.dp))\n            AddMultiDownloadTable(\n                Modifier.weight(1f),\n                addMultiDownloadComponent,\n            )\n        }\n        Footer(\n            Modifier,\n            addMultiDownloadComponent,\n        )\n    }\n    val currentDownloadConfigurableList by addMultiDownloadComponent.currentDownloadConfigurableList.collectAsState()\n    currentDownloadConfigurableList?.let {\n        ExtraConfig(\n            onDismiss = {\n                addMultiDownloadComponent.openConfigurableList(null)\n            },\n            configurables = it\n        )\n    }\n    if (addMultiDownloadComponent.showAddToQueue) {\n        ShowAddToQueueDialog(\n            queueList = addMultiDownloadComponent.queueList.collectAsState().value,\n            onQueueSelected = { queue, startQueue ->\n                addMultiDownloadComponent.requestAddDownloads(\n                    queue, startQueue\n                )\n            },\n            onClose = {\n                addMultiDownloadComponent.closeAddToQueue()\n            }\n        )\n    }\n}\n\n@Composable\nfun Footer(\n    modifier: Modifier = Modifier,\n    component: DesktopAddMultiDownloadComponent,\n) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Column(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n        ) {\n            Row(\n                Modifier\n                    .padding(horizontal = 16.dp)\n                    .padding(vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                SaveSettings(\n                    modifier = Modifier.fillMaxWidth().weight(1f),\n                    component = component,\n                )\n                Spacer(Modifier.width(8.dp))\n                Column(\n                    Modifier.align(Alignment.Bottom)\n                        .width(IntrinsicSize.Max),\n                ) {\n                    val filterText by component.filterText.collectAsState()\n                    SearchBox(\n                        text = filterText,\n                        onTextChange = component::setFilterText,\n                        placeholder = myStringResource(Res.string.search),\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .align(Alignment.End),\n                        textPadding = PaddingValues(\n                            vertical = 6.dp,\n                            horizontal = 8.dp\n                        ),\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    Row(\n                        horizontalArrangement = Arrangement.End,\n                    ) {\n                        PrimaryMainActionButton(\n                            text = myStringResource(Res.string.add),\n                            onClick = {\n                                component.openAddToQueueDialog()\n                            },\n                            enabled = component.canClickAdd,\n                            modifier = Modifier,\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        ActionButton(\n                            text = myStringResource(Res.string.cancel),\n                            onClick = {\n                                component.requestClose()\n                            },\n                            modifier = Modifier,\n                        )\n                    }\n                }\n            }\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .height(2.dp)\n                    .background(\n                        myColors.surface\n                    )\n            )\n            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) {\n                SelectionDetail(\n                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),\n                    totalDownloadItems = component.totalList.size,\n                    selectedDownloadItems = component.selectionList.size,\n                    sizes = component.selectedTotalSize.collectAsState().value,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SaveSettings(\n    modifier: Modifier,\n    component: DesktopAddMultiDownloadComponent,\n) {\n    val selectedCategory by component.selectedCategory.collectAsState()\n\n    val folder by component.folder.collectAsState()\n\n    Column(modifier) {\n        Text(\"${myStringResource(Res.string.save_to)}:\")\n        Spacer(Modifier.height(8.dp))\n        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {\n            CategorySaveOption(selectedCategory, component)\n            Spacer(Modifier.width(8.dp))\n            LocationSaveOption(component, folder)\n            Spacer(Modifier)\n        }\n    }\n}\n\n@Composable\nprivate fun SelectionDetail(\n    modifier: Modifier,\n    totalDownloadItems: Int,\n    selectedDownloadItems: Int,\n    sizes: List<DownloadSize>,\n) {\n    val selectionCount = \"$selectedDownloadItems / $totalDownloadItems\"\n    Row(\n        modifier,\n        horizontalArrangement = Arrangement.spacedBy(4.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        WithContentAlpha(.25f) {\n            MyIcon(\n                icon = MyIcons.activeCount,\n                contentDescription = null,\n                modifier = Modifier.size(16.dp)\n            )\n        }\n        Text(selectionCount, fontSize = myTextSizes.base)\n\n        val sizes = sizes.ifThen(sizes.isEmpty()){\n            listOf(DownloadSize.Bytes.Zero)\n        }\n        WithContentAlpha(.25f) {\n            MyIcon(\n                icon = MyIcons.data,\n                contentDescription = null,\n                modifier = Modifier.size(16.dp)\n            )\n        }\n        for (sizeType in sizes) {\n            val sizeString = sizeType.rememberString()\n            Text(sizeString, fontSize = myTextSizes.base)\n        }\n    }\n}\n\n@Composable\nprivate fun RowScope.LocationSaveOption(\n    component: DesktopAddMultiDownloadComponent,\n    folder: String\n) {\n    val allItemsInSameLocation by component.allInSameLocation.collectAsState()\n    SaveOption(\n        title = myStringResource(Res.string.all_items_in_one_Location),\n        selectedHelp = myStringResource(Res.string.all_items_in_one_Location_description),\n        unselectedHelp = myStringResource(Res.string.unselected_all_items_in_specific_location_description),\n        selected = allItemsInSameLocation,\n        onSelectedChange = {\n            component.setAllItemsInSameLocation(it)\n        },\n        selectedContent = {\n            LocationTextField(\n                text = folder,\n                setText = {\n                    component.setFolder(it)\n                },\n                modifier = Modifier.fillMaxWidth(),\n                lastUsedLocations = component.lastUsedLocations.collectAsState().value,\n                onRequestRemoveSaveLocation = component::removeFromLastDownloadLocation\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun RowScope.CategorySaveOption(\n    selectedCategory: Category?,\n    component: DesktopAddMultiDownloadComponent\n) {\n\n    SaveOption(\n        title = myStringResource(Res.string.all_items_in_one_category),\n        selectedHelp = myStringResource(Res.string.all_items_in_one_category_description),\n        unselectedHelp = myStringResource(Res.string.each_item_on_its_own_category_description),\n        selected = selectedCategory != null,\n        onSelectedChange = {\n            if (it) {\n                component.setSelectedCategory(component.categories.value.firstOrNull())\n            } else {\n                component.setSelectedCategory(null)\n            }\n            component.setAlsoAutoCategorize(!it)\n        },\n        selectedContent = {\n            Row(\n                Modifier.height(IntrinsicSize.Max).fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CategorySelect(\n                    categories = component.categories.collectAsState().value,\n                    modifier = Modifier.weight(1f),\n                    selectedCategory = component.selectedCategory.collectAsState().value,\n                    onCategorySelected = {\n                        component.setSelectedCategory(it)\n                    }\n                )\n                Spacer(Modifier.width(8.dp))\n                CategoryAddButton(\n                    Modifier.fillMaxHeight(),\n                    enabled = true,\n                    onClick = {\n                        component.onRequestAddCategory()\n                    },\n                )\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun RowScope.SaveOption(\n    title: String,\n    selectedHelp: String,\n    unselectedHelp: String,\n    selected: Boolean,\n    onSelectedChange: (Boolean) -> Unit,\n    selectedContent: @Composable () -> Unit\n) {\n    ExpandableItem(\n        modifier = Modifier.fillMaxWidth().weight(1f),\n        isExpanded = selected,\n        header = {\n            Row(\n                modifier = Modifier.onClick { onSelectedChange(!selected) },\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                CheckBox(\n                    value = selected,\n                    onValueChange = onSelectedChange\n                )\n                Text(title)\n                Help(if (selected) selectedHelp else unselectedHelp)\n            }\n        },\n        body = {\n            Column {\n                Spacer(Modifier.height(8.dp))\n                selectedContent()\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiItemTable.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.multiple\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.styled.MyStyledTableHeader\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.isCtrlPressed\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.input.pointer.isShiftPressed\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CustomCellRenderer\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputsUniqueIdType\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.NewMultiDownloadState\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.SortableCell\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun AddMultiDownloadTable(\n    modifier: Modifier,\n    component: DesktopAddMultiDownloadComponent,\n) {\n    var isCtrlPressed by remember { mutableStateOf(false) }\n\n    val lastSelectedId = component.lastSelectedId\n    val context = AddMultiItemListContext(component, component.isAllFilteredSelected.collectAsState().value)\n    val iconProvider = component.fileIconProvider\n    val list by component.filteredList.collectAsState()\n\n    CompositionLocalProvider(\n        LocalAddMultiItemListContext provides context,\n    ) {\n        val itemHorizontalPadding = 16.dp\n        Table(\n            key = {\n                it.id\n            },\n            tableState = component.tableState,\n            list = list,\n            modifier = modifier\n                .onKeyEvent {\n                    isCtrlPressed = it.isCtrlPressed\n                    false\n                }\n                .onKeyEvent {\n                    if (it.key == Key.Escape) {\n                        context.changeAllSelection(false)\n                        true\n                    } else {\n                        false\n                    }\n                }\n                .onKeyEvent {\n                    if (isCtrlPressed && it.key == Key.A) {\n                        context.changeAllSelection(true)\n                        true\n                    } else {\n                        false\n                    }\n                },\n            wrapHeader = {\n                MyStyledTableHeader(\n                    itemHorizontalPadding = itemHorizontalPadding,\n                    content = it\n                )\n            },\n            wrapItem = { _, item, content ->\n                val shape = RoundedCornerShape(12.dp)\n                WithContentAlpha(1f) {\n                    val isSelected = remember(item, component.selectionList) {\n                        component.isSelected(item.id)\n                    }\n                    CompositionLocalProvider(\n                        LocalIsChecked provides isSelected,\n                    ) {\n                        val itemInteractionSource = remember { MutableInteractionSource() }\n                        Box(\n                            Modifier\n                                .widthIn(min = getTableSize().visibleWidth)\n                                .onClick(\n                                    interactionSource = itemInteractionSource\n                                ) {\n                                    if (isCtrlPressed) {\n                                        context.select(item.id, !isSelected)\n                                    } else {\n                                        context.changeAllSelection(false)\n                                        context.select(item.id, true)\n                                    }\n                                }\n                                .onClick(\n                                    enabled = lastSelectedId != null,\n                                    keyboardModifiers = {\n                                        this.isShiftPressed\n                                    }\n                                ) {\n                                    val lastSelected = lastSelectedId ?: return@onClick\n                                    val currentId = item.id\n                                    val ids = component.tableState.getARangeOfItems(\n                                        list = list,\n                                        id = { it.id },\n                                        fromItem = lastSelected,\n                                        toItem = currentId,\n                                    )\n                                    context.newSelection(ids, true)\n                                }\n                                .onClick(\n                                    matcher = PointerMatcher.mouse(PointerButton.Secondary)\n                                ) {\n                                    component.openConfigurableList(item.id)\n                                }\n                                .fillMaxWidth()\n                                .padding(vertical = 1.dp)\n                                .clip(shape)\n                                .indication(\n                                    interactionSource = itemInteractionSource,\n                                    indication = LocalIndication.current\n                                )\n                                .hoverable(itemInteractionSource)\n                                .let {\n                                    if (isSelected) {\n                                        val selectionColor = myColors.onBackground\n                                        it\n                                            .border(\n                                                1.dp,\n                                                myColors.selectionGradient(0.10f, 0.05f, selectionColor),\n                                                shape\n                                            )\n                                            .background(myColors.selectionGradient(0.15f, 0f, selectionColor))\n                                    } else {\n                                        it.border(1.dp, Color.Transparent)\n                                    }\n                                }\n                                .padding(vertical = 8.dp, horizontal = itemHorizontalPadding)\n                        ) {\n                            content()\n                        }\n                    }\n                }\n            }\n        ) { cell, item ->\n            when (cell) {\n                AddMultiItemTableCells.Check -> {\n                    CheckCell(\n                        newMultiDownloadState = item,\n                        onCheckedChange = { dc, b ->\n                            component.setSelect(item.id, b)\n                        }\n                    )\n                }\n\n                AddMultiItemTableCells.Name -> {\n                    NameCell(\n                        item = item,\n                        iconProvider = iconProvider,\n                    )\n                }\n\n                AddMultiItemTableCells.Link -> {\n                    LinkCell(item)\n                }\n\n                AddMultiItemTableCells.SizeCell -> {\n                    SizeCell(item)\n                }\n            }\n\n        }\n    }\n}\n\nprivate val LocalIsChecked = compositionLocalOf<Boolean> {\n    error(\"LocalIsChecked not provided\")\n}\nprivate val LocalAddMultiItemListContext = compositionLocalOf<AddMultiItemListContext> {\n    error(\"LocalAddMultiItemListContext not provided\")\n}\n\nclass AddMultiItemListContext(\n    val component: DesktopAddMultiDownloadComponent,\n    val isAllSelected: Boolean,\n) {\n    fun changeAllSelection(boolean: Boolean) {\n        component.selectAll(boolean)\n    }\n\n    fun select(id: NewDownloadInputsUniqueIdType, boolean: Boolean) {\n        component.setSelect(id, boolean)\n    }\n\n    fun newSelection(ids: List<NewDownloadInputsUniqueIdType>, boolean: Boolean) {\n        component.resetSelectionTo(ids, boolean)\n    }\n}\n\nsealed class AddMultiItemTableCells : TableCell<NewMultiDownloadState> {\n    companion object {\n        fun all(): List<AddMultiItemTableCells> {\n            return listOf(\n                Check,\n                Name,\n                SizeCell,\n                Link,\n            )\n        }\n    }\n\n    data object Check : AddMultiItemTableCells(),\n        CustomCellRenderer {\n        override val id: String = \"#\"\n        override val name: StringSource = \"#\".asStringSource()\n        override val size: CellSize = CellSize.Fixed(26.dp)\n\n        @Composable\n        override fun drawHeader() {\n            val context = LocalAddMultiItemListContext.current\n            CheckBox(\n                context.isAllSelected,\n                { context.component.selectAll(!context.isAllSelected) },\n                size = 12.dp\n            )\n        }\n    }\n\n    data object Name : AddMultiItemTableCells(), SortableCell<NewMultiDownloadState> {\n        override val id: String = \"Name\"\n        override val name: StringSource = Res.string.name.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(120.dp..1000.dp, 350.dp)\n        override fun comparator(): Comparator<NewMultiDownloadState> {\n            return compareBy { it.name }\n        }\n    }\n\n    data object Link : AddMultiItemTableCells(), SortableCell<NewMultiDownloadState> {\n        override val id: String = \"Link\"\n        override val name: StringSource = Res.string.link.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(120.dp..2000.dp, 240.dp)\n        override fun comparator(): Comparator<NewMultiDownloadState> {\n            return compareBy { it.link }\n        }\n    }\n\n    data object SizeCell : AddMultiItemTableCells(), SortableCell<NewMultiDownloadState> {\n        override val id: String = \"Size\"\n        override val name: StringSource = Res.string.size.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..180.dp, 100.dp)\n        override fun comparator(): Comparator<NewMultiDownloadState> {\n            return compareBy { it.size }\n        }\n    }\n}\n\n\n@Composable\nprivate fun CellText(\n    text: String,\n) {\n    Text(\n        text,\n        fontSize = myTextSizes.base,\n        maxLines = 1,\n        overflow = TextOverflow.Ellipsis,\n    )\n}\n\n@Composable\nprivate fun NameCell(\n    item: NewMultiDownloadState,\n    iconProvider: FileIconProvider,\n) {\n    val name = item.name\n    val icon = iconProvider.rememberIcon(name)\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        MyIcon(\n            icon = icon,\n            contentDescription = null,\n            modifier = Modifier.size(16.dp).alpha(0.75f)\n        )\n        Spacer(Modifier.width(8.dp))\n        CellText(name)\n    }\n\n}\n\n@Composable\nprivate fun LinkCell(\n    item: NewMultiDownloadState,\n) {\n    CellText(item.link)\n}\n\n@Composable\nprivate fun SizeCell(\n    multiDownloadState: NewMultiDownloadState,\n) {\n    CellText(\n        multiDownloadState.sizeString.rememberString()\n    )\n}\n\n@Composable\nprivate fun CheckCell(\n    onCheckedChange: (NewMultiDownloadState, Boolean) -> Unit,\n    newMultiDownloadState: NewMultiDownloadState,\n) {\n    val isChecked = LocalIsChecked.current\n    CheckBox(\n        value = isChecked,\n        onValueChange = {\n            onCheckedChange(newMultiDownloadState, it)\n        },\n        modifier = Modifier,\n        size = 12.dp,\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/DesktopAddMultiDownloadComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.multiple\n\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.BaseAddMultiDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.multiple.OnRequestAdd\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.queue.QueueManager\n\nclass DesktopAddMultiDownloadComponent(\n    ctx: ComponentContext,\n    id: String,\n    onRequestClose: () -> Unit,\n    onRequestAdd: OnRequestAdd,\n    private val categoryDialogManager: CategoryDialogManager,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n    perHostSettingsManager: PerHostSettingsManager, downloadSystem: DownloadSystem,\n    fileIconProvider: FileIconProvider,\n    appRepository: AppRepository,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n) : BaseAddMultiDownloadComponent(\n    ctx = ctx,\n    id = id,\n    lastSavedLocationsStorage = lastSavedLocationsStorage,\n    onRequestAdd = onRequestAdd,\n    onRequestClose = onRequestClose,\n    perHostSettingsManager = perHostSettingsManager,\n    downloadSystem = downloadSystem,\n    appRepository = appRepository,\n    fileIconProvider = fileIconProvider,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n) {\n    override fun getCategoryPageManager(): CategoryDialogManager {\n        return categoryDialogManager\n    }\n    val tableState = TableState(\n        cells = AddMultiItemTableCells.all(),\n        forceVisibleCells = listOf(\n            AddMultiItemTableCells.Check,\n            AddMultiItemTableCells.Name,\n        )\n    )\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/shared/CategorySelect.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.shared\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.*\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.rememberIconPainter\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun CategorySelect(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    categories: List<Category>,\n    selectedCategory: Category?,\n    onCategorySelected: (Category) -> Unit,\n) {\n    var isSelectionOpen by remember {\n        mutableStateOf(false)\n    }\n    val closeDialog = {\n        isSelectionOpen = false\n    }\n    DialogDropDown(\n        selectedItem = selectedCategory,\n        possibleItems = categories,\n        onItemSelected = onCategorySelected,\n        enabled = enabled,\n        renderItem = {\n            RenderCategory(\n                category = it,\n                modifier = Modifier,\n            )\n        },\n        dropdownOpen = isSelectionOpen,\n        onRequestCloseDropDown = {\n            closeDialog()\n        },\n        onRequestOpenDropDown = {\n            isSelectionOpen = true\n        },\n        modifier = modifier,\n        renderEmpty = {\n            Column(\n                modifier = Modifier.fillMaxSize().wrapContentSize(),\n                horizontalAlignment = Alignment.CenterHorizontally,\n            ) {\n                MyIcon(MyIcons.info, null, Modifier.size(64.dp))\n                Spacer(Modifier.height(16.dp))\n                Text(\n                    myStringResource(Res.string.no_categories_found),\n                    fontWeight = FontWeight.Bold,\n                    fontSize = myTextSizes.lg,\n                )\n            }\n        },\n        dropDownSize = DpSize(220.dp, 220.dp),\n    )\n}\n\n@Composable\nprivate fun RenderCategory(\n    modifier: Modifier,\n    category: Category,\n) {\n    Row(\n        modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val icon = category.rememberIconPainter()\n        val iconModifier = Modifier.size(16.dp)\n        if (icon != null) {\n            MyIcon(\n                icon,\n                null,\n                iconModifier,\n            )\n        } else {\n            Spacer(iconModifier)\n        }\n        Spacer(Modifier.width(8.dp))\n        Text(\n            category.name,\n            softWrap = false,\n            maxLines = 1,\n            modifier = Modifier.weight(1f)\n        )\n    }\n}\n\n@Composable\nfun CategoryAddButton(\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    val borderColor = myColors.onBackground / 0.1f\n    val background = myColors.surface / 50\n    val shape = myShapes.defaultRounded\n    Box(\n        modifier\n            .clip(shape)\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .border(1.dp, borderColor, shape)\n            .background(background)\n            .clickable(\n                enabled = enabled\n            ) { onClick() }\n            .aspectRatio(1f)\n//            .padding(horizontal = 8.dp)\n    ) {\n        MyIcon(\n            MyIcons.add,\n            contentDescription = \"Add Category\",\n            Modifier\n                .align(Alignment.Center)\n                .size(16.dp)\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/shared/DialogDropDown.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.shared\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.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberDialogState\nimport com.abdownloadmanager.desktop.window.custom.BaseOptionDialog\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.desktop.window.moveSafe\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.MouseInfo\n\n@Composable\nfun <T> DialogDropDown(\n    selectedItem: T?,\n    possibleItems: List<T>,\n    onItemSelected: (T) -> Unit,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    dropdownOpen: Boolean,\n    onRequestOpenDropDown: () -> Unit,\n    onRequestCloseDropDown: () -> Unit,\n    dropDownSize: DpSize = DpSize(220.dp, 250.dp),\n    renderItem: @Composable (T) -> Unit,\n    renderEmpty: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        DropDownHeader(\n            item = selectedItem,\n            enabled = enabled,\n            onClick = onRequestOpenDropDown,\n            renderItem = renderItem\n        )\n        if (dropdownOpen) {\n            DropDownContent(\n                closeDialog = onRequestCloseDropDown,\n                dropDownSize = dropDownSize,\n                possibleItems = possibleItems,\n                selectedItem = selectedItem,\n                onItemSelected = onItemSelected,\n                drawOnEmpty = renderEmpty,\n                renderItem = renderItem,\n            )\n        }\n    }\n}\n\n@Composable\nfun <T> DropDownContent(\n    closeDialog: () -> Unit,\n    dropDownSize: DpSize,\n    possibleItems: List<T>,\n    selectedItem: T?,\n    onItemSelected: (T) -> Unit,\n    drawOnEmpty: @Composable () -> Unit,\n    renderItem: @Composable (T) -> Unit,\n) {\n    BaseOptionDialog(\n        onCloseRequest = closeDialog,\n        state = rememberDialogState(\n            size = dropDownSize.applyUiScale(LocalUiScale.current)\n        ),\n        resizeable = true,\n        content = {\n            LaunchedEffect(window) {\n                window.moveSafe(\n                    MouseInfo.getPointerInfo().location.run {\n                        DpOffset(\n                            x = x.dp,\n                            y = y.dp\n                        )\n                    }\n                )\n            }\n            val shape = myShapes.defaultRounded\n\n            Box(\n                Modifier\n                    .clip(shape)\n                    .border(2.dp, myColors.onBackground / 10, shape)\n                    .background(\n                        Brush.linearGradient(\n                            listOf(\n                                myColors.surface,\n                                myColors.background,\n                            )\n                        )\n                    )\n            ) {\n                val listState = rememberLazyListState()\n                LazyColumn(\n                    Modifier\n                        .fillMaxSize()\n                        .padding(8.dp),\n                    state = listState,\n                ) {\n                    items(possibleItems) {\n                        val isSelected = it == selectedItem\n                        WithContentAlpha(\n                            if (isSelected) 1f else 0.75f\n                        ) {\n                            Row(\n                                Modifier\n                                    .clip(shape)\n                                    .clickable {\n                                        onItemSelected(it)\n                                        closeDialog()\n                                    }\n                                    .padding(\n                                        vertical = 8.dp,\n                                        horizontal = 8.dp\n                                    )\n                            ) {\n                                Box(\n                                    Modifier.weight(1f)\n                                ) {\n                                    renderItem(it)\n                                }\n                                val selectedIconModifier = Modifier.size(16.dp)\n                                if (isSelected) {\n                                    MyIcon(\n                                        MyIcons.check,\n                                        null,\n                                        selectedIconModifier,\n                                    )\n                                } else {\n                                    Spacer(selectedIconModifier)\n                                }\n                            }\n                        }\n                    }\n                }\n                if (possibleItems.isEmpty()) {\n                    Box(Modifier.padding().fillMaxSize()) {\n                        drawOnEmpty()\n                    }\n                }\n                AnimatedVisibility(\n                    visible = listState.canScrollForward,\n                    modifier = Modifier.matchParentSize(),\n                    enter = fadeIn(),\n                    exit = fadeOut(),\n                ) {\n                    Spacer(\n                        Modifier\n                            .fillMaxSize()\n                            .background(\n                                Brush.verticalGradient(\n                                    colorStops = arrayOf(\n                                        0f to Color.Transparent,\n                                        0.8f to Color.Transparent,\n                                        1f to myColors.background,\n                                    )\n                                )\n                            )\n                    )\n                }\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun <T> DropDownHeader(\n    item: T?,\n    enabled: Boolean,\n    onClick: () -> Unit,\n    renderItem: @Composable (T) -> Unit,\n) {\n    val borderColor = myColors.onBackground / 0.1f\n    val background = myColors.surface / 50\n    val shape = myShapes.defaultRounded\n    Row(\n        Modifier\n            .height(IntrinsicSize.Max)\n            .clip(shape)\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .border(1.dp, borderColor, shape)\n            .background(background)\n            .clickable(\n                enabled = enabled\n            ) { onClick() }\n            .padding(horizontal = 8.dp)\n    ) {\n        val contentModifier = Modifier\n            .padding(vertical = 8.dp)\n            .weight(1f)\n        if (item != null) {\n            Box(contentModifier) {\n                renderItem(item)\n            }\n        } else {\n            Text(\n                myStringResource(Res.string.no_category_selected),\n                contentModifier\n            )\n        }\n        Spacer(\n            Modifier\n                .padding(horizontal = 8.dp)\n                .fillMaxHeight().padding(vertical = 1.dp)\n                .width(1.dp)\n                .background(borderColor)\n        )\n        MyIcon(\n            MyIcons.down,\n            null,\n            Modifier\n                .align(Alignment.CenterVertically)\n                .size(16.dp),\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/shared/ExtraConfig.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.shared\n\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.desktop.window.custom.BaseOptionDialog\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.desktop.window.moveSafe\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.window.WindowDraggableArea\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\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.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberDialogState\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.Dimension\nimport java.awt.MouseInfo\n\n@Composable\nfun ExtraConfig(\n    onDismiss: () -> Unit,\n    configurables: List<Configurable<*>>,\n) {\n    val h = 250\n    val w = 350\n    val state = rememberDialogState(\n        size = DpSize(\n            height = h.dp,\n            width = w.dp,\n        ).applyUiScale(LocalUiScale.current),\n    )\n    BaseOptionDialog(onDismiss, state) {\n        LaunchedEffect(window){\n            window.moveSafe(\n                MouseInfo.getPointerInfo().location.run {\n                    DpOffset(\n                        x = x.dp,\n                        y = y.dp\n                    )\n                }\n            )\n        }\n\n\n        val shape = myShapes.defaultRounded\n        Column(\n            Modifier\n                .fillMaxSize()\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n        ) {\n            WithContentColor(myColors.onBackground) {\n                LaunchedEffect(w, h) {\n                    window.minimumSize = Dimension(w, h)\n                }\n                Column {\n                    WindowDraggableArea(Modifier.fillMaxWidth()) {\n                        Text(\n                            \"Extra Config\", Modifier\n                                .padding(vertical = 8.dp)\n                                .fillMaxWidth()\n                                .wrapContentWidth()\n                        )\n                    }\n                    Divider()\n                    Box {\n                        val scrollState = rememberScrollState()\n                        Column(\n                            Modifier.verticalScroll(scrollState)\n                        ) {\n                            for ((index, cfg) in configurables.withIndex()) {\n                                RenderConfigurable(\n                                    cfg,\n                                    ConfigurableUiProps(\n                                        itemPaddingValues = PaddingValues(vertical = 8.dp, horizontal = 32.dp)\n                                    )\n                                )\n                                if (index != configurables.lastIndex) {\n                                    Divider()\n                                }\n                            }\n                        }\n                        MultiplatformVerticalScrollbar(\n                            rememberScrollbarAdapter(scrollState),\n                            Modifier.fillMaxHeight()\n                                .align(Alignment.CenterEnd)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier.fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/shared/LocationTextField.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.shared\n\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MyDropDown\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport com.abdownloadmanager.desktop.ui.util.rememberMyDirectoryPickerLauncher\nimport java.io.File\n\n@Composable\nfun LocationTextField(\n    modifier: Modifier,\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    lastUsedLocations: List<String> = emptyList(),\n    onRequestRemoveSaveLocation: (String) -> Unit,\n) {\n    var showLastUsedLocations by remember { mutableStateOf(false) }\n\n    val downloadLauncherFolderPickerLauncher = rememberMyDirectoryPickerLauncher(\n        title = myStringResource(Res.string.download_location),\n        initialDirectory = remember(text) {\n            runCatching {\n                File(text).canonicalPath\n            }.getOrNull()\n        },\n        attachToWindow = true\n    ) { directory ->\n        directory?.let(setText)\n    }\n\n    var widthForDropDown by remember {\n        mutableStateOf(0.dp)\n    }\n    val density = LocalDensity.current\n    Box(modifier) {\n        MyTextFieldWithIcons(\n            text,\n            setText,\n            myStringResource(Res.string.location),\n            modifier = Modifier\n                .fillMaxWidth()\n                .onGloballyPositioned {\n                    widthForDropDown = with(density) {\n                        it.size.width.toDp()\n                    }\n                },\n            errorText = errorText,\n            end = {\n                Row {\n                    MyTextFieldIcon(MyIcons.folder) {\n                        downloadLauncherFolderPickerLauncher.launch()\n                    }\n                    MyTextFieldIcon(MyIcons.down) {\n                        showLastUsedLocations = !showLastUsedLocations\n                    }\n                }\n            }\n        )\n        if (showLastUsedLocations) {\n            ShowSuggestions(\n                width = { widthForDropDown },\n                suggestions = lastUsedLocations,\n                onSuggestionSelected = {\n                    setText(it)\n                    showLastUsedLocations = false\n                },\n                onDismiss = {\n                    showLastUsedLocations = false\n                },\n                onRequestRemove = onRequestRemoveSaveLocation\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ShowSuggestions(\n    width: () -> Dp,\n    suggestions: List<String>,\n    onRequestRemove: (String) -> Unit,\n    onSuggestionSelected: (String) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    MyDropDown(onDismiss) {\n        Column(\n            Modifier\n                .width(width())\n                .clip(myShapes.defaultRounded)\n                .background(myColors.surface)\n                .verticalScroll(rememberScrollState())\n        ) {\n            for (l in suggestions) {\n                Row(\n                    Modifier.height(IntrinsicSize.Max)\n                ) {\n                    Text(\n                        text = l,\n                        modifier = Modifier\n                            .weight(1f)\n                            .clickable {\n                                onSuggestionSelected(l)\n                            }\n                            .padding(vertical = 4.dp, horizontal = 4.dp),\n                        fontSize = myTextSizes.sm\n                    )\n                    MyIcon(\n                        MyIcons.clear,\n                        null,\n                        Modifier\n                            .fillMaxHeight()\n                            .clickable {\n                                onRequestRemove(l)\n                            }\n                            .wrapContentHeight()\n                            .padding(horizontal = 2.dp)\n                            .size(12.dp)\n                            .alpha(0.25f)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/shared/SelectQueue.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.shared\n\nimport com.abdownloadmanager.desktop.actions.newQueueAction\nimport com.abdownloadmanager.desktop.window.custom.BaseOptionDialog\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.desktop.window.moveSafe\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.window.WindowDraggableArea\nimport androidx.compose.runtime.*\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.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberDialogState\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.MouseInfo\n\n@Composable\nfun ShowAddToQueueDialog(\n    queueList: List<DownloadQueue>,\n    onQueueSelected: (Long?, Boolean) -> Unit,\n    onClose: () -> Unit,\n) {\n    val h = 210\n    val w = 250\n    val state = rememberDialogState(\n        size = DpSize(\n            height = h.dp,\n            width = w.dp,\n        ).applyUiScale(LocalUiScale.current),\n    )\n    val close = {\n        onClose()\n    }\n    val (startQueue, setStartQueue) = remember {\n        mutableStateOf(false)\n    }\n    BaseOptionDialog(\n        onCloseRequest = close,\n        state = state,\n        resizeable = false,\n    ) {\n        LaunchedEffect(window) {\n            window.moveSafe(\n                MouseInfo.getPointerInfo().location.run {\n                    DpOffset(\n                        x = x.dp,\n                        y = y.dp\n                    )\n                }\n            )\n        }\n\n\n        val shape = myShapes.defaultRounded\n        Column(\n            Modifier\n                .fillMaxSize()\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n        ) {\n            WithContentColor(myColors.onBackground) {\n                Column(\n                    Modifier.fillMaxWidth()\n                ) {\n                    WindowDraggableArea(Modifier.fillMaxWidth()) {\n                        Text(\n                            myStringResource(Res.string.select_queue),\n                            modifier = Modifier\n                                .padding(vertical = 8.dp)\n                                .fillMaxWidth()\n                                .wrapContentWidth(),\n                            fontSize = myTextSizes.lg,\n                        )\n                    }\n                    Divider()\n                    Column(\n                        Modifier\n                            .padding(horizontal = 8.dp)\n                            .padding(bottom = 8.dp)\n                    ) {\n                        val addToQueueModifier = Modifier.fillMaxWidth()\n                        Spacer(Modifier.height(8.dp))\n                        Box(\n                            Modifier\n                                .border(1.dp, myColors.onBackground / 5, shape)\n                                .padding(1.dp)\n                                .weight(1f)\n                        ) {\n                            val scrollState = rememberScrollState()\n                            Column(\n                                modifier = Modifier\n                                    .verticalScroll(scrollState)\n                            ) {\n                                for (q in queueList) {\n                                    key(q.id) {\n                                        val queueModel by q.queueModel.collectAsState()\n                                        QueueItemToSelect(\n                                            modifier = addToQueueModifier,\n                                            name = queueModel.name,\n                                            onSelect = {\n                                                onQueueSelected(queueModel.id, startQueue)\n                                            }\n                                        )\n                                    }\n                                }\n                            }\n                            MultiplatformVerticalScrollbar(\n                                rememberScrollbarAdapter(scrollState),\n                                Modifier.fillMaxHeight()\n                                    .align(Alignment.CenterEnd)\n                            )\n                        }\n                        Spacer(Modifier.height(4.dp))\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier\n                                .onClick {\n                                    setStartQueue(!startQueue)\n                                }\n                                .padding(vertical = 4.dp)\n                                .padding(start = 2.dp)\n                        ) {\n                            CheckBox(\n                                size = 14.dp,\n                                value = startQueue,\n                                onValueChange = setStartQueue\n                            )\n                            Spacer(Modifier.width(4.dp))\n                            Text(myStringResource(Res.string.start_queue))\n                        }\n                        Row(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.SpaceBetween\n                        ) {\n                            IconActionButton(\n                                MyIcons.add,\n                                contentDescription = Res.string.add_new_queue.asStringSource(),\n                                onClick = newQueueAction\n                            )\n                            ActionButton(\n                                text = myStringResource(Res.string.without_queue),\n                                modifier = Modifier,\n                                onClick = {\n                                    onQueueSelected(null, startQueue)\n                                }\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun QueueItemToSelect(\n    modifier: Modifier,\n    name: String,\n    onSelect: () -> Unit,\n) {\n    Row(\n        modifier\n            .clickable(onClick = onSelect)\n            .padding(vertical = 4.dp)\n            .padding(horizontal = 4.dp)\n    ) {\n        Text(\n            \"$name\",\n            fontSize = myTextSizes.base,\n        )\n    }\n}\n\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier.fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddDownloadPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.single\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.desktop.window.custom.BaseOptionDialog\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.desktop.window.moveSafe\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.window.WindowDraggableArea\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.*\nimport androidx.compose.ui.window.*\nimport arrow.core.Some\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.*\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.add.CanAddResult\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport ir.amirab.util.compose.asStringSource\nimport java.awt.MouseInfo\n\n@Composable\nfun AddDownloadPage(\n    component: BaseAddSingleDownloadComponent,\n) {\n    val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n    Column(\n        Modifier\n            .padding(horizontal = 32.dp)\n            .padding(top = 8.dp, bottom = 16.dp)\n    ) {\n        val credentials by component.credentials.collectAsState()\n        fun setLink(link: String) {\n            component.setCredentials(\n                credentials.copy(link = Some(link))\n            )\n        }\n\n        HandleEffects(component) {\n            when (it) {\n                is BaseAddSingleDownloadComponent.Effects.Common -> {\n                    when (it) {\n                        is BaseAddSingleDownloadComponent.Effects.Common.SuggestUrl -> {\n                            setLink(it.link)\n                        }\n                    }\n                }\n\n                is BaseAddSingleDownloadComponent.Effects.Platform -> {\n                    // support platform effects if any\n                }\n            }\n        }\n        UrlTextField(\n            text = credentials.link,\n            setText = {\n                setLink(it)\n            },\n            modifier = Modifier\n        )\n        Row(\n        ) {\n            val canAddResult by component.canAddResult.collectAsState()\n            Column(Modifier.weight(1f)) {\n                val useCategory by component.useCategory.collectAsState()\n                Spacer(Modifier.size(8.dp))\n                Row(\n                    modifier = Modifier.height(IntrinsicSize.Max),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier\n                            .onClick {\n                                component.setUseCategory(!useCategory)\n                            }\n                            .padding(vertical = 4.dp)\n                    ) {\n                        CheckBox(\n                            size = 16.dp,\n                            value = useCategory,\n                            onValueChange = { component.setUseCategory(it) }\n                        )\n                        Spacer(Modifier.width(4.dp))\n                        Text(myStringResource(Res.string.use_category))\n                    }\n                    Spacer(Modifier.width(8.dp))\n                    CategorySelect(\n                        modifier = Modifier.weight(1f),\n                        enabled = useCategory,\n                        categories = component.categories.collectAsState().value,\n                        selectedCategory = component.selectedCategory.collectAsState().value,\n                        onCategorySelected = {\n                            component.setSelectedCategory(it)\n                        },\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    CategoryAddButton(\n                        enabled = useCategory,\n                        modifier = Modifier.fillMaxHeight(),\n                        onClick = {\n                            component.addNewCategory()\n                        },\n                    )\n                }\n                Spacer(Modifier.size(8.dp))\n                LocationTextField(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = component.folder.collectAsState().value,\n                    setText = {\n                        component.setFolder(it)\n                    },\n                    errorText = when (canAddResult) {\n                        CanAddResult.CantWriteInThisFolder -> myStringResource(Res.string.cant_write_to_this_folder)\n                        else -> null\n                    },\n                    lastUsedLocations = component.lastUsedLocations.collectAsState().value,\n                    onRequestRemoveSaveLocation = component::removeFromLastDownloadLocation,\n                )\n                val name by component.name.collectAsState()\n                Spacer(Modifier.size(8.dp))\n                NameTextField(\n                    text = name,\n                    setText = {\n                        component.setName(it)\n                    },\n                    errorText = when (canAddResult) {\n                        is CanAddResult.DownloadAlreadyExists -> {\n                            if (onDuplicateStrategy == null) {\n                                myStringResource(Res.string.download_already_exists)\n                            } else {\n                                null\n                            }\n                        }\n\n                        CanAddResult.InvalidFileName -> myStringResource(Res.string.invalid_file_name)\n                        else -> null\n                    }.takeIf { name.isNotEmpty() }\n                )\n            }\n            Spacer(Modifier.size(24.dp))\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = Modifier\n                    .align(Alignment.Top)\n                    .width(IntrinsicSize.Max)\n            ) {\n                RenderFileTypeAndSize(component)\n                RenderResumeSupport(component)\n                ConfigActionsButtons(component)\n            }\n        }\n        Spacer(Modifier.weight(1f))\n        MainActionButtons(component)\n        if (component.showSolutionsOnDuplicateDownloadUi) {\n            ShowSolutionsOnDuplicateDownload(component)\n        }\n        if (component.shouldShowAddToQueue) {\n            ShowAddToQueueDialog(\n                queueList = component.queues.collectAsState().value,\n                onClose = { component.shouldShowAddToQueue = false },\n                onQueueSelected = { queue, startQueue ->\n                    component.onRequestAddToQueue(queue, startQueue)\n                }\n            )\n        }\n        if (component.showMoreSettings) {\n            ExtraConfig(\n                onDismiss = { component.showMoreSettings = false },\n                configurables = component.configurables,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ShowSolutionsOnDuplicateDownload(component: BaseAddSingleDownloadComponent) {\n    val h = 250\n    val w = 300\n    val state = rememberDialogState(\n        size = DpSize(\n            height = Dp.Unspecified,\n            width = Dp.Unspecified,\n        ),\n    )\n    val close = {\n        component.showSolutionsOnDuplicateDownloadUi = false\n    }\n    val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n    BaseOptionDialog(\n        onCloseRequest = close,\n        state = state,\n        resizeable = false,\n    ) {\n        LaunchedEffect(window) {\n            window.moveSafe(\n                MouseInfo.getPointerInfo().location.run {\n                    DpOffset(\n                        x = x.dp,\n                        y = y.dp\n                    )\n                }\n            )\n        }\n\n\n        val shape = myShapes.defaultRounded\n        Column(\n            Modifier\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n        ) {\n            WithContentColor(myColors.onBackground) {\n                Column(\n                    Modifier.widthIn(max = 300.dp)\n                ) {\n                    WindowDraggableArea(Modifier) {\n                        Column(\n                            Modifier.padding(vertical = 8.dp, horizontal = 16.dp)\n                        ) {\n                            Text(\n                                myStringResource(Res.string.select_a_solution),\n                                Modifier,\n                                fontSize = myTextSizes.base\n                            )\n                            Spacer(Modifier.height(8.dp))\n                            WithContentAlpha(0.75f) {\n                                Text(\n                                    myStringResource(Res.string.select_download_strategy_description),\n                                    Modifier,\n                                    fontSize = myTextSizes.sm,\n                                )\n                            }\n                        }\n                    }\n                    Column(\n                        Modifier\n                            .padding(horizontal = 8.dp)\n                            .padding(bottom = 8.dp)\n                    ) {\n                        Spacer(Modifier.height(4.dp))\n                        Divider()\n                        Spacer(Modifier.height(4.dp))\n                        Column {\n                            OnDuplicateStrategySolutionItem(\n                                isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered,\n                                title = myStringResource(Res.string.download_strategy_add_a_numbered_file),\n                                description = myStringResource(Res.string.download_strategy_add_a_numbered_file_description),\n                            ) {\n                                component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered)\n                                close()\n                            }\n                            OnDuplicateStrategySolutionItem(\n                                isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload,\n                                title = myStringResource(Res.string.download_strategy_override_existing_file),\n                                description = myStringResource(Res.string.download_strategy_override_existing_file_description),\n                            ) {\n                                component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload)\n                                close()\n                            }\n                            OnDuplicateStrategySolutionItem(\n                                isSelected = null,\n                                title = myStringResource(Res.string.download_strategy_update_download_link),\n                                description = myStringResource(Res.string.download_strategy_update_download_link_description),\n                            ) {\n                                component.updateDownloadCredentialsOfOriginalDownload()\n                                close()\n                            }\n                            OnDuplicateStrategySolutionItem(\n                                isSelected = null,\n                                title = myStringResource(Res.string.download_strategy_show_downloaded_file),\n                                description = myStringResource(Res.string.download_strategy_show_downloaded_file_description),\n                            ) {\n                                component.openDownloadFileForCurrentLink()\n                                close()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun OnDuplicateStrategySolutionItem(\n    title: String,\n    description: String,\n    isSelected: Boolean?,\n    onClick: () -> Unit,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(8.dp)\n    ) {\n        isSelected?.let {\n            CheckBox(isSelected, { onClick() }, size = 12.dp)\n        }\n        Spacer(Modifier.width(8.dp))\n        Column {\n            Text(\n                title,\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold\n            )\n            Spacer(Modifier.height(4.dp))\n            WithContentAlpha(0.7f) {\n                Text(\n                    text = description,\n                    fontSize = myTextSizes.sm,\n                    modifier = Modifier\n                )\n            }\n        }\n\n    }\n}\n\n\n@Composable\nprivate fun Divider() {\n    Spacer(\n        Modifier.fillMaxWidth()\n            .height(1.dp)\n            .background(myColors.onBackground / 10),\n    )\n}\n\n\n@Composable\nfun RenderResumeSupport(component: BaseAddSingleDownloadComponent) {\n    val fileInfo by component.linkResponseInfo.collectAsState()\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier.height(16.dp)\n\n    ) {\n        val lineModifier = Modifier.weight(1f)\n            .height(1.dp)\n            .background(myColors.onBackground / 10)\n        Box(lineModifier)\n        val canAddToDownloads by component.canAddToDownloads.collectAsState()\n        AnimatedVisibility(\n            visible = canAddToDownloads && fileInfo != null,\n        ) {\n            fileInfo?.let { fileInfo ->\n                if (fileInfo.resumeSupport) {\n                    val iconModifier = Modifier\n                        .padding(horizontal = 2.dp)\n                        .size(10.dp)\n                    if (fileInfo.resumeSupport) {\n                        MyIcon(\n                            icon = MyIcons.check,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.success\n                        )\n                    } else {\n                        MyIcon(\n                            icon = MyIcons.clear,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.error,\n                        )\n                    }\n                }\n            }\n        }\n        Box(lineModifier)\n\n\n    }\n}\n\n@Composable\nprivate fun MainConfigActionButton(\n    text: String,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    ActionButton(text, modifier, enabled, onClick)\n}\n\n\n@Composable\nfun ConfigActionsButtons(component: BaseAddSingleDownloadComponent) {\n    val responseInfo by component.linkResponseInfo.collectAsState()\n    Row {\n        IconActionButton(MyIcons.refresh, Res.string.refresh.asStringSource()) {\n            component.refresh()\n        }\n        Spacer(Modifier.width(6.dp))\n        IconActionButton(\n            MyIcons.settings,\n            Res.string.settings.asStringSource(),\n            indicateActive = component.showMoreSettings,\n            requiresAttention = responseInfo?.requireBasicAuth ?: false\n        ) {\n            component.showMoreSettings = true\n        }\n    }\n}\n\n@Composable\nprivate fun MainActionButtons(component: BaseAddSingleDownloadComponent) {\n    Row {\n        val onDuplicateStrategy by component.onDuplicateStrategy.collectAsState()\n        val canAddResult by component.canAddResult.collectAsState()\n        if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {\n            MainConfigActionButton(\n                text = myStringResource(Res.string.show_solutions),\n                modifier = Modifier,\n                onClick = { component.showSolutionsOnDuplicateDownloadUi = true },\n            )\n            if (component.shouldShowOpenFile.collectAsState().value) {\n                Spacer(Modifier.width(8.dp))\n                MainConfigActionButton(\n                    text = myStringResource(Res.string.open_file),\n                    modifier = Modifier,\n                    onClick = { component.openExistingFile() },\n                )\n            }\n        } else {\n            val canAddToDownloads by component.canAddToDownloads.collectAsState()\n            MainConfigActionButton(\n                text = myStringResource(Res.string.add),\n                modifier = Modifier,\n                enabled = canAddToDownloads,\n                onClick = {\n                    component.shouldShowAddToQueue = true\n                },\n            )\n            Spacer(Modifier.width(8.dp))\n            PrimaryMainActionButton(\n                text = myStringResource(Res.string.download),\n                modifier = Modifier,\n                enabled = canAddToDownloads,\n                onClick = {\n                    component.onRequestDownload()\n                },\n            )\n            if (onDuplicateStrategy != null) {\n                Spacer(Modifier.width(8.dp))\n                MainConfigActionButton(\n                    text = myStringResource(Res.string.change_solution),\n                    modifier = Modifier,\n                    onClick = { component.showSolutionsOnDuplicateDownloadUi = true },\n                )\n            }\n\n        }\n        //        Spacer(Modifier.weight(1f))\n        Spacer(Modifier.weight(1f))\n\n        MainConfigActionButton(\n            text = myStringResource(Res.string.cancel),\n            modifier = Modifier,\n            onClick = {\n                component.onRequestClose()\n            },\n        )\n    }\n}\n\n@Composable\nfun RenderFileTypeAndSize(\n    component: BaseAddSingleDownloadComponent,\n) {\n    val isLinkLoading by component.isLinkLoading.collectAsState()\n    val fileInfo by component.linkResponseInfo.collectAsState()\n    val fileIconProvider = component.iconProvider\n    val iconModifier = Modifier.size(16.dp)\n    Box(Modifier.padding(top = 16.dp)) {\n        AnimatedContent(\n            targetState = isLinkLoading,\n            transitionSpec = {\n                fadeIn() togetherWith fadeOut()\n            }\n        ) { loading ->\n            if (loading) {\n                LoadingIndicator(iconModifier)\n            } else {\n//                val extension = getExtension(fileInfo?.fileName ?: usersSetFileName) ?: \"unknown\"\n                val downloadItem by component.downloadItem.collectAsState()\n                val icon = fileIconProvider.rememberIcon(downloadItem.name)\n\n//                val bitmap = FileIconProvider.getIconOfFileExtension(extension)\n\n                AnimatedContent(\n                    fileInfo,\n                ) { fileInfo ->\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(1f) {\n                            if (fileInfo != null) {\n                                if (fileInfo.requiresAuth) {\n                                    MyIcon(\n                                        MyIcons.lock,\n                                        null,\n                                        iconModifier,\n                                        tint = myColors.error\n                                    )\n                                }\n                                MyIcon(\n                                    icon,\n                                    null,\n                                    iconModifier\n                                )\n                                val size = component.getLengthString()\n                                Spacer(Modifier.width(8.dp))\n                                Text(\n                                    size.rememberString(),\n                                    fontSize = myTextSizes.sm,\n                                )\n                            } else {\n                                MyIcon(\n                                    icon = MyIcons.question,\n                                    contentDescription = null,\n                                    modifier = iconModifier,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nfun getExtension(s: String): String? {\n    if (s.isBlank()) return null\n    return s.substringAfterLast(\".\", \"\")\n        .takeIf { it.isNotBlank() }\n}\n\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    modifier: Modifier = Modifier,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        end = {\n            MyTextFieldIcon(MyIcons.paste) {\n                setText(\n                    ClipboardUtil.read()\n                        .orEmpty()\n                )\n            }\n        },\n        errorText = errorText\n    )\n}\n\n@Composable\nprivate fun NameTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.name),\n        modifier = Modifier.fillMaxWidth(),\n        errorText = errorText,\n    )\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/DesktopAddSingleDownloadComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.addDownload.single\n\nimport com.abdownloadmanager.shared.action.createNewQueueAction\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUi\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.pages.adddownload.single.BaseAddSingleDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.single.OnRequestAddSingleItem\nimport com.abdownloadmanager.shared.pages.adddownload.single.OnRequestDownloadSingleItem\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.activate\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.dismiss\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.CoroutineScope\n\nclass DesktopAddSingleDownloadComponent(\n    ctx: ComponentContext,\n    onRequestClose: () -> Unit,\n    onRequestDownload: OnRequestDownloadSingleItem,\n    onRequestAddToQueue: OnRequestAddSingleItem,\n    openExistingDownload: (Long) -> Unit,\n    updateExistingDownloadCredentials: (Long, IDownloadCredentials, DownloadJobExtraConfig?) -> Unit,\n    downloadItemOpener: DownloadItemOpener,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n    downloadSystem: DownloadSystem,\n    appSettings: BaseAppSettingsStorage,\n    iconProvider: FileIconProvider,\n    appScope: CoroutineScope,\n    appRepository: BaseAppRepository,\n    perHostSettingsManager: PerHostSettingsManager,\n    importOptions: ImportOptions,\n    id: String,\n    downloaderInUi: DownloaderInUi<IDownloadCredentials, *, *, *, *, *, *, *, *, *>,\n    initialCredentials: AddDownloadCredentialsInUiProps,\n    private val categoryDialogManager: CategoryDialogManager,\n) : BaseAddSingleDownloadComponent(\n    ctx = ctx,\n    onRequestClose = onRequestClose,\n    onRequestDownload = onRequestDownload,\n    onRequestAddToQueue = onRequestAddToQueue,\n    openExistingDownload = openExistingDownload,\n    updateExistingDownloadCredentials = updateExistingDownloadCredentials,\n    downloadItemOpener = downloadItemOpener,\n    lastSavedLocationsStorage = lastSavedLocationsStorage,\n    importOptions = importOptions,\n    id = id,\n    downloaderInUi = downloaderInUi,\n    initialCredentials = initialCredentials,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n    downloadSystem = downloadSystem,\n    appSettings = appSettings,\n    iconProvider = iconProvider,\n    appScope = appScope,\n    appRepository = appRepository,\n    perHostSettingsManager = perHostSettingsManager,\n) {\n    override fun getCategoryPageManager(): CategoryDialogManager {\n        return categoryDialogManager\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.batchdownload\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.pages.batchdownload.BaseBatchDownloadComponent\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun BatchDownloadWindow(desktopBatchDownloadComponent: DesktopBatchDownloadComponent) {\n    CustomWindow(\n        state = rememberWindowState(\n            size = DpSize(500.dp, 420.dp)\n                .applyUiScale(LocalUiScale.current),\n            position = WindowPosition(Alignment.Center)\n        ),\n        onCloseRequest = desktopBatchDownloadComponent.onClose\n    ) {\n        HandleEffects(desktopBatchDownloadComponent) {\n            when (it) {\n                DesktopBatchDownloadComponent.Effects.BringToFront -> window.toFront()\n                is BaseBatchDownloadComponent.Effects.PlatformEffects -> {\n                    //\n                }\n            }\n        }\n        BatchDownload(desktopBatchDownloadComponent)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownnload.kt",
    "content": "package com.abdownloadmanager.desktop.pages.batchdownload\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.*\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.unit.dp\nimport com.abdownloadmanager.desktop.pages.batchdownload.WildcardSelect.*\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.batchdownload.BatchDownloadValidationResult\nimport com.abdownloadmanager.shared.pages.batchdownload.WildcardLength\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun BatchDownload(\n    component: DesktopBatchDownloadComponent,\n) {\n    WindowTitle(myStringResource(Res.string.batch_download))\n    val link by component.link.collectAsState()\n    val setLink = component::setLink\n    val start by component.start.collectAsState()\n    val setStart = component::setStart\n    val end by component.end.collectAsState()\n    val setEnd = component::setEnd\n    val scrollState = rememberScrollState()\n    val scrollAdapter = rememberScrollbarAdapter(scrollState)\n    val validationResult by component.validationResult.collectAsState()\n    val linkFocusRequester = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        linkFocusRequester.requestFocus()\n    }\n    Column(Modifier.padding(16.dp)) {\n        Row(Modifier.weight(1f)) {\n            Column(\n                modifier = Modifier\n                    .weight(1f)\n                    .verticalScroll(scrollState)\n            ) {\n                LabeledContent(\n                    label = {\n                        Text(myStringResource(Res.string.batch_download_link_help))\n                    },\n                    content = {\n                        MyTextFieldWithIcons(\n                            text = link,\n                            onTextChange = setLink,\n                            placeholder = \"https://example.com/photo-*.png\",\n                            modifier = Modifier\n                                .focusRequester(linkFocusRequester)\n                                .fillMaxWidth(),\n                            start = {\n                                MyTextFieldIcon(MyIcons.link)\n                            },\n                            end = {\n                                MyTextFieldIcon(MyIcons.paste) {\n                                    val v = ClipboardUtil.read()\n                                    if (v != null) {\n                                        setLink(v)\n                                    }\n                                }\n                            },\n                            errorText = when (val v = validationResult) {\n                                BatchDownloadValidationResult.URLInvalid -> {\n                                    myStringResource(Res.string.invalid_url)\n                                }\n\n                                is BatchDownloadValidationResult.MaxRangeExceed -> myStringResource(\n                                    Res.string.list_is_too_large_maximum_n_items_allowed,\n                                    Res.string.list_is_too_large_maximum_n_items_allowed_createArgs(\n                                        count = v.allowed.toString()\n                                    )\n                                )\n\n                                BatchDownloadValidationResult.Others -> null\n                                BatchDownloadValidationResult.Ok -> null\n                            }\n                        )\n                    }\n                )\n                Spacer(Modifier.height(8.dp))\n                LabeledContent(\n                    label = {\n                        Text(myStringResource(Res.string.enter_range))\n                    },\n                    content = {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            MyTextFieldWithIcons(\n                                text = start,\n                                onTextChange = setStart,\n                                placeholder = \"\",\n                                modifier = Modifier.width(90.dp),\n                                start = {\n                                    Text(\n                                        \"${myStringResource(Res.string.range_from)}:\",\n                                        Modifier.padding(horizontal = 8.dp)\n                                    )\n                                }\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(\"...\")\n                            Spacer(Modifier.width(8.dp))\n\n                            MyTextFieldWithIcons(\n                                text = end,\n                                onTextChange = setEnd,\n                                placeholder = \"\",\n                                modifier = Modifier.width(90.dp),\n                                start = {\n                                    Text(\n                                        \"${myStringResource(Res.string.range_to)}:\",\n                                        Modifier.padding(horizontal = 8.dp)\n                                    )\n                                }\n                            )\n                        }\n                    }\n                )\n                Spacer(Modifier.height(8.dp))\n                LabeledContent(\n                    label = {\n                        Text(myStringResource(Res.string.batch_download_wildcard_length))\n                    },\n                    content = {\n                        WildcardLengthUi(\n                            component.wildcardLength.collectAsState().value,\n                            component::setWildCardLength\n                        )\n                    }\n                )\n                Spacer(Modifier.height(8.dp))\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    val lineModifier = Modifier\n                        .height(1.dp)\n                        .padding(horizontal = 5.dp)\n                        .background(LocalContentColor.current.copy(0.05f))\n\n                    Spacer(Modifier.padding(vertical = 4.dp).fillMaxWidth().then(lineModifier))\n                }\n                Spacer(Modifier.height(8.dp))\n                LabeledContent(\n                    label = {\n                        Text(myStringResource(Res.string.first_link))\n                    },\n                    content = {\n                        LinkPreview(component.startLinkResult.collectAsState().value)\n                    }\n                )\n                Spacer(Modifier.height(8.dp))\n                LabeledContent(\n                    label = {\n                        Text(myStringResource(Res.string.last_link))\n                    },\n                    content = {\n                        LinkPreview(component.endLinkResult.collectAsState().value)\n                    }\n                )\n            }\n            MultiplatformVerticalScrollbar(scrollAdapter, Modifier.fillMaxHeight())\n        }\n        Spacer(Modifier.height(8.dp))\n        Row(\n            modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)\n        ) {\n            ActionButton(\n                text = myStringResource(Res.string.ok),\n                enabled = component.canConfirm.collectAsState().value,\n                onClick = component::confirm\n            )\n            Spacer(Modifier.width(8.dp))\n            ActionButton(myStringResource(Res.string.close), onClick = component.onClose)\n        }\n    }\n}\n\n@Composable\nfun LinkPreview(link: String) {\n    Text(\n        link,\n        Modifier\n            .fillMaxWidth()\n            .clip(myShapes.defaultRounded)\n            .background(myColors.surface)\n            .padding(vertical = 4.dp, horizontal = 6.dp)\n    )\n}\n\nenum class WildcardSelect(\n    val text: StringSource,\n) {\n    Auto(Res.string.auto.asStringSource()),\n    Unspecified(Res.string.unspecified.asStringSource()),\n    Custom(Res.string.custom.asStringSource());\n\n    companion object {\n        fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect {\n            return when (wildcardLength) {\n                WildcardLength.Auto -> Auto\n                is WildcardLength.Custom -> Custom\n                WildcardLength.Unspecified -> Unspecified\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun WildcardLengthUi(\n    wildcardLength: WildcardLength,\n    onChangeWildcardLength: (WildcardLength) -> Unit,\n) {\n    var customLength by remember {\n        mutableStateOf(2)\n    }\n    Row(\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Multiselect(\n            selections = entries,\n            selectedItem = WildcardSelect.fromWildcardLength(wildcardLength),\n            onSelectionChange = {\n                onChangeWildcardLength(\n                    when (it) {\n                        Auto -> WildcardLength.Auto\n                        Unspecified -> WildcardLength.Unspecified\n                        Custom -> WildcardLength.Custom(customLength)\n                    }\n                )\n            },\n            render = {\n                Text(it.text.rememberString())\n            }\n        )\n        AnimatedVisibility(wildcardLength is WildcardLength.Custom) {\n            Row {\n                Spacer(Modifier.width(8.dp))\n                IntTextField(\n                    value = customLength,\n                    onValueChange = {\n                        customLength = it\n                        onChangeWildcardLength(\n                            WildcardLength.Custom(it)\n                        )\n                    },\n                    range = 1..10,\n                    keyboardOptions = KeyboardOptions.Default,\n                    modifier = Modifier.width(72.dp)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LabeledContent(\n    label: @Composable () -> Unit,\n    content: @Composable () -> Unit,\n) {\n    Column {\n        label()\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/DesktopBatchDownloadComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.batchdownload\n\nimport com.abdownloadmanager.shared.pages.batchdownload.BaseBatchDownloadComponent\nimport com.arkivanov.decompose.ComponentContext\n\nclass DesktopBatchDownloadComponent(\n    ctx: ComponentContext,\n    onClose: () -> Unit,\n    importLinks: (List<String>) -> Unit,\n) : BaseBatchDownloadComponent(\n    ctx = ctx,\n    onClose = onClose,\n    importLinks = importLinks\n) {\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    sealed interface Effects : BaseBatchDownloadComponent.Effects.PlatformEffects {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/DesktopCategoryDialogManager.kt",
    "content": "package com.abdownloadmanager.desktop.pages.category\n\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport kotlinx.coroutines.flow.StateFlow\n\ninterface DesktopCategoryDialogManager : CategoryDialogManager {\n    val openedCategoryDialogs: StateFlow<List<CategoryComponent>>\n    fun closeCategoryDialog(categoryId: Long)\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/NewCategoryPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.category\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport com.abdownloadmanager.desktop.ui.util.rememberMyDirectoryPickerLauncher\nimport java.io.File\n\n@Composable\nfun NewCategory(\n    categoryComponent: CategoryComponent,\n) {\n    WindowTitle(\n        myStringResource(\n            if (categoryComponent.isEditMode) {\n                Res.string.edit_category\n            } else {\n                Res.string.add_category\n            }\n        )\n    )\n    Column(\n        modifier = Modifier\n            .padding(horizontal = 32.dp)\n            .padding(vertical = 16.dp)\n    ) {\n        Column(\n            Modifier\n                .weight(1f)\n                .verticalScroll(rememberScrollState())\n        ) {\n            Row {\n                CategoryIcon(\n                    iconSource = categoryComponent.icon.collectAsState().value,\n                    onChange = categoryComponent::setIcon\n                )\n                Spacer(Modifier.width(16.dp))\n                CategoryName(\n                    modifier = Modifier.weight(1f),\n                    name = categoryComponent.name.collectAsState().value,\n                    onNameChanged = categoryComponent::setName\n                )\n            }\n            Spacer(Modifier.height(12.dp))\n            CategoryAutoTypes(\n                types = categoryComponent.types.collectAsState().value,\n                onTypesChanged = categoryComponent::setTypes,\n                enabled = categoryComponent.typesEnabled.collectAsState().value,\n                setEnabled = categoryComponent::setTypesEnabled\n            )\n            Spacer(Modifier.height(12.dp))\n            CategoryAutoUrls(\n                urlPatterns = categoryComponent.urlPatterns.collectAsState().value,\n                onUrlPatternChanged = categoryComponent::setUrlPatterns,\n                enabled = categoryComponent.urlPatternsEnabled.collectAsState().value,\n                setEnabled = categoryComponent::setUrlPatternsEnabled\n            )\n            Spacer(Modifier.height(12.dp))\n            CategoryDefaultPath(\n                path = categoryComponent.path.collectAsState().value,\n                onPathChanged = categoryComponent::setPath,\n                defaultDownloadLocation = categoryComponent.defaultDownloadLocation.collectAsState().value,\n                checked = categoryComponent.usePath.collectAsState().value,\n                setChecked = categoryComponent::setUsePath\n            )\n        }\n        Spacer(Modifier.height(12.dp))\n        Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {\n            ActionButton(\n                myStringResource(\n                    when (categoryComponent.isEditMode) {\n                        true -> Res.string.change\n                        false -> Res.string.add\n                    }\n                ),\n                enabled = categoryComponent.canSubmit.collectAsState().value,\n                onClick = {\n                    categoryComponent.submit()\n                }\n            )\n            Spacer(Modifier.width(8.dp))\n            ActionButton(\n                myStringResource(Res.string.cancel),\n                onClick = {\n                    categoryComponent.close()\n                }\n            )\n        }\n    }\n}\n\n@Composable\nfun CategoryDefaultPath(\n    defaultDownloadLocation: String,\n    path: String,\n    onPathChanged: (String) -> Unit,\n    checked: Boolean,\n    setChecked: (Boolean) -> Unit,\n) {\n    val initialDirectory = remember(path, defaultDownloadLocation) {\n        path\n            .takeIf { it.isNotBlank() }\n            ?.let {\n                runCatching {\n                    File(path).canonicalPath\n                }.getOrNull()\n            } ?: defaultDownloadLocation\n    }\n    val downloadFolderPickerLauncher = rememberMyDirectoryPickerLauncher(\n        title = myStringResource(Res.string.category_download_location),\n        initialDirectory = initialDirectory,\n        attachToWindow = true\n    ) { directory ->\n        directory?.let(onPathChanged)\n    }\n\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_download_location),\n        helpText = myStringResource(Res.string.category_download_location_description),\n        enabled = checked,\n        setEnabled = setChecked,\n    ) {\n        MyTextFieldWithIcons(\n            text = path,\n            onTextChange = onPathChanged,\n            modifier = Modifier.fillMaxWidth(),\n            enabled = checked,\n            placeholder = \"\",\n            errorText = null,\n            end = {\n                MyTextFieldIcon(\n                    MyIcons.folder,\n                    enabled = checked,\n                ) {\n                    downloadFolderPickerLauncher.launch()\n                }\n            }\n        )\n    }\n}\n\n@Composable\nfun CategoryAutoTypes(\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    types: String,\n    onTypesChanged: (String) -> Unit,\n) {\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_file_types),\n        helpText = myStringResource(Res.string.category_file_types_description),\n        enabled = enabled,\n        setEnabled = setEnabled,\n    ) {\n        MyTextFieldWithIcons(\n            text = types,\n            onTextChange = onTypesChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"ext1 ext2 ext3\",\n            enabled = enabled,\n            singleLine = false,\n        )\n    }\n}\n\n@Composable\nfun CategoryAutoUrls(\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    urlPatterns: String,\n    onUrlPatternChanged: (String) -> Unit,\n) {\n    OptionalWithLabel(\n        label = myStringResource(Res.string.category_url_patterns),\n        helpText = myStringResource(Res.string.category_url_patterns_description),\n        enabled = enabled,\n        setEnabled = setEnabled\n    ) {\n        MyTextFieldWithIcons(\n            text = urlPatterns,\n            onTextChange = onUrlPatternChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"dl.example.com/pics example.com/*/path\",\n            enabled = enabled,\n            singleLine = false,\n        )\n    }\n}\n\n@Composable\nfun CategoryName(\n    name: String,\n    onNameChanged: (String) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    WithLabel(\n        myStringResource(Res.string.category_name),\n        modifier,\n    ) {\n        MyTextFieldWithIcons(\n            text = name,\n            onTextChange = onNameChanged,\n            modifier = Modifier.fillMaxWidth(),\n            placeholder = \"Something...\",\n        )\n    }\n}\n\n@Composable\nprivate fun WithLabel(\n    label: String,\n    modifier: Modifier = Modifier,\n    helpText: String? = null,\n    content: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(label)\n            helpText?.let {\n                Spacer(Modifier.width(8.dp))\n                Help(helpText)\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n\n@Composable\nprivate fun OptionalWithLabel(\n    label: String,\n    modifier: Modifier = Modifier,\n    enabled: Boolean,\n    setEnabled: (Boolean) -> Unit,\n    helpText: String? = null,\n    content: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Row(\n                modifier = Modifier.onClick {\n                    setEnabled(!enabled)\n                },\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CheckBox(enabled, setEnabled, size = 16.dp)\n                Spacer(Modifier.width(8.dp))\n                Text(label)\n            }\n            helpText?.let {\n                Spacer(Modifier.width(8.dp))\n                Help(helpText)\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        content()\n    }\n}\n\n@Composable\nprivate fun CategoryIcon(\n    iconSource: IconSource?,\n    onChange: (IconSource) -> Unit,\n) {\n    var showIconPicker by remember {\n        mutableStateOf(false)\n    }\n    WithLabel(\n        myStringResource(Res.string.icon)\n    ) {\n        RenderIcon(\n            icon = iconSource,\n            requiresAttention = iconSource == null,\n            onClick = {\n                showIconPicker = !showIconPicker\n            }\n        )\n        if (showIconPicker) {\n            IconPick(\n                selectedIcon = iconSource,\n                icons = listOf(\n                    MyIcons.pictureFile,\n                    MyIcons.musicFile,\n                    MyIcons.zipFile,\n                    MyIcons.videoFile,\n                    MyIcons.applicationFile,\n                    MyIcons.documentFile,\n                    MyIcons.otherFile,\n\n                    MyIcons.file,\n                    MyIcons.folder,\n\n                    MyIcons.browserIntegration,\n                    MyIcons.appearance,\n\n                    MyIcons.settings,\n                    MyIcons.search,\n                    MyIcons.info,\n                    MyIcons.check,\n                    MyIcons.link,\n                    MyIcons.download,\n                    MyIcons.speaker,\n                    MyIcons.group,\n                    MyIcons.activeCount,\n                    MyIcons.speed,\n                    MyIcons.resume,\n                    MyIcons.pause,\n                    MyIcons.stop,\n                    MyIcons.queue,\n                    MyIcons.remove,\n                    MyIcons.clear,\n                    MyIcons.add,\n                    MyIcons.paste,\n                    MyIcons.copy,\n                    MyIcons.refresh,\n                    MyIcons.share,\n                    MyIcons.lock,\n                    MyIcons.question,\n                    MyIcons.verticalDirection,\n                    MyIcons.downloadEngine,\n                    MyIcons.network,\n                    MyIcons.externalLink,\n                ),\n                onSelected = {\n                    onChange(it)\n                    showIconPicker = false\n                },\n                onCancel = {\n                    showIconPicker = false\n                }\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderIcon(\n    icon: IconSource?,\n    indicateActive: Boolean = false,\n    requiresAttention: Boolean = false,\n    onClick: () -> Unit,\n) {\n    val shape = RoundedCornerShape(10.dp)\n    Box(\n        Modifier\n            .border(\n                1.dp,\n                myColors.onBackground / 10,\n                shape\n            )\n            .ifThen(indicateActive || requiresAttention) {\n                border(\n                    1.dp,\n                    myColors.primary / if (indicateActive) 1f else alphaFlicker(),\n                    shape\n                )\n            }\n            .clip(shape)\n            .background(myColors.surface)\n            .clickable {\n                onClick()\n            }\n            .padding(6.dp)\n    ) {\n        val modifier = Modifier\n            .size(20.dp)\n        if (icon != null) {\n            MyIcon(\n                icon,\n                null,\n                modifier,\n            )\n        } else {\n            Spacer(modifier)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/ShowCategoryDialogs.kt",
    "content": "package com.abdownloadmanager.desktop.pages.category\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.pages.category.CategoryComponent\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun ShowCategoryDialogs(dialogManager: DesktopCategoryDialogManager) {\n    val dialogs by dialogManager.openedCategoryDialogs.collectAsState()\n    for (d in dialogs) {\n        CategoryDialog(d)\n    }\n}\n\n@Composable\nprivate fun CategoryDialog(\n    component: CategoryComponent,\n) {\n    CustomWindow(\n        onCloseRequest = {\n            component.close()\n        },\n        alwaysOnTop = true,\n        state = rememberWindowState(\n            size = DpSize(350.dp, 400.dp).applyUiScale(LocalUiScale.current),\n            position = WindowPosition.Aligned(Alignment.Center),\n        )\n    ) {\n        NewCategory(component)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/checksum/DesktopFileChecksumComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.checksum\n\nimport com.abdownloadmanager.shared.pages.checksum.BaseFileChecksumComponent\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.arkivanov.decompose.ComponentContext\nimport java.util.UUID\n\nclass DesktopFileChecksumComponent(\n    ctx: ComponentContext,\n    id: String,\n    itemIds: List<Long>,\n    closeComponent: () -> Unit,\n    downloadSystem: DownloadSystem\n) : BaseFileChecksumComponent(\n    ctx = ctx,\n    id = id,\n    itemIds = itemIds,\n    closeComponent = closeComponent,\n    downloadSystem = downloadSystem,\n) {\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    data class Config(\n        val id: String = UUID.randomUUID().toString(),\n        override val itemIds: List<Long>,\n    ) : BaseFileChecksumComponent.Config\n\n    sealed interface Effects : BaseFileChecksumComponent.Effects.Platform {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/checksum/FileChecksumPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.checksum\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.checksum.ChecksumStatus\nimport com.abdownloadmanager.shared.pages.checksum.DownloadItemWithChecksum\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Help\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.styled.MyStyledTableHeader\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.WithContextMenu\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MyDropDown\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.FileChecksumAlgorithm\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberDotLoading\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport kotlinx.coroutines.flow.MutableStateFlow\n\n@Composable\nfun FileChecksumPage(component: DesktopFileChecksumComponent) {\n    WindowTitle(myStringResource(Res.string.file_checksum_page))\n    val horizontalPadding = 16.dp\n    Column {\n        Table(\n            modifier = Modifier.weight(1f).fillMaxWidth(),\n            list = component.state.collectAsState().value.items,\n            tableState = remember {\n                TableState(FileChecksumTableCells.cells)\n            },\n            wrapHeader = {\n                MyStyledTableHeader(\n                    itemHorizontalPadding = horizontalPadding,\n                    content = it,\n                )\n            },\n            wrapItem = { index, item, content ->\n                Box(Modifier.padding(horizontal = horizontalPadding).let {\n                    val mutableInteractionSource = remember { MutableInteractionSource() }\n                    it.indication(mutableInteractionSource, LocalIndication.current)\n                        .hoverable(mutableInteractionSource)\n                }) {\n                    content()\n                }\n            },\n            renderCell = { cell, item ->\n                when (cell) {\n                    FileChecksumTableCells.Name -> {\n                        FileChecksumTableCellRenderers.RenderName(item)\n                    }\n\n                    FileChecksumTableCells.Status -> {\n                        FileChecksumTableCellRenderers.RenderStatus(item)\n                    }\n\n                    FileChecksumTableCells.Algorithm -> {\n                        FileChecksumTableCellRenderers.RenderAlgorithm(item)\n                    }\n\n                    FileChecksumTableCells.CalculatedChecksum -> {\n                        FileChecksumTableCellRenderers.RenderCalculatedChecksum(item)\n                    }\n\n                    FileChecksumTableCells.SavedChecksum -> {\n                        FileChecksumTableCellRenderers.RenderSavedChecksum(\n                            item = item,\n                            onRequestSaveNewChecksum = {\n                                component.updateChecksum(item.downloadItem.id, it)\n                            }\n                        )\n                    }\n                }\n            })\n        Actions(\n            Modifier,\n            component,\n        )\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier,\n    component: DesktopFileChecksumComponent,\n) {\n    val uiState by component.state.collectAsState()\n    Column(modifier) {\n        Spacer(\n            Modifier.fillMaxWidth().height(1.dp).background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier.fillMaxWidth().background(myColors.surface / 0.5f).padding(horizontal = 16.dp)\n                .padding(vertical = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm)\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Help(\n                        myStringResource(Res.string.file_checksum_page_file_checksum_default_algorithm_help)\n                    )\n                }\n                Spacer(Modifier.size(8.dp))\n                RenderSpinner(\n                    modifier = Modifier,\n                    possibleValues = FileChecksumAlgorithm.all(),\n                    value = uiState.defaultAlgorithm,\n                    enabled = !uiState.isChecking,\n                    onSelect = {\n                        component.onAlgorithmChange(it)\n                    },\n                    render = {\n                        Text(it.algorithm)\n                    })\n            }\n            Spacer(Modifier.weight(1f))\n            Row {\n                ActionButton(\n                    myStringResource(Res.string.start),\n                    onClick = component::onRequestStartCheck,\n                    enabled = !uiState.isChecking\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(\n                    myStringResource(Res.string.close),\n                    onClick = component::onRequestClose,\n                )\n            }\n        }\n    }\n\n}\n\n\nprivate data object FileChecksumTableCellRenderers {\n    private val itemVerticalPadding = 8.dp\n\n    @Composable\n    private fun CellContent(\n        content: @Composable () -> Unit\n    ) {\n        Box(\n            modifier = Modifier.padding(vertical = itemVerticalPadding)\n        ) {\n            content()\n        }\n    }\n\n    @Composable\n    fun RenderName(item: DownloadItemWithChecksum) {\n        CellContent {\n            SimpleText(item.downloadItem.name)\n        }\n    }\n\n    @Composable\n    fun RenderStatus(item: DownloadItemWithChecksum) {\n        CellContent {\n            when (val status = item.checksumStatus) {\n                is ChecksumStatus.Checking -> {\n                    RenderCheckingStatus(status.percent)\n                }\n\n                ChecksumStatus.Error.DownloadNotFinished -> {\n                    RenderErrorStatus(myStringResource(Res.string.download_not_finished))\n                }\n\n                is ChecksumStatus.Error.Exception -> {\n                    RenderErrorStatus(status.t.localizedMessage ?: status.t::class.simpleName.orEmpty())\n                }\n\n                ChecksumStatus.Error.FileNotFound -> {\n                    RenderErrorStatus(myStringResource(Res.string.file_not_found))\n                }\n\n                is ChecksumStatus.Finished -> {\n                    RenderFinishedStatus(\n                        status = status,\n                    )\n                }\n\n                ChecksumStatus.Waiting -> {\n                    RenderWaitingStatus()\n                }\n            }\n        }\n    }\n\n    @Composable\n    fun RenderAlgorithm(item: DownloadItemWithChecksum) {\n        CellContent {\n            SimpleText(item.algorithm)\n        }\n    }\n\n    @Composable\n    fun RenderCalculatedChecksum(item: DownloadItemWithChecksum) {\n        val calculatedChecksum = item.calculatedChecksum\n        WithContextMenu(\n            menuProvider = {\n                buildMenu {\n                    if (calculatedChecksum != null) {\n                        item(\n                            title = Res.string.copy.asStringSource(),\n                            icon = MyIcons.copy,\n                            onClick = {\n                                ClipboardUtil.copy(calculatedChecksum)\n                            }\n                        )\n                    }\n                }\n            }\n        ) {\n            if (calculatedChecksum != null) {\n                SimpleText(calculatedChecksum)\n            } else if (item.isProcessing) {\n                //shimmer\n                ShimmerEffect(\n                    centerColor = myColors.onBackground / 0.4f,\n                    surroundingColor = myColors.onBackground / 0.1f,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .clip(myShapes.defaultRounded)\n                        .height(myTextSizes.base.value.dp)\n                )\n            } else if (item.isError) {\n                SimpleText(\"!\")\n            }\n        }\n    }\n\n    @Composable\n    fun RenderSavedChecksum(\n        item: DownloadItemWithChecksum,\n        onRequestSaveNewChecksum: (FileChecksum?) -> Unit\n    ) {\n        fun createMenu(\n            item: DownloadItemWithChecksum,\n            onRequestEdit: (Boolean) -> Unit,\n        ): List<MenuItem> {\n            val savedChecksum = item.savedChecksum\n            return buildMenu {\n                if (savedChecksum != null) {\n                    item(\n                        title = Res.string.copy.asStringSource(),\n                        icon = MyIcons.copy,\n                        onClick = {\n                            ClipboardUtil.copy(savedChecksum)\n                        },\n                    )\n                }\n                item(\n                    title = Res.string.edit.asStringSource(),\n                    icon = MyIcons.edit,\n                    onClick = {\n                        onRequestEdit(true)\n                    },\n                )\n            }\n        }\n\n        var edit by remember { mutableStateOf(false) }\n        WithContextMenu(\n            menuProvider = {\n                createMenu(\n                    item = item,\n                    onRequestEdit = { edit = it }\n                )\n            }\n        ) {\n            Column(Modifier.fillMaxSize()) {\n                CellContent {\n                    SimpleText(item.savedChecksum.orEmpty())\n                }\n                if (edit) {\n                    ChecksumEditDropDown(\n                        item = item,\n                        onRequestSaveNewChecksum = {\n                            onRequestSaveNewChecksum(it)\n                            edit = false\n                        },\n                        onCloseRequest = {\n                            edit = false\n                        },\n                    )\n                }\n            }\n        }\n    }\n\n    @Composable\n    private fun ChecksumEditDropDown(\n        item: DownloadItemWithChecksum,\n        onCloseRequest: () -> Unit,\n        onRequestSaveNewChecksum: (FileChecksum?) -> Unit,\n    ) {\n        val editChecksumFlow = remember {\n            MutableStateFlow<FileChecksum?>(FileChecksum(item.algorithm, item.savedChecksum.orEmpty()))\n        }\n        val fileChecksumConfigurable = remember {\n            FileChecksumConfigurable(\n                title = Res.string.download_item_settings_file_checksum.asStringSource(),\n                description = Res.string.download_item_settings_file_checksum_description.asStringSource(),\n                backedBy = editChecksumFlow,\n                describe = {\n                    \"\".asStringSource()\n                },\n            )\n        }\n        MyDropDown(\n            onDismissRequest = onCloseRequest,\n            anchor = Alignment.BottomEnd,\n            alignment = Alignment.BottomStart,\n        ) {\n            val shape = myShapes.defaultRounded\n            Column(\n                Modifier\n                    .shadow(24.dp)\n                    .clip(shape)\n                    .width(350.dp)\n                    .border(1.dp, myColors.surface, shape)\n                    .background(myColors.menuGradientBackground)\n                    .padding(horizontal = 16.dp, vertical = 12.dp)\n            ) {\n                RenderConfigurable(fileChecksumConfigurable, ConfigurableUiProps())\n                ActionButton(\n                    text = myStringResource(Res.string.update),\n                    modifier = Modifier\n                        .align(Alignment.End),\n                    onClick = {\n                        val newChecksum = editChecksumFlow.value\n                        onRequestSaveNewChecksum(\n                            newChecksum.takeIf { it?.value?.isNotEmpty() ?: false }\n                        )\n                    }\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun ShimmerEffect(\n        modifier: Modifier = Modifier,\n        centerColor: Color = Color.Gray,\n        surroundingColor: Color = Color.Gray,\n    ) {\n        val transition = rememberInfiniteTransition()\n        val translateAnim = transition.animateFloat(\n            initialValue = 0f,\n            targetValue = 1000f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(\n                    durationMillis = 3000,\n                    easing = LinearEasing\n                )\n            )\n        )\n\n        val brush = Brush.linearGradient(\n            colors = listOf(\n                surroundingColor,\n                centerColor,\n                surroundingColor,\n            ),\n            start = Offset(0f, 0f),\n            end = Offset(translateAnim.value, 0f)\n        )\n\n        Box(\n            modifier = modifier\n                .background(brush = brush)\n        )\n    }\n\n    @Composable\n    private fun RenderErrorStatus(message: String) {\n        IconWithText(\n            icon = MyIcons.info,\n            text = message,\n            color = myColors.error,\n        )\n    }\n\n    @Composable\n    private fun RenderFinishedStatus(\n        status: ChecksumStatus.Finished,\n    ) {\n        val text: StringSource\n        val color: Color\n        val icon: IconSource\n        when (status) {\n            ChecksumStatus.Finished.Done -> {\n                text = Res.string.done.asStringSource()\n                icon = MyIcons.check\n                color = myColors.info\n            }\n\n            ChecksumStatus.Finished.Matches -> {\n                text = Res.string.matches.asStringSource()\n                icon = MyIcons.check\n                color = myColors.success\n            }\n\n            ChecksumStatus.Finished.NotMatches -> {\n                text = Res.string.not_matches.asStringSource()\n                icon = MyIcons.info\n                color = myColors.warning\n            }\n        }\n        IconWithText(\n            icon = icon,\n            text = text.rememberString(),\n            color = color,\n        )\n    }\n\n    @Composable\n    private fun IconWithText(\n        icon: IconSource,\n        text: String,\n        color: Color,\n    ) {\n        WithContentColor(color) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                MyIcon(\n                    icon,\n                    modifier = Modifier.size(16.dp),\n                    contentDescription = null,\n                )\n                Spacer(Modifier.width(2.dp))\n                SimpleText(text)\n            }\n        }\n    }\n\n    @Composable\n    private fun RenderCheckingStatus(percent: Int) {\n        Column {\n            ProgressStatus(percent, myColors.primaryGradient)\n        }\n    }\n\n    @Composable\n    private fun RenderWaitingStatus() {\n        Row {\n            SimpleText(\"${myStringResource(Res.string.waiting)} ${rememberDotLoading()}\")\n        }\n    }\n\n    @Composable\n    private fun ProgressStatus(\n        percent: Int?,\n        background: Brush = myColors.primaryGradient,\n    ) {\n        Box(\n            Modifier.fillMaxWidth().clip(CircleShape).background(myColors.surface)\n        ) {\n            if (percent != null) {\n                val w = (percent / 100f).coerceIn(0f..1f)\n                Spacer(\n                    Modifier.height(5.dp).fillMaxWidth(\n                        animateFloatAsState(\n                            w, tween(100)\n                        ).value\n                    ).background(background)\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun SimpleText(string: String, modifier: Modifier = Modifier) {\n        Text(\n            string,\n            modifier = modifier,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\nprivate sealed class FileChecksumTableCells : TableCell<DownloadItemWithChecksum> {\n    data object Name : FileChecksumTableCells() {\n        override val id: String = \"name\"\n        override val name: StringSource = Res.string.name.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 300.dp)\n    }\n\n    data object Status : FileChecksumTableCells() {\n        override val id: String = \"status\"\n        override val name: StringSource = Res.string.status.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)\n    }\n\n    data object Algorithm : FileChecksumTableCells() {\n        override val id: String = \"algorithm\"\n        override val name: StringSource = Res.string.checksum_algorithm.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(60.dp..300.dp, 60.dp)\n    }\n\n    data object SavedChecksum : FileChecksumTableCells() {\n        override val id: String = \"saved_checksum\"\n        override val name: StringSource = Res.string.saved_checksum.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)\n    }\n\n    data object CalculatedChecksum : FileChecksumTableCells() {\n        override val id: String = \"calculated_checksum\"\n        override val name: StringSource = Res.string.calculated_checksum.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 150.dp)\n    }\n\n    companion object {\n        val cells = listOf(\n            Name,\n            Status,\n            Algorithm,\n            CalculatedChecksum,\n            SavedChecksum,\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/checksum/FileChecksumWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.checksum\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.pages.checksum.BaseFileChecksumComponent\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun FileChecksumWindow(\n    component: AppComponent\n) {\n    component.openedFileChecksumDialog.collectAsState().value.child?.instance?.let {\n        FileChecksumWindow(it)\n    }\n}\n\n@Composable\nfun FileChecksumWindow(\n    component: DesktopFileChecksumComponent\n) {\n    val uiScale = LocalUiScale.current\n    val state = rememberWindowState(\n        position = WindowPosition.Aligned(Alignment.Center),\n        size = DpSize(900.dp, 400.dp).applyUiScale(uiScale)\n    )\n    CustomWindow(\n        state = state,\n        onCloseRequest = component::onRequestClose\n    ) {\n        HandleEffects(component) {\n            when (it) {\n                is BaseFileChecksumComponent.Effects.Platform -> {\n                    when (it as DesktopFileChecksumComponent.Effects) {\n                        DesktopFileChecksumComponent.Effects.BringToFront -> {\n                            state.isMinimized = false\n                            window.toFront()\n                        }\n                    }\n                }\n            }\n        }\n        FileChecksumPage(component)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/confirmexit/ConfirmExit.kt",
    "content": "package com.abdownloadmanager.desktop.pages.confirmexit\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.ui.widget.ConfirmDialog\nimport com.abdownloadmanager.desktop.ui.widget.ConfirmDialogType\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nfun ConfirmExit(appComponent: AppComponent) {\n    val showExitDialog by appComponent.showConfirmExitDialog.collectAsState()\n    if (showExitDialog) {\n        ConfirmDialog(\n            Res.string.confirm_exit.asStringSource(),\n            Res.string.confirm_exit_description.asStringSource(),\n            onCancel = appComponent::closeConfirmExit,\n            onConfirm = appComponent::exitAppAsync,\n            type = ConfirmDialogType.Warning,\n        )\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/credits/translators/TranslatorsPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.credits.translators\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\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 com.abdownloadmanager.desktop.di.Di\nimport com.abdownloadmanager.shared.ui.widget.MaybeLinkText\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.styled.MyStyledTableHeader\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.credits.translators.LanguageTranslationInfo\nimport com.abdownloadmanager.shared.pages.credits.translators.TranslatorData\nimport com.abdownloadmanager.shared.ui.widget.PrimaryMainActionButton\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.localizationmanager.LanguageNameProvider\nimport ir.amirab.util.compose.localizationmanager.MyLocale\nimport ir.amirab.util.compose.resources.myStringResource\nimport kotlinx.serialization.json.Json\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\nimport org.koin.core.component.get\n\n@Composable\ninternal fun Translators(modifier: Modifier) {\n    Column(\n        modifier\n    ) {\n        TranslatorsTable(\n            Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 8.dp)\n                .weight(1f)\n        )\n        ContributionNotice(\n            modifier = Modifier,\n            onUserWantsToContribute = {\n                URLOpener.openUrl(AppInfo.translationsUrl)\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun ContributionNotice(\n    modifier: Modifier,\n    onUserWantsToContribute: () -> Unit,\n) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Column(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n                .padding(horizontal = 32.dp)\n                .padding(vertical = 16.dp),\n        ) {\n            Text(\n                myStringResource(Res.string.translators_page_thanks),\n                modifier = Modifier,\n                fontSize = myTextSizes.lg,\n                fontWeight = FontWeight.Bold,\n            )\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 8.dp)\n                    .height(1.dp)\n                    .background(myColors.surface)\n            )\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Column(\n                    Modifier.weight(1f)\n                ) {\n                    Text(\n                        myStringResource(Res.string.translators_contribute_title),\n                        fontSize = myTextSizes.lg,\n                        fontWeight = FontWeight.Bold,\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    Text(\n                        myStringResource(Res.string.translators_contribute_description),\n                        fontSize = myTextSizes.base,\n                        color = LocalContentColor.current / 0.75f\n                    )\n                }\n                Spacer(Modifier.width(32.dp))\n                PrimaryMainActionButton(\n                    text = myStringResource(Res.string.contribute),\n                    onClick = onUserWantsToContribute,\n                    modifier = Modifier,\n                    enabled = true,\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun TranslatorsTable(\n    modifier: Modifier,\n) {\n    val tableState = remember {\n        TableState(\n            cells = TranslatorsCells.all()\n        )\n    }\n    val itemHorizontalPadding = 16.dp\n    Table(\n        modifier = modifier,\n        list = rememberLanguageTranslationInfo(),\n        listState = rememberLazyListState(),\n        tableState = tableState,\n        wrapHeader = {\n            MyStyledTableHeader(\n                itemHorizontalPadding = itemHorizontalPadding,\n                content = it,\n            )\n        },\n        wrapItem = { index, _, rowContent ->\n            val interactionSource = remember { MutableInteractionSource() }\n            Box(\n                Modifier\n                    .widthIn(getTableSize().visibleWidth)\n                    .hoverable(interactionSource)\n                    .indication(\n                        interactionSource,\n                        LocalIndication.current,\n                    )\n                    .background(\n                        if (index % 2 == 0) Color.Transparent else myColors.surface / 0.35f\n                    )\n                    .padding(vertical = 12.dp, horizontal = itemHorizontalPadding)\n            ) {\n                rowContent()\n            }\n        },\n        renderCell = { libraryCell, translationInfo ->\n            when (libraryCell) {\n                TranslatorsCells.LanguageName -> {\n                    Column {\n                        WithContentAlpha(1f) {\n                            Text(\n                                translationInfo.nativeName,\n                                fontSize = myTextSizes.base,\n                                overflow = TextOverflow.Ellipsis,\n                                maxLines = 1\n                            )\n                        }\n                        Spacer(Modifier.height(4.dp))\n                        WithContentAlpha(0.75f) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                Text(\n                                    translationInfo.englishName,\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                    fontSize = myTextSizes.base,\n                                )\n                                Spacer(Modifier.width(4.dp))\n                                Text(\n                                    translationInfo.locale,\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                    fontSize = myTextSizes.base,\n                                    color = myColors.primary,\n                                    modifier = Modifier\n                                        .background(myColors.primary / 10)\n                                        .padding(vertical = 0.dp, horizontal = 4.dp)\n                                )\n                            }\n                        }\n                    }\n                }\n\n                TranslatorsCells.Translators -> {\n                    Column(\n                        verticalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        translationInfo.translators.forEach {\n                            MaybeLinkText(\n                                it.name,\n                                it.link,\n                            )\n                        }\n                    }\n                }\n            }\n        },\n    )\n}\n\nprivate fun convertLanguageToMyLocale(language: String): MyLocale {\n    return language.split(\"-\").run {\n        MyLocale(\n            languageCode = get(0),\n            countryCode = getOrNull(1)\n        )\n    }\n}\n\n@Composable\nprivate fun rememberLanguageTranslationInfo(): List<LanguageTranslationInfo> {\n    return remember {\n        val json = Di.get<Json>()\n        val translatorData = FileSystem.RESOURCES.read(\n            \"/com/abdownloadmanager/resources/credits/translators.json\".toPath(),\n            {\n                readUtf8()\n            }\n        ).let {\n            json.decodeFromString<TranslatorData>(it)\n        }\n        translatorData.map {\n            val name = LanguageNameProvider.getName(convertLanguageToMyLocale(it.key))\n            LanguageTranslationInfo(\n                locale = it.key,\n                englishName = name.englishName,\n                nativeName = name.nativeName,\n                translators = it.value,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/credits/translators/TranslatorsTable.kt",
    "content": "package com.abdownloadmanager.desktop.pages.credits.translators\n\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.SortableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.credits.translators.LanguageTranslationInfo\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\nsealed interface TranslatorsCells : TableCell<LanguageTranslationInfo> {\n    data object LanguageName : TranslatorsCells,\n        SortableCell<LanguageTranslationInfo> {\n        override fun comparator(): Comparator<LanguageTranslationInfo> = compareBy { it.locale }\n        override val id: String = \"language\"\n        override val name: StringSource = Res.string.language.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 200.dp)\n    }\n\n    data object Translators : TranslatorsCells {\n        override val id: String = \"translators\"\n        override val name: StringSource = Res.string.translators.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 350.dp)\n    }\n\n    companion object {\n        fun all() = listOf(\n            LanguageName,\n            Translators,\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/credits/translators/TranslatorsWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.credits.translators\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.resources.myStringResource\n\n\n@Composable\nfun ShowTranslators(\n    appComponent: AppComponent,\n) {\n    TranslatorsWindow(\n        isVisible = appComponent.showTranslators.collectAsState().value,\n        onRequestClose = {\n            appComponent.closeTranslatorsPage()\n        }\n    )\n}\n\n@Composable\nprivate fun TranslatorsWindow(\n    isVisible: Boolean,\n    onRequestClose: () -> Unit,\n) {\n    if (!isVisible) return\n    CustomWindow(\n        onCloseRequest = onRequestClose,\n        state = rememberWindowState(\n            size = DpSize(650.dp, 500.dp)\n        )\n    ) {\n        WindowTitle(myStringResource(Res.string.meet_the_translators))\n        Translators(\n            modifier = Modifier.fillMaxSize(),\n        )\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/DesktopEditDownloadComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.editdownload\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.editdownload.BaseEditDownloadComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.flow.*\nimport org.koin.core.component.KoinComponent\n\nsealed interface EditDownloadPageEffects {\n    data object BringToFront : EditDownloadPageEffects\n}\n\nclass DesktopEditDownloadComponent(\n    ctx: ComponentContext,\n    onRequestClose: () -> Unit,\n    downloadId: Long,\n    acceptEdit: StateFlow<Boolean>,\n    onEdited: ((IDownloadItem) -> Unit, DownloadJobExtraConfig?) -> Unit,\n    downloadSystem: DownloadSystem,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    iconProvider: FileIconProvider,\n) : BaseEditDownloadComponent(\n    ctx = ctx,\n    downloadSystem = downloadSystem,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    iconProvider = iconProvider,\n    onEdited = onEdited,\n    onRequestClose = onRequestClose,\n    downloadId = downloadId,\n    acceptEdit = acceptEdit,\n),\n    ContainsEffects<EditDownloadPageEffects> by supportEffects(),\n    KoinComponent {\n    fun bringToFront() {\n        sendEffect(EditDownloadPageEffects.BringToFront)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/editdownload/EditDownload.kt",
    "content": "package com.abdownloadmanager.desktop.pages.editdownload\n\nimport androidx.compose.runtime.Composable\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.*\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.*\nimport androidx.compose.ui.window.*\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.desktop.pages.addDownload.shared.ExtraConfig\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditDownloadResult\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditWarnings\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.edit.TAEditDownloadInputs\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun EditDownloadWindow(\n    component: DesktopEditDownloadComponent,\n) {\n    CustomWindow(\n        state = rememberWindowState(\n            size = DpSize(450.dp, 230.dp)\n                .applyUiScale(LocalUiScale.current),\n            position = WindowPosition.Aligned(Alignment.Center)\n        ),\n        alwaysOnTop = true,\n        onCloseRequest = {\n            component.onRequestClose()\n        },\n    ) {\n        HandleEffects(component) {\n            when (it) {\n                EditDownloadPageEffects.BringToFront -> {\n                    window.toFront()\n                }\n            }\n        }\n        EditDownloadPage(component)\n    }\n}\n\n@Composable\nfun EditDownloadPage(\n    component: DesktopEditDownloadComponent,\n) {\n    WindowTitle(myStringResource(Res.string.edit_download_title))\n    component.editDownloadUiChecker.collectAsState().value?.let { editDownloadUiChecker ->\n        Column(\n            Modifier\n                .padding(horizontal = 32.dp)\n                .padding(top = 8.dp, bottom = 16.dp)\n        ) {\n            val canAddResult by editDownloadUiChecker.canEditDownloadResult.collectAsState()\n            val link by editDownloadUiChecker.link.collectAsState()\n            fun setLink(link: String) {\n                editDownloadUiChecker.setLink(link)\n            }\n\n            val linkFocus = remember { FocusRequester() }\n            LaunchedEffect(Unit) {\n                linkFocus.requestFocus()\n            }\n\n            UrlTextField(\n                text = link,\n                setText = {\n                    setLink(it)\n                },\n                modifier = Modifier.focusRequester(linkFocus),\n                errorText = when (canAddResult) {\n                    CanEditDownloadResult.InvalidURL -> Res.string.invalid_url\n                    else -> null\n                }?.takeIf { link.isNotEmpty() }?.asStringSource()?.rememberString()\n                // ATTENTION DO NOT use composable functions in when branches\n                // it seems buggy (compose won't render ui properly)\n                // stranger part is that in this case if we use ? before takeIf then it will work! (`}.takeIf {` is  buggy but `}?.takeIf {` works!)\n                // maybe there is a bug in compose compiler, or maybe I'm missed something. if you read this ,and you know why! please let me know!\n            )\n            Row {\n                Column(Modifier.weight(1f)) {\n                    val name by editDownloadUiChecker.name.collectAsState()\n                    Spacer(Modifier.size(8.dp))\n                    NameTextField(\n                        text = name,\n                        setText = {\n                            editDownloadUiChecker.setName(it)\n                        },\n                        errorText = when (canAddResult) {\n                            CanEditDownloadResult.FileNameAlreadyExists -> Res.string.file_name_already_exists\n                            CanEditDownloadResult.InvalidFileName -> Res.string.invalid_file_name\n                            else -> null\n                        }?.takeIf { name.isNotEmpty() }?.asStringSource()?.rememberString()\n                    )\n                    Spacer(Modifier.size(8.dp))\n                    BrowserImportButton(component, editDownloadUiChecker)\n                }\n                Spacer(Modifier.size(24.dp))\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier\n                        .align(Alignment.Top)\n                        .width(IntrinsicSize.Max)\n                ) {\n                    RenderFileTypeAndSize(component.iconProvider, editDownloadUiChecker)\n                    RenderResumeSupport(editDownloadUiChecker)\n                    ConfigActionsButtons(editDownloadUiChecker)\n                }\n            }\n            Spacer(Modifier.weight(1f))\n            MainActionButtons(component, editDownloadUiChecker)\n            if (editDownloadUiChecker.showMoreSettings.collectAsState().value) {\n                ExtraConfig(\n                    onDismiss = {\n                        editDownloadUiChecker.setShowMoreSettings(false)\n                    },\n                    configurables = editDownloadUiChecker.configurableList,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BrowserImportButton(\n    component: DesktopEditDownloadComponent,\n    downloadUiState: EditDownloadInputs<*, *, *, *, *, *>,\n) {\n    val credentialsImportedFromExternal by component.credentialsImportedFromExternal.collectAsState()\n    val downloadPage = downloadUiState.currentDownloadItem.collectAsState().value.downloadPage\n    Column {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            ActionButton(\n                myStringResource(Res.string.edit_download_update_from_download_page),\n                enabled = downloadPage != null,\n                onClick = {\n                    downloadPage?.let {\n                        URLOpener.openUrl(it)\n                    }\n                },\n//                borderColor = when (credentialsImportedFromExternal) {\n//                    true -> SolidColor(myColors.success)\n//                    false -> SolidColor(myColors.onBackground / 10)\n//                },\n                contentPadding = PaddingValues(\n                    vertical = 6.dp,\n                    horizontal = animateDpAsState(\n                        if (credentialsImportedFromExternal) 8.dp\n                        else 16.dp\n                    ).value,\n                ),\n                end = {\n                    AnimatedVisibility(credentialsImportedFromExternal) {\n                        Row {\n                            Spacer(Modifier.width(8.dp))\n                            MyIcon(\n                                MyIcons.check,\n                                null,\n                                Modifier.size(16.dp),\n                                tint = myColors.success,\n                            )\n                        }\n                    }\n                }\n            )\n            Spacer(Modifier.width(8.dp))\n            Help(myStringResource(Res.string.edit_download_update_from_download_page_description))\n        }\n    }\n}\n\n@Composable\nprivate fun RenderResumeSupport(\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier.height(16.dp)\n    ) {\n        val lineModifier = Modifier.weight(1f)\n            .height(1.dp)\n            .background(myColors.onBackground / 10)\n        Box(lineModifier)\n        val canEditDownload by editDownloadUiChecker.canEdit.collectAsState()\n        AnimatedVisibility(\n            visible = canEditDownload && fileInfo != null,\n        ) {\n            fileInfo?.let { fileInfo ->\n                if (fileInfo.resumeSupport) {\n                    val iconModifier = Modifier\n                        .padding(horizontal = 2.dp)\n                        .size(10.dp)\n                    if (fileInfo.resumeSupport) {\n                        MyIcon(\n                            icon = MyIcons.check,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.success\n                        )\n                    } else {\n                        MyIcon(\n                            icon = MyIcons.clear,\n                            contentDescription = null,\n                            modifier = iconModifier,\n                            tint = myColors.error,\n                        )\n                    }\n                }\n            }\n        }\n        Box(lineModifier)\n    }\n}\n\n@Composable\nprivate fun MainConfigActionButton(\n    text: String,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    ActionButton(text, modifier, enabled, onClick)\n}\n\n@Composable\nfun ConfigActionsButtons(\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    val showMoreSettings by editDownloadUiChecker.showMoreSettings.collectAsState()\n    val requiresAuth = editDownloadUiChecker.responseInfo.collectAsState().value?.requireBasicAuth ?: false\n    Row {\n        IconActionButton(MyIcons.refresh, Res.string.refresh.asStringSource()) {\n            editDownloadUiChecker.refresh()\n        }\n        Spacer(Modifier.width(6.dp))\n        IconActionButton(\n            MyIcons.settings,\n            Res.string.settings.asStringSource(),\n            indicateActive = showMoreSettings,\n            requiresAttention = requiresAuth\n        ) {\n            editDownloadUiChecker.setShowMoreSettings(true)\n        }\n    }\n}\n\n@Composable\nprivate fun MainActionButtons(\n    component: DesktopEditDownloadComponent,\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    Row {\n        val canEditResult by editDownloadUiChecker.canEditDownloadResult.collectAsState()\n\n        val canEdit = run {\n            val canBeEdited = editDownloadUiChecker.canEdit.collectAsState().value\n            val componentAllowsEdit = component.acceptEdit.collectAsState().value\n            canBeEdited && componentAllowsEdit\n        }\n        val warnings = (canEditResult as? CanEditDownloadResult.CanEdit)?.warnings.orEmpty()\n        Spacer(Modifier.width(8.dp))\n        var showWarningPrompt by remember {\n            mutableStateOf(false)\n        }\n        Box {\n            if (showWarningPrompt) {\n                WarningPrompt(\n                    warnings = warnings,\n                    onClose = {\n                        showWarningPrompt = false\n                    },\n                    onConfirm = {\n                        if (canEdit) {\n                            component.onRequestEdit()\n                        }\n                    }\n                )\n            }\n            PrimaryMainActionButton(\n                text = myStringResource(Res.string.change),\n                modifier = Modifier,\n                enabled = canEdit,\n                onClick = {\n                    if (warnings.isNotEmpty()) {\n                        showWarningPrompt = true\n                    } else {\n                        component.onRequestEdit()\n                    }\n                },\n            )\n        }\n        //        Spacer(Modifier.weight(1f))\n        Spacer(Modifier.weight(1f))\n\n        MainConfigActionButton(\n            text = myStringResource(Res.string.cancel),\n            modifier = Modifier,\n            onClick = {\n                component.onRequestClose()\n            },\n        )\n    }\n}\n\n@Composable\nfun WarningPrompt(\n    warnings: List<CanEditWarnings>,\n    onClose: () -> Unit,\n    onConfirm: () -> Unit,\n) {\n    Popup(\n        popupPositionProvider = rememberMyComponentRectPositionProvider(\n            anchor = Alignment.TopStart,\n            alignment = Alignment.TopEnd,\n        ),\n        onDismissRequest = onClose\n    ) {\n        val shape = myShapes.defaultRounded\n        Box(\n            Modifier\n                .padding(vertical = 4.dp)\n                .widthIn(max = 240.dp)\n                .shadow(24.dp)\n                .clip(shape)\n                .border(1.dp, myColors.surface, shape)\n                .background(myColors.menuGradientBackground)\n                .padding(8.dp)\n        ) {\n            WithContentColor(myColors.onSurface) {\n                Column {\n                    Text(\n                        myStringResource(Res.string.warning),\n                        fontWeight = FontWeight.Bold,\n                        color = myColors.warning\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    warnings.forEach {\n                        Text(\n                            it.asStringSource().rememberString(),\n                            fontSize = myTextSizes.base,\n                        )\n                    }\n                    Text(myStringResource(Res.string.warning_you_may_have_to_restart_the_download_later))\n                    Spacer(Modifier.height(8.dp))\n                    ActionButton(\n                        modifier = Modifier.align(Alignment.CenterHorizontally),\n                        text = myStringResource(Res.string.change_anyway),\n                        onClick = onConfirm,\n                        borderColor = SolidColor(myColors.error),\n                        contentColor = myColors.error,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderFileTypeAndSize(\n    iconProvider: FileIconProvider,\n    editDownloadUiChecker: TAEditDownloadInputs,\n) {\n    val isLinkLoading by editDownloadUiChecker.isLinkLoading.collectAsState()\n    val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()\n    val iconModifier = Modifier.size(16.dp)\n    Box(Modifier.padding(top = 16.dp)) {\n        AnimatedContent(\n            targetState = isLinkLoading,\n            transitionSpec = {\n                fadeIn() togetherWith fadeOut()\n            }\n        ) { loading ->\n            if (loading) {\n                LoadingIndicator(iconModifier)\n            } else {\n                val icon = iconProvider.rememberIcon(editDownloadUiChecker.name.collectAsState().value)\n                AnimatedContent(\n                    fileInfo,\n                ) { fileInfo ->\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(1f) {\n                            if (fileInfo != null) {\n                                if (fileInfo.requiresAuth) {\n                                    MyIcon(\n                                        MyIcons.lock,\n                                        null,\n                                        iconModifier,\n                                        tint = myColors.error\n                                    )\n                                }\n                                MyIcon(\n                                    icon,\n                                    null,\n                                    iconModifier\n                                )\n\n                                val size by editDownloadUiChecker.lengthStringFlow.collectAsState()\n                                Spacer(Modifier.width(8.dp))\n                                Text(\n                                    size.rememberString(),\n                                    fontSize = myTextSizes.sm,\n                                )\n                            } else {\n                                MyIcon(\n                                    icon = MyIcons.question,\n                                    contentDescription = null,\n                                    modifier = iconModifier,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MyTextFieldIcon(\n    icon: IconSource,\n    onClick: (() -> Unit)? = null,\n) {\n    MyIcon(icon, null, Modifier\n        .fillMaxHeight()\n        .ifThen(onClick != null) {\n            pointerHoverIcon(PointerIcon.Default)\n                .clickable { onClick?.invoke() }\n        }\n        .wrapContentHeight()\n        .padding(horizontal = 8.dp)\n        .size(16.dp))\n}\n\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    modifier: Modifier = Modifier,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        start = {\n            MyTextFieldIcon(MyIcons.link)\n        },\n        end = {\n            MyTextFieldIcon(MyIcons.paste) {\n                setText(\n                    ClipboardUtil.read()\n                        .orEmpty()\n                )\n            }\n        },\n        errorText = errorText\n    )\n}\n\n@Composable\nprivate fun NameTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.name),\n        modifier = Modifier.fillMaxWidth(),\n        errorText = errorText,\n    )\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/enterurl/DesktopEnterNewURLComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.enterurl\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.pages.enterurl.BaseEnterNewURLComponent\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\n\nclass DesktopEnterNewURLComponent(\n    ctx: ComponentContext,\n    config: Config,\n    downloaderInUiRegistry: DownloaderInUiRegistry,\n    onCloseRequest: () -> Unit,\n    onRequestFinished: (IDownloadCredentials) -> Unit,\n) : BaseEnterNewURLComponent(\n    ctx = ctx,\n    config = config,\n    downloaderInUiRegistry = downloaderInUiRegistry,\n    onCloseRequest = onCloseRequest,\n    onRequestFinished = onRequestFinished,\n) {\n    sealed interface Effects : BaseEnterNewURLComponent.Effects.PlatformEffects {\n        data object BringToFront : Effects\n    }\n\n    data object Config : BaseEnterNewURLComponent.Config\n\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/enterurl/EnterNewDownloadWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.enterurl\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun EnterNewDownloadWindow(\n    appComponent: AppComponent\n) {\n    val child = appComponent.enterNewURLWindowSlot.rememberChild()\n    child?.let {\n        EnterNewDownloadWindow(child)\n    }\n}\n\n@Composable\nprivate fun EnterNewDownloadWindow(\n    component: DesktopEnterNewURLComponent,\n) {\n    val windowState = rememberWindowState(\n        size = DpSize(400.dp, 150.dp)\n            .applyUiScale(LocalUiScale.current),\n        position = WindowPosition.Aligned(Alignment.Center)\n    )\n    CustomWindow(\n        state = windowState,\n        onCloseRequest = component::close\n    ) {\n        WindowTitle(\n            myStringResource(Res.string.new_download)\n        )\n        HandleEffects(component) {\n            when (it) {\n                DesktopEnterNewURLComponent.Effects.BringToFront -> {\n                    windowState.isMinimized = false\n                    window.toFront()\n                }\n                else -> {}\n            }\n        }\n        EnterNewURLPage(\n            component,\n        )\n    }\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/enterurl/EnterNewURLPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.enterurl\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\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.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberDialogState\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.desktop.window.custom.BaseOptionDialog\nimport com.abdownloadmanager.desktop.window.moveSafe\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.TADownloaderInUI\nimport com.abdownloadmanager.shared.pages.enterurl.DownloaderSelection\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.resources.myStringResource\nimport java.awt.MouseInfo\n\n@Composable\nfun EnterNewURLPage(component: DesktopEnterNewURLComponent) {\n    val linkFocus = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        linkFocus.requestFocus()\n        component.onPageOpen()\n    }\n    val text by component.url.collectAsState()\n    Column {\n        Column(\n            Modifier\n                .padding(top = 8.dp)\n                .padding(horizontal = 16.dp)\n        ) {\n\n            UrlTextField(\n                text = text,\n                setText = component::setURL,\n                modifier = Modifier\n                    .focusRequester(linkFocus)\n                    .fillMaxWidth()\n            )\n        }\n        Spacer(Modifier.weight(1f))\n        Column {\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .height(1.dp)\n                    .background(myColors.onBackground / 0.15f)\n            )\n            Row(\n                Modifier\n                    .fillMaxWidth()\n                    .background(myColors.surface / 0.5f)\n                    .padding(horizontal = 16.dp)\n                    .padding(vertical = 8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                DownloaderSelectionSection(component)\n                Spacer(Modifier.weight(1f))\n                Actions(component)\n            }\n        }\n    }\n\n}\n\n@Composable\nprivate fun DownloaderSelectionSection(\n    component: DesktopEnterNewURLComponent,\n) {\n    val downloaderSelection = component.downloaderSelection.collectAsState().value\n    val bestDownloader = component.bestDownloader.collectAsState().value\n    var isSelecting by remember { mutableStateOf(false) }\n    val selectedName = rememberDownloaderSelectionItemString(\n        downloaderSelection, bestDownloader\n    )\n    ActionButton(\n        text = selectedName,\n        end = {\n            Row(\n                Modifier.align(Alignment.CenterVertically)\n            ) {\n                Spacer(Modifier.width(4.dp))\n                MyIcon(MyIcons.down, null, Modifier.size(12.dp))\n            }\n        },\n        onClick = {\n            isSelecting = !isSelecting\n        }\n    )\n\n    if (isSelecting) {\n        val state = rememberDialogState(\n            size = DpSize.Unspecified,\n        )\n        BaseOptionDialog(\n            onCloseRequest = {\n                isSelecting = false\n            },\n            state = state,\n            resizeable = false,\n        ) {\n            LaunchedEffect(window) {\n                window.moveSafe(\n                    MouseInfo.getPointerInfo().location.run {\n                        DpOffset(\n                            x = x.dp,\n                            y = y.dp\n                        )\n                    }\n                )\n            }\n\n\n            val shape = myShapes.defaultRounded\n            Column(\n                Modifier\n                    .clip(shape)\n                    .border(2.dp, myColors.onBackground / 10, shape)\n                    .background(\n                        Brush.linearGradient(\n                            listOf(\n                                myColors.surface,\n                                myColors.background,\n                            )\n                        )\n                    )\n            ) {\n                WithContentColor(myColors.onBackground) {\n                    Column(\n                        Modifier\n                            .widthIn(min = 100.dp, max = 300.dp)\n                            .width(IntrinsicSize.Max)\n                    ) {\n                        component.possibleValues.onEach {\n                            val text = rememberDownloaderSelectionItemString(\n                                it,\n                                bestDownloader,\n                            )\n                            Text(\n                                text,\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        component.selectDownloader(it)\n                                        isSelecting = false\n                                    }\n                                    .padding(vertical = 8.dp, horizontal = 16.dp)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun rememberDownloaderSelectionItemString(\n    downloaderSelection: DownloaderSelection,\n    bestDownloader: TADownloaderInUI?,\n): String {\n    return when (downloaderSelection) {\n        DownloaderSelection.Auto -> {\n            val autoText = myStringResource(Res.string.auto)\n            val bestDownloaderName = bestDownloader?.name?.rememberString()\n            buildString {\n                append(autoText)\n                if (bestDownloader != null) {\n                    append(\" ($bestDownloaderName)\")\n                }\n            }\n        }\n\n        is DownloaderSelection.Fixed -> {\n            downloaderSelection.downloaderInUi.name.rememberString()\n        }\n    }\n}\n\n@Composable\nprivate fun Actions(\n    component: DesktopEnterNewURLComponent,\n) {\n    ActionButton(\n        myStringResource(Res.string.ok),\n        enabled = component.canAdd.collectAsState().value,\n        onClick = {\n            component.newDownloadEntered()\n        }\n    )\n    Spacer(Modifier.width(8.dp))\n    ActionButton(\n        myStringResource(Res.string.cancel),\n        onClick = component::close\n    )\n}\n\n@Composable\nprivate fun UrlTextField(\n    text: String,\n    setText: (String) -> Unit,\n    errorText: String? = null,\n    modifier: Modifier = Modifier,\n) {\n    MyTextFieldWithIcons(\n        text,\n        setText,\n        myStringResource(Res.string.download_link),\n        modifier = modifier.fillMaxWidth(),\n        start = {\n            MyIcon(\n                MyIcons.link,\n                null,\n                Modifier.padding(horizontal = 8.dp)\n                    .size(16.dp),\n            )\n        },\n        end = {\n            MyTextFieldIcon(MyIcons.paste) {\n                setText(\n                    ClipboardUtil.read()\n                        .orEmpty()\n                )\n            }\n        },\n        errorText = errorText\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/extenallibs/ExternalLibsPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.extenallibs\n\nimport com.abdownloadmanager.shared.util.ui.ProvideTextStyle\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.styled.MyStyledTableHeader\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.mikepenz.aboutlibraries.Libs\nimport com.mikepenz.aboutlibraries.entity.Library\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\n\n@Composable\ninternal fun ExternalLibsPage() {\n    val libs = rememberLibs()\n    OpenSourceLibraries(\n        libs = libs,\n        modifier = Modifier.fillMaxSize(),\n    )\n}\n\n@Composable\nprivate fun OpenSourceLibraries(\n    libs: Libs,\n    modifier: Modifier,\n) {\n    var currentDialog by remember {\n        mutableStateOf(null as Library?)\n    }\n    Column(\n        modifier\n    ) {\n        val tableState = remember {\n            TableState(\n                cells = LibraryCells.all()\n            )\n        }\n        val itemHorizontalPadding = 16.dp\n        Table(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(8.dp)\n                .weight(1f),\n            list = libs.libraries,\n            listState = rememberLazyListState(),\n            tableState = tableState,\n            wrapHeader = {\n                MyStyledTableHeader(\n                    itemHorizontalPadding = itemHorizontalPadding,\n                    content = it,\n                )\n            },\n            wrapItem = { _, item, rowContent ->\n                Box(\n                    Modifier\n                        .clickable {\n                            currentDialog = item\n                        }\n                        .widthIn(getTableSize().visibleWidth)\n                        .padding(vertical = 6.dp, horizontal = itemHorizontalPadding)) {\n                    rowContent()\n                }\n            },\n            renderCell = { libraryCell, library ->\n                when (libraryCell) {\n                    LibraryCells.Name -> {\n                        Column {\n                            WithContentAlpha(1f) {\n                                Row(Modifier) {\n                                    Text(\n                                        library.name,\n                                        fontSize = myTextSizes.base,\n                                        overflow = TextOverflow.Ellipsis,\n                                        maxLines = 1\n                                    )\n                                    Spacer(Modifier.width(2.dp))\n                                    library.artifactVersion?.let { version ->\n                                        Text(\n                                            text = version,\n                                            fontSize = myTextSizes.base,\n                                            overflow = TextOverflow.Ellipsis,\n                                            maxLines = 1,\n                                        )\n                                    }\n                                }\n                            }\n                            WithContentAlpha(0.75f) {\n                                Text(\n                                    library.artifactId,\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                    fontSize = myTextSizes.sm,\n                                )\n                            }\n                        }\n                    }\n\n                    LibraryCells.Author -> {\n                        val by = library.by()\n                        if (by.isNotEmpty()) {\n                            Row {\n                                WithContentAlpha(0.7f) {\n                                    ProvideTextStyle(\n                                        TextStyle(fontSize = myTextSizes.sm)\n                                    ) {\n                                        for ((name) in by) {\n                                            Spacer(Modifier.width(4.dp))\n                                            Text(\n                                                text = name,\n                                                fontSize = myTextSizes.base,\n                                                maxLines = 1,\n                                                overflow = TextOverflow.Ellipsis,\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    LibraryCells.License -> {\n                        WithContentAlpha(0.75f) {\n                            Text(\n                                text = library.licenses.joinToString(\", \") { it.name },\n                                fontSize = myTextSizes.base,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        }\n                    }\n                }\n            },\n        )\n    }\n    currentDialog.let { library ->\n        if (library != null) {\n            LibraryDialog(library) {\n                currentDialog = null\n            }\n        }\n    }\n\n}\n\nprivate fun Library.by(): List<Pair<String, String?>> {\n    val d = developers.filter {\n        it.name != null\n    }.map {\n        it.name!! to it.organisationUrl\n    }.takeIf { it.isNotEmpty() }\n    if (d != null) return d\n    return organization?.let {\n        listOf(it.name to it.url)\n    } ?: emptyList()\n}\n\n@Composable\nprivate fun rememberLibs(): Libs {\n    return remember {\n        val jsonContent = FileSystem.RESOURCES.read(\"aboutlibraries.json\".toPath()) {\n            readUtf8()\n        }\n        Libs.Builder().withJson(jsonContent).build()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/extenallibs/ExternalLibsWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.extenallibs\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun ShowOpenSourceLibraries(appComponent: AppComponent){\n    ShowOpenSourceLibraries(\n        visible = appComponent.showOpenSourceLibraries.collectAsState().value,\n        onRequestClose = {\n            appComponent.closeOpenSourceLibraries()\n        }\n    )\n}\n\n@Composable\nfun ShowOpenSourceLibraries(\n    visible: Boolean,\n    onRequestClose:()->Unit,\n) {\n    if (!visible) return\n    CustomWindow(\n        onCloseRequest = onRequestClose,\n        state = rememberWindowState(\n            size = DpSize(650.dp, 400.dp)\n        )\n    ) {\n        WindowTitle(myStringResource(Res.string.open_source_software_used_in_this_app))\n        ExternalLibsPage()\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/extenallibs/LibraryDialog.kt",
    "content": "package com.abdownloadmanager.desktop.pages.extenallibs\n\nimport com.abdownloadmanager.shared.ui.widget.MaybeLinkText\nimport com.abdownloadmanager.shared.util.ui.ProvideTextStyle\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.mikepenz.aboutlibraries.entity.Developer\nimport com.mikepenz.aboutlibraries.entity.Library\nimport com.mikepenz.aboutlibraries.entity.License\nimport com.mikepenz.aboutlibraries.entity.Organization\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.collections.immutable.ImmutableSet\n\n@Composable\nfun LibraryDialog(\n    library: Library, onCloseRequest: () -> Unit,\n) {\n    Dialog(\n        onCloseRequest, properties = DialogProperties()\n    ) {\n        ProvideTextStyle(\n            TextStyle(fontSize = myTextSizes.base)\n        ) {\n            val shape = myShapes.defaultRounded\n            Column(\n                Modifier\n                    .clip(shape)\n                    .border(2.dp, myColors.onBackground / 10, shape)\n                    .background(\n                        Brush.linearGradient(\n                            listOf(\n                                myColors.surface,\n                                myColors.background,\n                            )\n                        )\n                    )\n                    .padding(16.dp)\n            ) {\n                Column(\n                    Modifier\n                        .weight(1f, false)\n                        .verticalScroll(rememberScrollState())\n                ) {\n                    LibraryNameAndVersion(library.name, library.artifactVersion, library.artifactId)\n                    Spacer(Modifier.height(16.dp))\n                    library.description?.let {\n                        LibraryDescription(it)\n                    }\n                    Spacer(Modifier.height(16.dp))\n                    library.developers.takeIf { it.isNotEmpty() }?.let {\n                        LibraryDevelopers(it)\n                    }\n                    library.organization?.let {\n                        LibraryOrganization(it)\n                    }\n                    val links = buildList {\n                        library.scm?.url?.let {\n                            add(Res.string.source_code.asStringSource() to it)\n                        }\n                        library.website?.let {\n                            add(Res.string.website.asStringSource() to it)\n                        }\n                    }\n                    links.takeIf { it.isNotEmpty() }?.let {\n                        LibraryLinks(links)\n                    }\n                    LibraryLicenseInfo(library.licenses)\n                }\n                Spacer(Modifier.height(8.dp))\n                Row(\n                    Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.End,\n                ) {\n                    ActionButton(myStringResource(Res.string.close), onClick = {\n                        onCloseRequest()\n                    })\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LibraryLinks(links: List<Pair<StringSource, String>>) {\n    KeyValue(myStringResource(Res.string.links)) {\n        ListOfNamesWithLinks(links)\n    }\n}\n\n@Composable\nprivate fun LibraryDescription(description: String) {\n    Text(\n        description,\n        modifier = Modifier.background(myColors.surface).padding(8.dp),\n        color = myColors.onSurface,\n    )\n}\n\n@Composable\nprivate fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {\n    KeyValue(myStringResource(Res.string.license)) {\n        val l = licenses.map {\n            it.name.asStringSource() to it.url\n        }\n        if (l.isEmpty()) {\n            Text(myStringResource(Res.string.no_license_found))\n        } else {\n            ListOfNamesWithLinks(l)\n        }\n    }\n}\n\n@Composable\nprivate fun LibraryDevelopers(devs: List<Developer>) {\n    KeyValue(myStringResource(Res.string.developers)) {\n        ListOfNamesWithLinks(\n            devs\n                .filter { it.name != null }\n                .map {\n                    it.name!!.asStringSource() to it.organisationUrl\n                }\n        )\n    }\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable\nprivate fun ListOfNamesWithLinks(map: List<Pair<StringSource, String?>>) {\n    FlowRow {\n        for ((i, v) in map.withIndex()) {\n            val (name, link) = v\n            MaybeLinkText(name.rememberString(), link)\n            if (i < map.lastIndex) {\n                Text(\", \")\n            }\n        }\n    }\n}\n\n@Composable\nfun LibraryOrganization(organization: Organization) {\n    KeyValue(myStringResource(Res.string.organization)) {\n        MaybeLinkText(organization.name, organization.url)\n    }\n}\n\n@Composable\nprivate fun LibraryNameAndVersion(\n    name: String, version: String?,\n    artifactId: String,\n) {\n    val nameWithVersion = name + (version?.let { \" $it\" }.orEmpty())\n    Column {\n        Row {\n            Text(\n                \"$nameWithVersion\",\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.base,\n            )\n        }\n        Spacer(Modifier.height(4.dp))\n        WithContentAlpha(0.75f) {\n            Row {\n                Text(\n                    \"($artifactId)\",\n                    fontSize = myTextSizes.sm,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun KeyValue(\n    key: String,\n    value: @Composable () -> Unit,\n) {\n    Row {\n        WithContentAlpha(0.75f) {\n            Text(\n                \"$key:\",\n                maxLines = 1,\n            )\n        }\n        Spacer(Modifier.width(8.dp))\n        value()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/extenallibs/OpenSourceLibraryTable.kt",
    "content": "package com.abdownloadmanager.desktop.pages.extenallibs\n\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.SortableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.mikepenz.aboutlibraries.entity.Library\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\nsealed interface LibraryCells : TableCell<Library> {\n    data object Name : LibraryCells,\n        SortableCell<Library> {\n        override fun comparator(): Comparator<Library> = compareBy { it.name }\n        override val id: String = \"Name\"\n        override val name: StringSource = Res.string.name.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp, 250.dp)\n    }\n\n    data object Author : LibraryCells,\n        SortableCell<Library> {\n        override fun comparator(): Comparator<Library> = compareBy { item ->\n            item.licenses.firstOrNull()?.name.orEmpty()\n        }\n\n        override val id: String = \"Author\"\n        override val name: StringSource = Res.string.author.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)\n    }\n\n    data object License : LibraryCells,\n        SortableCell<Library> {\n        override fun comparator(): Comparator<Library> = compareBy { it.licenses.firstOrNull()?.name.orEmpty() }\n\n        override val id: String = \"License\"\n        override val name: StringSource = Res.string.license.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)\n    }\n\n    companion object {\n        fun all() = listOf(\n            Name,\n            Author,\n            License,\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/Actions.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MyDropDown\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.SubMenu\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport ir.amirab.util.compose.action.MenuItem\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.Tooltip\nimport com.abdownloadmanager.shared.util.ui.LocalMultiplatformScrollbarStyle\nimport com.abdownloadmanager.shared.util.ui.MultiplatformHorizontalScrollbar\nimport com.abdownloadmanager.shared.util.ui.needScroll\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\n@Composable\nfun Actions(\n    list: List<MenuItem>,\n    showLabels: Boolean,\n) {\n    val scrollState = rememberScrollState()\n    Column {\n        Row(\n            Modifier\n                .height(IntrinsicSize.Max)\n                .horizontalScroll(scrollState)\n        ) {\n            for (a in list) {\n                when (a) {\n                    MenuItem.Separator -> {\n                        Spacer(\n                            Modifier\n                                .padding(horizontal = 4.dp)\n                                .fillMaxHeight()\n                                .padding(vertical = 4.dp)\n                                .width(1.dp)\n                                .background(myColors.onBackground / 5)\n                        )\n                    }\n\n                    is MenuItem.SingleItem -> {\n                        ActionButton(Modifier, a, showLabels)\n                    }\n\n                    is MenuItem.SubMenu -> {\n                        GroupActionButton(Modifier, a, showLabels)\n                    }\n                }\n            }\n        }\n        val adapter = rememberScrollbarAdapter(scrollState)\n        if (adapter.needScroll()) {\n            MultiplatformHorizontalScrollbar(\n                adapter = adapter,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 2.dp),\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ActionButton(\n    modifier: Modifier = Modifier,\n    action: MenuItem.SingleItem,\n    showLabels: Boolean,\n) {\n    val enabled by action.isEnabled.collectAsState()\n    Column(modifier) {\n        ActionIconWithLabel(\n            title = action.title.collectAsState().value,\n            icon = action.icon.collectAsState().value,\n            showLabels = showLabels,\n            onClick = {\n                action()\n            },\n            enabled = enabled\n        )\n    }\n}\n\n@Composable\nprivate fun GroupActionButton(\n    modifier: Modifier = Modifier,\n    action: MenuItem.SubMenu,\n    showLabels: Boolean,\n) {\n    val enabled by action.isEnabled.collectAsState()\n    var showSubMenu by remember { mutableStateOf(false) }\n    Column(modifier) {\n        ActionIconWithLabel(\n            title = action.title.collectAsState().value,\n            icon = action.icon.collectAsState().value,\n            showLabels = showLabels,\n            onClick = {\n                showSubMenu = !showSubMenu\n            },\n            enabled = enabled\n        )\n        val close = {\n            showSubMenu = false\n        }\n        if (enabled && showSubMenu) {\n            MyDropDown(onDismissRequest = close) {\n                val items by action.items.collectAsState()\n                SubMenu(subMenu = items, onRequestClose = close)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ActionIconWithLabel(\n    title: StringSource,\n    icon: IconSource?,\n    showLabels: Boolean,\n    onClick: () -> Unit,\n    enabled: Boolean,\n) {\n    OptionalTooltip(\n        title.takeIf { !showLabels },\n    ) {\n        Column(\n            modifier = Modifier\n                .clickable(enabled = enabled, onClick = onClick)\n                .ifThen(!enabled) {\n                    alpha(0.5f)\n                }\n                .padding(if (showLabels) 8.dp else 12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            WithContentColor(myColors.onBackground) {\n                WithContentAlpha(1f) {\n                    icon?.let {\n                        val iconSize = if (showLabels) 16.dp else 24.dp\n                        MyIcon(\n                            icon = it,\n                            contentDescription = null,\n                            modifier = Modifier.size(iconSize)\n                        )\n                    }\n                    if (showLabels) {\n                        Spacer(Modifier.size(2.dp))\n                        Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.sm)\n                    }\n                }\n            }\n        }\n    }\n\n}\n\n@Composable\nprivate fun OptionalTooltip(\n    tooltip: StringSource?,\n    content: @Composable () -> Unit\n) {\n    if (tooltip != null) {\n        Tooltip(tooltip) {\n            content()\n        }\n    } else {\n        content()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/DesktopDownloadActions.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pages.home.AbstractDownloadActions\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\n\nclass DesktopDownloadActions(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    downloadDialogManager: DownloadDialogManager,\n    editDownloadDialogManager: EditDownloadDialogManager,\n    fileChecksumDialogManager: FileChecksumDialogManager,\n    selections: StateFlow<List<IDownloadItemState>>,\n    queueManager: QueueManager,\n    categoryManager: CategoryManager,\n    openFile: (Long) -> Unit,\n    requestDelete: (List<Long>) -> Unit,\n    mainItem: StateFlow<Long?>,\n    private val openFolder: (Long) -> Unit,\n) : AbstractDownloadActions(\n    scope = scope,\n    downloadSystem = downloadSystem,\n    downloadDialogManager = downloadDialogManager,\n    editDownloadDialogManager = editDownloadDialogManager,\n    fileChecksumDialogManager = fileChecksumDialogManager,\n    selections = selections,\n    queueManager = queueManager,\n    categoryManager = categoryManager,\n    openFile = openFile,\n    requestDelete = requestDelete,\n    mainItem = mainItem,\n) {\n    val openFolderAction = simpleAction(\n        title = Res.string.open_folder.asStringSource(),\n        icon = MyIcons.folderOpen,\n        onActionPerformed = {\n            scope.launch {\n                val d = defaultItem.value ?: return@launch\n                openFolder(d.id)\n            }\n        }\n    )\n\n    val menu: List<MenuItem> = buildMenu {\n        +openFileAction\n        +openFolderAction\n        +(resumeAction)\n        +pauseAction\n        separator()\n        +(deleteAction)\n        +(reDownloadAction)\n        separator()\n        +moveToQueueItems\n        +moveToCategoryAction\n        separator()\n        subMenu(Res.string.copy.asStringSource(), MyIcons.copy) {\n            +(copyDownloadLinkAction)\n            +(copyDownloadCredentialsAsCurlAction)\n        }\n        +editDownloadAction\n        +fileChecksumAction\n        +(openDownloadDialogAction)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/DownloadItemListDataFlavor.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport java.awt.datatransfer.DataFlavor\nimport java.awt.datatransfer.Transferable\nimport java.awt.datatransfer.UnsupportedFlavorException\nimport java.io.File\n\nval DownloadItemListDataFlavor = DataFlavor(\n    IDownloadItemState::class.java,\n    \"Download Item\"\n)\n\nclass DownloadItemTransferable(\n    val items: List<IDownloadItemState>,\n) : Transferable {\n    override fun getTransferDataFlavors(): Array<DataFlavor> {\n        return arrayOf(\n            DataFlavor.javaFileListFlavor,\n            DownloadItemListDataFlavor,\n        )\n    }\n\n    override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {\n        return (flavor in arrayOf(\n            DataFlavor.javaFileListFlavor,\n            DownloadItemListDataFlavor,\n        ))\n    }\n\n    override fun getTransferData(flavor: DataFlavor?): Any {\n        return when (flavor) {\n            DataFlavor.javaFileListFlavor -> items.map {\n                File(it.folder, it.name)\n            }\n\n            DownloadItemListDataFlavor -> items\n            else -> throw UnsupportedFlavorException(flavor)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport com.abdownloadmanager.desktop.*\nimport com.abdownloadmanager.desktop.actions.*\nimport com.abdownloadmanager.desktop.pages.home.sections.DownloadListCells\nimport com.abdownloadmanager.desktop.storage.PageStatesStorage\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.desktop.utils.*\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.desktop.pages.category.DesktopCategoryDialogManager\nimport com.abdownloadmanager.desktop.storage.AppSettingsStorage\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.action.donate\nimport com.abdownloadmanager.shared.action.supportActionGroup\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EnterNewURLDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pages.home.BaseHomeComponent\nimport com.abdownloadmanager.shared.util.*\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.monitor.*\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.DownloadCredentialFromStringExtractor\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.AppVersionTracker\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isLinux\nimport ir.amirab.util.platform.isMac\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport java.awt.event.KeyEvent\nimport java.io.File\nimport kotlin.collections.map\nimport kotlin.getValue\n\n\nclass HomeComponent(\n    ctx: ComponentContext,\n    downloadItemOpener: DownloadItemOpener,\n    downloadDialogManager: DesktopDownloadDialogManager,\n    editDownloadDialogManager: EditDownloadDialogManager,\n    override val enterNewURLDialogManager: EnterNewURLDialogManager,\n    desktopAddDownloadDialogManager: DesktopAddDownloadDialogManager,\n    fileChecksumDialogManager: FileChecksumDialogManager,\n    queuePageManager: QueuePageManager,\n    categoryDialogManager: DesktopCategoryDialogManager,\n    notificationSender: NotificationSender,\n    downloadSystem: DownloadSystem,\n    categoryManager: CategoryManager,\n    queueManager: QueueManager,\n    defaultCategories: DefaultCategories,\n    fileIconProvider: FileIconProvider,\n) : BaseHomeComponent(\n    componentContext = ctx,\n    downloadItemOpener = downloadItemOpener,\n    downloadDialogManager = downloadDialogManager,\n    editDownloadDialogManager = editDownloadDialogManager,\n    addDownloadDialogManager = desktopAddDownloadDialogManager,\n    fileChecksumDialogManager = fileChecksumDialogManager,\n    queuePageManager = queuePageManager,\n    categoryDialogManager = categoryDialogManager,\n    notificationSender = notificationSender,\n    downloadSystem = downloadSystem,\n    categoryManager = categoryManager,\n    queueManager = queueManager,\n    defaultCategories = defaultCategories,\n    fileIconProvider = fileIconProvider,\n),\n    ContainsShortcuts,\n    KoinComponent {\n    private val pageStorage: PageStatesStorage by inject()\n    private val appSettings: AppSettingsStorage by inject()\n    private val updateManager: UpdateManager by inject()\n    private val appVersionTracker: AppVersionTracker by inject()\n    val mergeTopBarWithTitleBar = appSettings.mergeTopBarWithTitleBar\n    val useNativeMenuBar = appSettings.useNativeMenuBar\n\n    private val homePageStateToPersist = MutableStateFlow(pageStorage.homePageStorage.value)\n\n    init {\n        HomeComponent.homeComponentCreationCount++\n    }\n\n    private fun isFirstVisitInThisSession(): Boolean {\n        return HomeComponent.homeComponentCreationCount == 1\n    }\n\n    init {\n        homePageStateToPersist\n            .debounce(500)\n            .onEach { newValue ->\n                pageStorage.homePageStorage.update { newValue }\n            }.launchIn(scope)\n    }\n\n    private val _windowSize = homePageStateToPersist.mapTwoWayStateFlow(\n        map = {\n            it.windowSize.let { (x, y) ->\n                DpSize(x.dp, y.dp)\n            }\n        },\n        unMap = {\n            copy(\n                windowSize = it.width.value to it.height.value\n            )\n        }\n    )\n    val windowSize = _windowSize.asStateFlow()\n    fun setWindowSize(dpSize: DpSize) {\n        _windowSize.value = dpSize\n    }\n\n    private val _isMaximized = homePageStateToPersist.mapTwoWayStateFlow(\n        map = {\n            it.isMaximized\n        },\n        unMap = {\n            copy(isMaximized = it)\n        }\n    )\n    val isMaximized = _isMaximized.asStateFlow()\n    fun setIsMaximized(value: Boolean) {\n        _isMaximized.value = value\n    }\n\n    private val _categoriesWidth = homePageStateToPersist.mapTwoWayStateFlow(\n        map = {\n            it.categoriesWidth.dp.coerceIn(CATEGORIES_SIZE_RANGE)\n        },\n        unMap = {\n            copy(categoriesWidth = it.coerceIn(CATEGORIES_SIZE_RANGE).value)\n        }\n    )\n    val categoriesWidth = _categoriesWidth.asStateFlow()\n    fun setCategoriesWidth(updater: (Dp) -> Dp) {\n        _categoriesWidth.value = updater(_categoriesWidth.value)\n    }\n\n    private val mainItem = MutableStateFlow<Long?>(null)\n\n\n    val menu: List<MenuItem.SubMenu> = buildMenu {\n        subMenu(Res.string.file.asStringSource()) {\n            +newDownloadAction\n            +newDownloadFromClipboardAction\n            +batchDownloadAction\n            separator()\n            +requestExitAction\n\n        }\n        subMenu(Res.string.tasks.asStringSource()) {\n//            +toggleQueueAction\n            +startQueueGroupAction\n            +stopQueueGroupAction\n            separator()\n            +stopAllAction\n            separator()\n            subMenu(\n                title = Res.string.delete.asStringSource(),\n                icon = MyIcons.remove\n            ) {\n                item(Res.string.all_missing_files.asStringSource()) {\n                    requestDelete(downloadSystem.getListOfDownloadThatMissingFileOrHaveNotProgress().map { it.id })\n                }\n                item(Res.string.all_finished.asStringSource()) {\n                    requestDelete(downloadSystem.getFinishedDownloadIds())\n                }\n                item(Res.string.all_unfinished.asStringSource()) {\n                    requestDelete(downloadSystem.getUnfinishedDownloadIds())\n                }\n                item(Res.string.entire_list.asStringSource()) {\n                    requestDelete(downloadSystem.getAllDownloadIds())\n                }\n            }\n        }\n        subMenu(Res.string.tools.asStringSource()) {\n            if (AppInfo.isInDebugMode()) {\n                +dummyException\n                +dummyMessage\n                +shutdown\n                separator()\n            }\n            +browserIntegrations\n            if (Platform.isLinux()) {\n                +createDesktopEntryAction\n            }\n            separator()\n            +perHostSettings\n            +gotoSettingsAction\n        }\n        subMenu(Res.string.help.asStringSource()) {\n            +supportActionGroup\n            separator()\n            +openOpenSourceThirdPartyLibraries\n            +openTranslators\n            +donate\n            separator()\n            +checkForUpdateAction\n            +openAboutAction\n        }\n    }.filterIsInstance<MenuItem.SubMenu>()\n\n\n    private val shouldShowOptions = MutableStateFlow(false)\n    val downloadOptions = combineStateFlows(\n        shouldShowOptions,\n        selectionList,\n    ) { shouldShowOptions, selectionList ->\n        if (!shouldShowOptions) {\n            null\n        } else {\n            MenuItem.SubMenu(\n                icon = null,\n                title = if (selectionList.size == 1) {\n                    (downloadActions.defaultItem.value?.name ?: \"\")\n                        .asStringSource()\n                } else {\n                    Res.string.n_items_selected\n                        .asStringSourceWithARgs(\n                            Res.string.n_items_selected_createArgs(\n                                count = selectionList.size.toString()\n                            )\n                        )\n                },\n                items = downloadActions.menu\n            )\n        }\n    }\n\n    val tableState = TableState(\n        cells = listOf(\n            DownloadListCells.Check,\n            DownloadListCells.Name,\n            DownloadListCells.Size,\n            DownloadListCells.Status,\n            DownloadListCells.Speed,\n            DownloadListCells.TimeLeft,\n            DownloadListCells.DateAdded,\n        ),\n        forceVisibleCells = listOf(\n            DownloadListCells.Name,\n        ),\n        initialSortBy = Sort(DownloadListCells.DateAdded, Sort.DEFAULT_IS_DESCENDING)\n    ).apply {\n        homePageStateToPersist.value.downloadListState?.let {\n            load(it)\n        }\n        onPropChange.onEach {\n            homePageStateToPersist.update {\n                it.copy(downloadListState = save())\n            }\n        }.launchIn(scope)\n    }\n\n\n    fun onRequestOpenDownloadItemOption(\n        mainItem: IDownloadItemState?,\n    ) {\n        if (mainItem != null && mainItem.id !in selectionList.value) {\n            newSelection(listOf(mainItem.id))\n        }\n        this.mainItem.value = mainItem?.id\n        shouldShowOptions.update { true }\n    }\n\n    fun onRequestCloseDownloadItemOption() {\n        shouldShowOptions.update { false }\n        mainItem.value = null\n    }\n\n\n\n    fun importLinks(links: List<AddDownloadCredentialsInUiProps>) {\n        val size = links.size\n        when {\n            size <= 0 -> {\n                return\n            }\n\n            size > 0 -> {\n                requestAddNewDownload(links)\n            }\n        }\n    }\n\n    val currentActiveDrops: MutableStateFlow<List<IDownloadCredentials>?> = MutableStateFlow(null)\n\n\n    private fun parseLinks(v: String): List<IDownloadCredentials> {\n        return DownloadCredentialFromStringExtractor.extract(v)\n            .distinctBy { it.link }\n    }\n\n    fun onExternalTextDraggedIn(readText: () -> String) {\n        val v = readText()\n        val parsedLinks = parseLinks(v)\n        currentActiveDrops.update { parsedLinks }\n    }\n\n    fun onExternalFilesDraggedIn(getFilePaths: () -> List<File>) {\n        val filePaths = kotlin.runCatching { getFilePaths() }\n            .getOrNull()?.filter { it.length() <= 1024 * 1024 } ?: return\n        onExternalTextDraggedIn {\n            filePaths\n                .firstOrNull()\n                ?.readText()\n                .orEmpty()\n        }\n    }\n\n    fun onDragExit() {\n        currentActiveDrops.update { null }\n    }\n\n    fun onDropped() {\n        currentActiveDrops.value?.let {\n            importLinks(it.map {\n                AddDownloadCredentialsInUiProps(credentials = it)\n            })\n        }\n    }\n\n    fun openFolder(id: Long) {\n        scope.launch {\n            downloadItemOpener.openDownloadItemFolder(id)\n        }\n    }\n\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    init {\n        if (isFirstVisitInThisSession()) {\n            // if the app is updated then clean downloaded files\n            if (appVersionTracker.isUpgraded()) {\n                // clean update files\n                scope.launch {\n                    // temporary fix:\n                    // at the moment we relly on DownloadMonitor for getting the list of downloads by their folder\n                    // so wait for the download list to be updated by the download monitor\n                    delay(1000)\n                    // then clean up the downloaded files\n                    updateManager.cleanDownloadedFiles()\n                }\n                // show user about update\n                scope.launch {\n                    // let user focus to the app\n                    delay(1000)\n                    notificationSender.sendNotification(\n                        title = Res.string.update_updater.asStringSource(),\n                        description = Res.string.update_app_updated_to_version_n.asStringSourceWithARgs(\n                            Res.string.update_app_updated_to_version_n_createArgs(\n                                version = appVersionTracker.currentVersion.toString()\n                            )\n                        ),\n                        type = NotificationType.Success,\n                        tag = \"Updater\"\n                    )\n                }\n            }\n        }\n    }\n\n    private val downloadActions = DesktopDownloadActions(\n        scope = scope,\n        downloadSystem = downloadSystem,\n        downloadDialogManager = downloadDialogManager,\n        editDownloadDialogManager = editDownloadDialogManager,\n        fileChecksumDialogManager = fileChecksumDialogManager,\n        selections = selectionListItems,\n        mainItem = mainItem,\n        queueManager = queueManager,\n        categoryManager = categoryManager,\n        openFile = this::openFile,\n        openFolder = this::openFolder,\n        requestDelete = this::requestDelete,\n    )\n\n    override val shortcutManager = DesktopShortcutManager().apply {\n        val isMac = Platform.isMac()\n        val metaKey = if (isMac) \"meta\" else \"ctrl\"\n        if (isMac) {\n            KeyEvent.VK_BACK_SPACE to downloadActions.deleteAction\n        } else {\n            \"DELETE\" to downloadActions.deleteAction\n        }\n        \"$metaKey N\" to newDownloadAction\n        \"$metaKey V\" to newDownloadFromClipboardAction\n        \"$metaKey C\" to downloadActions.copyDownloadLinkAction\n        \"$metaKey alt S\" to gotoSettingsAction\n        \"$metaKey Q\" to requestExitAction\n        \"$metaKey O\" to downloadActions.openFileAction\n        \"$metaKey F\" to downloadActions.openFolderAction\n        \"$metaKey E\" to downloadActions.editDownloadAction\n        \"$metaKey P\" to downloadActions.pauseAction\n        \"$metaKey R\" to downloadActions.resumeAction\n        \"$metaKey I\" to downloadActions.openDownloadDialogAction\n    }\n    val showLabels = appSettings.showIconLabels\n    val headerActions = buildMenu {\n        separator()\n        +downloadActions.resumeAction\n        +downloadActions.pauseAction\n        separator()\n        +startQueueGroupAction\n        +stopQueueGroupAction\n        +openQueuesAction\n        separator()\n        +stopAllAction\n        separator()\n        +downloadActions.deleteAction\n        separator()\n        +gotoSettingsAction\n    }\n\n    companion object {\n        private var homeComponentCreationCount = 0\n        val CATEGORIES_SIZE_RANGE = 0.dp..500.dp\n    }\n    sealed interface Effects : BaseHomeComponent.Effects.PlatformEffects {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.draganddrop.dragAndDropTarget\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draganddrop.DragAndDropEvent\nimport androidx.compose.ui.draganddrop.DragAndDropTarget\nimport androidx.compose.ui.draganddrop.awtTransferable\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport com.abdownloadmanager.desktop.pages.home.sections.DownloadList\nimport com.abdownloadmanager.desktop.pages.home.sections.SearchBox\nimport com.abdownloadmanager.shared.pages.home.category.DefinedStatusCategories\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.desktop.pages.home.sections.category.StatusFilterItem\nimport com.abdownloadmanager.desktop.pages.home.sections.queue.QueuesSection\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.WindowEnd\nimport com.abdownloadmanager.desktop.window.custom.WindowStart\nimport com.abdownloadmanager.desktop.window.custom.WindowTitlePosition\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.home.BaseHomeComponent\nimport com.abdownloadmanager.shared.pages.home.CategoryActions\nimport com.abdownloadmanager.shared.pages.home.CategoryDeletePromptState\nimport com.abdownloadmanager.shared.pages.home.ConfirmPromptState\nimport com.abdownloadmanager.shared.pages.home.DeletePromptState\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MenuBar\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.ShowOptionsInPopup\nimport com.abdownloadmanager.shared.ui.widget.menu.native.NativeMenuBar\nimport com.abdownloadmanager.shared.util.LocalSpeedUnit\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.rememberIconPainter\nimport com.abdownloadmanager.shared.util.convertPositiveBytesToSizeUnit\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithTitleBarDirection\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.compose.localizationmanager.WithLanguageDirection\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.LocalFrameWindowScope\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isMac\nimport kotlinx.coroutines.launch\nimport java.awt.datatransfer.DataFlavor\nimport java.io.File\n\n\n@Composable\nfun HomePage(component: HomeComponent) {\n    val listState by component.downloadList.collectAsState()\n    var isDragging by remember { mutableStateOf(false) }\n\n    var showDeletePromptState by remember {\n        mutableStateOf(null as DeletePromptState?)\n    }\n\n    var showDeleteCategoryPromptState by remember {\n        mutableStateOf(null as CategoryDeletePromptState?)\n    }\n\n    var showConfirmPrompt by remember {\n        mutableStateOf(null as ConfirmPromptState?)\n    }\n\n    val coroutineScope = rememberCoroutineScope()\n    val lazyListState = rememberLazyListState()\n    val tableState = component.tableState\n\n    HandleEffects(component) { effect ->\n        when (effect) {\n            is BaseHomeComponent.Effects.Common -> {\n                when (effect) {\n                    is BaseHomeComponent.Effects.Common.DeleteItems -> {\n                        if (effect.list.isNotEmpty()) {\n                            showDeletePromptState = DeletePromptState(\n                                downloadList = effect.list,\n                                finishedCount = effect.finishedCount,\n                                unfinishedCount = effect.unfinishedCount,\n                            )\n                        }\n                    }\n\n                    is BaseHomeComponent.Effects.Common.DeleteCategory -> {\n                        showDeleteCategoryPromptState = CategoryDeletePromptState(effect.category)\n                    }\n\n                    is BaseHomeComponent.Effects.Common.AutoCategorize -> {\n                        showConfirmPrompt = ConfirmPromptState(\n                            title = Res.string.confirm_auto_categorize_downloads_title.asStringSource(),\n                            description = Res.string.confirm_auto_categorize_downloads_description.asStringSource(),\n                            onConfirm = component::onConfirmAutoCategorize\n                        )\n                    }\n\n                    is BaseHomeComponent.Effects.Common.ResetCategoriesToDefault -> {\n                        showConfirmPrompt = ConfirmPromptState(\n                            title = Res.string.confirm_reset_to_default_categories_title.asStringSource(),\n                            description = Res.string.confirm_reset_to_default_categories_description.asStringSource(),\n                            onConfirm = component::onConfirmResetCategories\n                        )\n                    }\n\n                    is BaseHomeComponent.Effects.Common.ScrollToDownloadItem -> {\n                        val id = effect.downloadId\n                        val positionOrNull = tableState\n                            .getItemPosition(listState) { it.id == id }\n                            .takeIf { it != -1 }\n                        positionOrNull?.let { index ->\n                            if (effect.skipIfVisible) {\n                                val isVisible = lazyListState.layoutInfo.visibleItemsInfo\n                                    .any { it.index == index }\n                                if (isVisible) {\n                                    return@let\n                                }\n                            }\n                            coroutineScope.launch {\n                                lazyListState.scrollToItem(index)\n                            }\n                        }\n                    }\n                }\n            }\n\n            is HomeComponent.Effects -> {\n                when (effect) {\n                    HomeComponent.Effects.BringToFront -> {\n                        // handled else where\n                    }\n                }\n            }\n            else -> {}\n        }\n    }\n    showDeletePromptState?.let {\n        ShowDeletePrompts(\n            deletePromptState = it,\n            onCancel = {\n                showDeletePromptState = null\n            },\n            onConfirm = {\n                showDeletePromptState = null\n                component.confirmDelete(it)\n            })\n    }\n    showDeleteCategoryPromptState?.let {\n        ShowDeleteCategoryPrompt(\n            deletePromptState = it,\n            onCancel = {\n                showDeleteCategoryPromptState = null\n            },\n            onConfirm = {\n                showDeleteCategoryPromptState = null\n                component.onConfirmDeleteCategory(it)\n            })\n    }\n    showConfirmPrompt?.let {\n        ShowConfirmPrompt(\n            promptState = it,\n            onCancel = {\n                showConfirmPrompt = null\n            },\n            onConfirm = {\n                showConfirmPrompt?.onConfirm?.invoke()\n                showConfirmPrompt = null\n            }\n        )\n    }\n    val mergeTopBar = shouldMergeTopBarWithTitleBar(component)\n    if (mergeTopBar) {\n        WindowTitlePosition(\n            TitlePosition(\n                centered = true,\n                afterStart = true,\n                padding = PaddingValues(end = 32.dp)\n            )\n        )\n        WindowStart {\n            HomeMenuBar(component, Modifier.fillMaxHeight())\n        }\n        WindowEnd {\n            HomeSearch(\n                component = component,\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(vertical = 2.dp)\n            )\n        }\n    } else {\n        WindowTitlePosition(\n            TitlePosition(centered = false, afterStart = false)\n        )\n    }\n\n    Box(\n        Modifier\n            .fillMaxSize()\n            .dragAndDropTarget(\n                shouldStartDragAndDrop = {\n                    if (it.awtTransferable.isDataFlavorSupported(DownloadItemListDataFlavor)) {\n                        // this item is ours we don't want to use our download item for import list usage\n                        return@dragAndDropTarget false\n                    } else it.awtTransferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||\n                            it.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor)\n                },\n                target = remember {\n                    object : DragAndDropTarget {\n                        private fun onDraggedIn(event: DragAndDropEvent) {\n                            if (event.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {\n                                component.onExternalTextDraggedIn {\n                                    (event.awtTransferable.getTransferData(\n                                        DataFlavor.stringFlavor\n                                    ) as String)\n                                }\n                                return\n                            }\n\n                            if (event.awtTransferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {\n                                component.onExternalFilesDraggedIn {\n                                    (event.awtTransferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>).filterIsInstance<File>()\n                                }\n                                return\n                            }\n                        }\n\n                        override fun onStarted(event: DragAndDropEvent) {\n                            isDragging = true\n                            onDraggedIn(event)\n                        }\n\n                        override fun onEnded(event: DragAndDropEvent) {\n                            isDragging = false\n                            component.onDragExit()\n                        }\n\n                        override fun onDrop(event: DragAndDropEvent): Boolean {\n                            isDragging = false\n                            if (Platform.isMac()) {\n                                onDraggedIn(event)\n                            }\n                            component.onDropped()\n                            return true\n                        }\n                    }\n                }\n            )\n    ) {\n        Column(\n            Modifier.alpha(\n                animateFloatAsState(if (isDragging) 0.2f else 1f).value\n            )\n        ) {\n            if (!mergeTopBar) {\n                WithTitleBarDirection {\n                    Spacer(Modifier.height(4.dp))\n                    TopBar(component)\n                    Spacer(Modifier.height(6.dp))\n                }\n            }\n            Spacer(\n                Modifier.fillMaxWidth()\n                    .height(1.dp)\n                    .background(myColors.surface)\n            )\n            Row {\n                val categoriesWidth by component.categoriesWidth.collectAsState()\n                Column(\n                    Modifier\n                        .padding(top = 8.dp).width(categoriesWidth)\n                        .verticalScroll(rememberScrollState())\n                ) {\n                    Categories(\n                        modifier = Modifier.fillMaxWidth(),\n                        component = component,\n                    )\n                    Spacer(Modifier.size(8.dp))\n                    QueuesSection(\n                        modifier = Modifier.fillMaxWidth(),\n                        component = component,\n                    )\n                    Spacer(Modifier.size(8.dp))\n                }\n                Spacer(Modifier.size(8.dp))\n                //split pane\n                Handle(\n                    Modifier.width(5.dp)\n                        .fillMaxHeight()\n                ) { delta ->\n                    component.setCategoriesWidth { it + delta }\n                }\n                Column(Modifier.weight(1f)) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Spacer(Modifier.size(4.dp))\n                        AddUrlButton {\n                            component.requestEnterNewURL()\n                        }\n                        Actions(\n                            component.headerActions,\n                            component.showLabels.collectAsState().value\n                        )\n                    }\n                    var lastSelected by remember { mutableStateOf(null as Long?) }\n                    DownloadList(\n                        modifier = Modifier\n                            .padding(horizontal = 4.dp)\n                            .fillMaxWidth()\n                            .weight(1f),\n                        downloadList = listState,\n                        downloadOptions = component.downloadOptions.collectAsState().value,\n                        onRequestCloseOption = {\n                            component.onRequestCloseDownloadItemOption()\n                        },\n                        onRequestOpenOption = { itemState ->\n                            component.onRequestOpenDownloadItemOption(itemState)\n                        },\n                        selectionList = component.selectionList.collectAsState().value,\n                        onItemSelectionChange = { id, checked ->\n                            lastSelected = id\n                            component.onItemSelectionChange(id, checked)\n                        },\n                        onRequestOpenDownload = {\n                            component.openFileOrShowProperties(it)\n                        },\n                        onNewSelection = {\n                            component.newSelection(ids = it)\n                        },\n                        lastSelectedId = lastSelected,\n                        tableState = tableState,\n                        fileIconProvider = component.fileIconProvider,\n                        categoryManager = component.categoryManager,\n                        lazyListState = lazyListState,\n                    )\n                    Spacer(\n                        Modifier\n                            .fillMaxWidth()\n                            .height(2.dp)\n                            .background(\n                                myColors.surface\n                            )\n                    )\n                    Footer(component)\n                }\n            }\n        }\n        NotificationArea(\n            Modifier\n                .width(310.dp)\n                .padding(24.dp)\n                .align(Alignment.BottomEnd)\n        )\n        AnimatedVisibility(\n            visible = isDragging,\n            enter = fadeIn(),\n            exit = fadeOut(),\n        ) {\n            DragWidget(\n                Modifier.fillMaxSize()\n                    .wrapContentSize(Alignment.Center),\n                component.currentActiveDrops.value?.size,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun shouldMergeTopBarWithTitleBar(component: HomeComponent): Boolean {\n    val mergeTopBarWithTitleBarInSettings = component.mergeTopBarWithTitleBar.collectAsState().value\n    if (!mergeTopBarWithTitleBarInSettings) return false\n    val density = LocalDensity.current\n    val widthDp = density.run {\n        LocalWindowInfo.current.containerSize.width.toDp()\n    }\n    return widthDp > 700.dp\n}\n\n\n@Composable\nprivate fun ShowDeletePrompts(\n    deletePromptState: DeletePromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val shape = myShapes.defaultRounded\n    Dialog(onDismissRequest = onCancel) {\n        Column(\n            Modifier\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n                .padding(16.dp)\n                .width(IntrinsicSize.Max)\n                .widthIn(max = 260.dp)\n        ) {\n            Text(\n                myStringResource(Res.string.confirm_delete_download_items_title),\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.xl,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            val finishedCount = deletePromptState.finishedCount\n            val unfinishedCount = deletePromptState.unfinishedCount\n            Text(\n                when {\n                    deletePromptState.hasBothFinishedAndUnfinished() -> {\n                        Res.string.confirm_delete_download_finished_and_unfinished_items_description.asStringSourceWithARgs(\n                            Res.string.confirm_delete_download_finished_and_unfinished_items_description_createArgs(\n                                finishedCount = finishedCount.toString(),\n                                unfinishedCount = unfinishedCount.toString(),\n                            )\n                        )\n                    }\n\n                    deletePromptState.hasUnfinishedDownloads -> {\n                        Res.string.confirm_delete_download_unfinished_items_description.asStringSourceWithARgs(\n                            Res.string.confirm_delete_download_unfinished_items_description_createArgs(\n                                count = unfinishedCount.toString(),\n                            )\n                        )\n                    }\n\n                    else -> {\n                        Res.string.confirm_delete_download_items_description.asStringSourceWithARgs(\n                            Res.string.confirm_delete_download_items_description_createArgs(\n                                count = finishedCount.toString()\n                            ),\n                        )\n                    }\n                }.rememberString(),\n                fontSize = myTextSizes.base,\n                color = myColors.onBackground,\n            )\n            if (deletePromptState.hasFinishedDownloads) {\n                Spacer(Modifier.height(12.dp))\n                val alsoDeleteFileInteractionSource = remember { MutableInteractionSource() }\n                Row(\n                    Modifier\n                        .clickable(\n                            interactionSource = alsoDeleteFileInteractionSource,\n                            indication = null\n                        ) {\n                            deletePromptState.alsoDeleteFile = !deletePromptState.alsoDeleteFile\n                        },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    CheckBox(\n                        value = deletePromptState.alsoDeleteFile,\n                        onValueChange = {\n                            deletePromptState.alsoDeleteFile = it\n                        },\n                        modifier = Modifier\n                            // the Row itself is clickable (focusable) so we don't need to focus this checkbox\n                            // is there a better way?\n                            .focusProperties { canFocus = false },\n                        interactionSource = alsoDeleteFileInteractionSource,\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Text(\n                        myStringResource(Res.string.also_delete_file_from_disk),\n                        fontSize = myTextSizes.base,\n                        color = myColors.onBackground,\n                    )\n                }\n            }\n            Spacer(Modifier.height(12.dp))\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                val confirmFocusRequester = remember { FocusRequester() }\n                LaunchedEffect(Unit) {\n                    confirmFocusRequester.requestFocus()\n                }\n                Spacer(Modifier.weight(1f))\n                ActionButton(\n                    text = myStringResource(Res.string.delete),\n                    onClick = onConfirm,\n                    focusedBorderColor = SolidColor(myColors.error),\n                    contentColor = myColors.error,\n                    modifier = Modifier.focusRequester(confirmFocusRequester)\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ShowConfirmPrompt(\n    promptState: ConfirmPromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val shape = myShapes.defaultRounded\n    Dialog(onDismissRequest = onCancel) {\n        Column(\n            Modifier\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n                .padding(16.dp)\n                .width(IntrinsicSize.Max)\n                .widthIn(max = 260.dp)\n        ) {\n            Text(\n                text = promptState.title.rememberString(),\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.xl,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            Text(\n                text = promptState.description.rememberString(),\n                fontSize = myTextSizes.base,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                val confirmFocusRequester = remember { FocusRequester() }\n                LaunchedEffect(Unit) {\n                    confirmFocusRequester.requestFocus()\n                }\n                Spacer(Modifier.weight(1f))\n                ActionButton(\n                    text = myStringResource(Res.string.ok),\n                    onClick = onConfirm,\n                    modifier = Modifier.focusRequester(confirmFocusRequester)\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ShowDeleteCategoryPrompt(\n    deletePromptState: CategoryDeletePromptState,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val shape = myShapes.defaultRounded\n    Dialog(onDismissRequest = onCancel) {\n        Column(\n            Modifier\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n                .padding(16.dp)\n                .width(IntrinsicSize.Max)\n                .widthIn(max = 260.dp)\n        ) {\n            Text(\n                myStringResource(\n                    Res.string.confirm_delete_category_item_title,\n                    Res.string.confirm_delete_category_item_title_createArgs(\n                        name = deletePromptState.category.name\n                    ),\n                ),\n                fontWeight = FontWeight.Bold,\n                fontSize = myTextSizes.xl,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            Text(\n                myStringResource(\n                    Res.string.confirm_delete_category_item_description,\n                    Res.string.confirm_delete_category_item_description_createArgs(\n                        value = deletePromptState.category.name\n                    )\n                ),\n                fontSize = myTextSizes.base,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            Text(\n                myStringResource(Res.string.your_download_will_not_be_deleted),\n                fontSize = myTextSizes.base,\n                color = myColors.onBackground,\n            )\n            Spacer(Modifier.height(12.dp))\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                val confirmFocusRequester = remember { FocusRequester() }\n                LaunchedEffect(Unit) {\n                    confirmFocusRequester.requestFocus()\n                }\n                Spacer(Modifier.weight(1f))\n                ActionButton(\n                    text = myStringResource(Res.string.delete),\n                    onClick = onConfirm,\n                    focusedBorderColor = SolidColor(myColors.error),\n                    modifier = Modifier.focusRequester(confirmFocusRequester),\n                    contentColor = myColors.error,\n                )\n                Spacer(Modifier.width(8.dp))\n                ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)\n            }\n        }\n    }\n}\n\n@Composable\nfun DragWidget(\n    modifier: Modifier,\n    linkCount: Int?,\n) {\n    val shape = RoundedCornerShape(12.dp)\n    val background = myColors.onBackground / 10\n    Column(\n        modifier\n            .clip(shape)\n            .background(background)\n            .padding(8.dp)\n            .dashedBorder(\n                shape = shape,\n                width = 2.dp,\n                color = myColors.onBackground,\n                on = 1.dp,\n                off = 4.dp\n            )\n            .padding(32.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        MyIcon(\n            MyIcons.download,\n            null,\n            Modifier.size(36.dp),\n        )\n        Text(\n            text = myStringResource(Res.string.drop_link_or_file_here),\n            fontSize = myTextSizes.xl\n        )\n        if (linkCount != null && Platform.isMac().not()) {\n            when {\n                linkCount > 0 -> {\n                    Text(\n                        myStringResource(\n                            Res.string.n_links_will_be_imported,\n                            Res.string.n_links_will_be_imported_createArgs(\n                                count = linkCount.toString()\n                            )\n                        ),\n                        fontSize = myTextSizes.base,\n                        color = myColors.success,\n                    )\n                }\n\n                linkCount == 0 -> {\n                    Text(myStringResource(Res.string.nothing_will_be_imported))\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun Categories(\n    modifier: Modifier,\n    component: HomeComponent,\n) {\n\n    val currentTypeFilter = component.filterState.typeCategoryFilter\n    val currentStatusFilter = component.filterState.statusFilter\n    val categories by component.categoryManager.categoriesFlow.collectAsState()\n    val clipShape = myShapes.defaultRounded\n    val showCategoryOption by component.categoryActions.collectAsState()\n\n    fun showCategoryOption(item: Category?) {\n        component.showCategoryOptions(item)\n    }\n\n    fun closeCategoryOptions() {\n        component.closeCategoryOptions()\n    }\n    Column(\n        modifier\n            .padding(start = 16.dp)\n            .clip(clipShape)\n            .border(1.dp, myColors.surface, clipShape)\n            .padding(1.dp)\n    ) {\n        var expendedItem: DownloadStatusCategoryFilter? by remember {\n            mutableStateOf(\n                currentStatusFilter\n            )\n        }\n        for (statusCategoryFilter in DefinedStatusCategories.values()) {\n            StatusFilterItem(\n                isExpanded = expendedItem == statusCategoryFilter,\n                currentTypeCategoryFilter = currentTypeFilter,\n                currentStatusCategoryFilter = currentStatusFilter,\n                statusFilter = statusCategoryFilter,\n                categories = categories,\n                onFilterChange = {\n                    component.onCategoryFilterChange(statusCategoryFilter, it)\n                },\n                onRequestExpand = { expand ->\n                    expendedItem = statusCategoryFilter.takeIf { expand }\n                },\n                onItemsDroppedInCategory = { category, ids ->\n                    component.moveItemsToCategory(category, ids)\n                },\n                onRequestOpenOptionMenu = {\n                    showCategoryOption(it)\n                },\n                onCategoryReorderRequest = {fromIndex, delta ->\n                    component.reorderCategory(fromIndex, delta)\n                }\n            )\n        }\n    }\n    showCategoryOption?.let {\n        CategoryOption(\n            categoryOptionMenuState = it,\n            onDismiss = {\n                closeCategoryOptions()\n            }\n        )\n    }\n}\n\n@Composable\nfun CategoryOption(\n    categoryOptionMenuState: CategoryActions,\n    onDismiss: () -> Unit,\n) {\n    ShowOptionsInPopup(\n        MenuItem.SubMenu(\n            icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),\n            title = categoryOptionMenuState.categoryItem?.name?.asStringSource()\n                ?: Res.string.categories.asStringSource(),\n            categoryOptionMenuState.menu,\n        ),\n        onDismiss\n    )\n}\n\n@Composable\nprivate fun HomeMenuBar(\n    component: HomeComponent,\n    modifier: Modifier,\n) {\n    val nativeMenuBarWithTitleBarInSettings by component.useNativeMenuBar.collectAsState()\n    val menu = component.menu\n    if (nativeMenuBarWithTitleBarInSettings) {\n        val scope = LocalFrameWindowScope.current\n        NativeMenuBar(scope, menu)\n    } else {\n        MenuBar(\n            modifier,\n            menu\n        )\n    }\n}\n\n@Composable\nprivate fun Footer(component: HomeComponent) {\n    Row(\n        modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),\n        horizontalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Spacer(Modifier.weight(1f))\n        val activeCount by component.activeDownloadCountFlow.collectAsState()\n        FooterItem(MyIcons.activeCount, activeCount.toString(), \"\")\n        val size by component.globalSpeedFlow.collectAsState(0)\n        val speed = convertPositiveBytesToSizeUnit(size, LocalSpeedUnit.current)\n        if (speed != null) {\n            val speedText = speed.formatedValue()\n            val unitText = speed.unit.toString() + \"/s\"\n            FooterItem(MyIcons.speed, speedText, unitText)\n        }\n    }\n}\n\n@Composable\nprivate fun FooterItem(icon: IconSource, value: String, unit: String) {\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        WithContentAlpha(0.25f) {\n            MyIcon(icon, null, Modifier.size(16.dp))\n        }\n        Spacer(Modifier.width(8.dp))\n        WithContentAlpha(0.75f) {\n            Text(value, maxLines = 1, fontSize = myTextSizes.base)\n        }\n        Spacer(Modifier.width(8.dp))\n        WithContentAlpha(0.25f) {\n            Text(unit, maxLines = 1, fontSize = myTextSizes.base)\n        }\n    }\n}\n\n@Composable\nprivate fun TopBar(component: HomeComponent) {\n    Row(\n        modifier = Modifier.padding(start = 16.dp, end = 16.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        HomeMenuBar(component, Modifier)\n        Box(Modifier.weight(1f))\n        HomeSearch(\n            component = component,\n            modifier = Modifier,\n            textPadding = PaddingValues(8.dp),\n        )\n    }\n}\n\n@Composable\nfun HomeSearch(\n    component: HomeComponent,\n    modifier: Modifier,\n    textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),\n) {\n    val searchBoxInteractionSource = remember { MutableInteractionSource() }\n\n    val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()\n    WithLanguageDirection {\n        SearchBox(\n            text = component.filterState.textToSearch,\n            onTextChange = {\n                component.filterState.textToSearch = it\n            },\n            textPadding = textPadding,\n            interactionSource = searchBoxInteractionSource,\n            modifier = modifier\n                .width(\n                    animateDpAsState(\n                        if (isFocused) 220.dp else 180.dp\n                    ).value\n                )\n        )\n    }\n}\n\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePersistedState.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport arrow.optics.Lens\nimport ir.amirab.util.config.floatKeyOf\nimport ir.amirab.util.config.getDecoded\nimport ir.amirab.util.config.keyOfEncoded\nimport ir.amirab.util.config.putEncodedNullable\nimport ir.amirab.util.config.MapConfig\nimport ir.amirab.util.config.booleanKeyOf\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\n\n@Serializable\ndata class HomePageStateToPersist(\n    val downloadListState: TableState.SerializableTableState? = null,\n    val windowSize: Pair<Float, Float> = 1000f to 500f,\n    val isMaximized: Boolean = false,\n    val categoriesWidth: Float = 185f,\n) {\n    class ConfigLens(prefix: String) : Lens<MapConfig, HomePageStateToPersist>,\n        KoinComponent {\n        private val json: Json by inject()\n\n        class Keys(prefix: String) {\n            val windowWidth = floatKeyOf(\"${prefix}window.width\")\n            val windowHeight = floatKeyOf(\"${prefix}window.height\")\n            val isMaximized = booleanKeyOf(\"${prefix}window.isMaximized\")\n            val categoriesWidth = floatKeyOf(\"${prefix}categories.width\")\n            val downloadListTableState = keyOfEncoded<TableState.SerializableTableState>(\"${prefix}downloadListState\")\n        }\n\n        private val keys = Keys(prefix)\n        override fun get(source: MapConfig): HomePageStateToPersist {\n            val default by lazy { HomePageStateToPersist() }\n            return with(json) {\n                HomePageStateToPersist(\n                    downloadListState = source.getDecoded(keys.downloadListTableState),\n                    categoriesWidth = source.get(keys.categoriesWidth) ?: default.categoriesWidth,\n                    windowSize = run {\n                        val width = source.get(keys.windowWidth)\n                        val height = source.get(keys.windowHeight)\n                        if (height != null && width != null) {\n                            width to height\n                        } else {\n                            default.windowSize\n                        }\n                    },\n                    isMaximized = source.get(keys.isMaximized) ?: default.isMaximized,\n                )\n            }\n        }\n\n        override fun set(source: MapConfig, focus: HomePageStateToPersist): MapConfig {\n            with(json) {\n                source.put(keys.windowWidth, focus.windowSize.first)\n                source.put(keys.windowHeight, focus.windowSize.second)\n                source.put(keys.isMaximized, focus.isMaximized)\n                source.put(keys.categoriesWidth, focus.categoriesWidth)\n                source.putEncodedNullable(keys.downloadListTableState, focus.downloadListState)\n            }\n            return source\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.window.WindowPlacement\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.util.LocalShortCutManager\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.rememberWindowController\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport java.awt.Dimension\n\n@Composable\nfun HomeWindow(\n    homeComponent: HomeComponent,\n    onCLoseRequest: () -> Unit,\n) {\n    val size by homeComponent.windowSize.collectAsState()\n    val isMaximized by homeComponent.isMaximized.collectAsState()\n    val windowState = rememberWindowState(\n        size = size,\n        position = WindowPosition.Aligned(Alignment.Center),\n        placement = if (isMaximized) {\n            WindowPlacement.Maximized\n        } else {\n            WindowPlacement.Floating\n        }\n    )\n    val onCloseRequest = onCLoseRequest\n    val windowIcon = MyIcons.appIcon\n    val windowController = rememberWindowController(\n        AppInfo.displayName,\n        windowIcon.rememberPainter(),\n    )\n\n    CompositionLocalProvider(\n        LocalShortCutManager provides homeComponent.shortcutManager\n    ) {\n        CustomWindow(\n            state = windowState,\n            onCloseRequest = onCloseRequest,\n            windowController = windowController,\n            onKeyEvent = {\n                homeComponent.shortcutManager.handle(it)\n            }\n        ) {\n            LaunchedEffect(windowState.size) {\n                if (!windowState.isMinimized && windowState.placement == WindowPlacement.Floating) {\n                    homeComponent.setWindowSize(windowState.size)\n                }\n            }\n            LaunchedEffect(windowState.placement) {\n                homeComponent.setIsMaximized(windowState.placement == WindowPlacement.Maximized)\n            }\n            window.minimumSize = Dimension(\n                400, 400\n            )\n            HandleEffects(homeComponent) {\n                when (it) {\n                    HomeComponent.Effects.BringToFront -> {\n                        windowState.isMinimized = false\n                        window.toFront()\n                    }\n\n                    else -> {}\n                }\n            }\n            BoxWithConstraints {\n                HomePage(homeComponent)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/dropDownloadItemsHere.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home\n\nimport androidx.compose.foundation.draganddrop.dragAndDropTarget\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draganddrop.DragAndDropEvent\nimport androidx.compose.ui.draganddrop.DragAndDropTarget\nimport androidx.compose.ui.draganddrop.awtTransferable\nimport ir.amirab.downloader.monitor.IDownloadItemState\n\ninternal fun Modifier.dropDownloadItemsHere(\n    onDragIn: () -> Unit,\n    onDragDone: () -> Unit,\n    onItemsDropped: (ids: List<Long>) -> Unit,\n): Modifier {\n    return composed {\n        val onDragIn by rememberUpdatedState(onDragIn)\n        val onDragDone by rememberUpdatedState(onDragDone)\n        val onItemsDropped by rememberUpdatedState(onItemsDropped)\n        dragAndDropTarget(\n            shouldStartDragAndDrop = {\n                it.awtTransferable.isDataFlavorSupported(DownloadItemListDataFlavor)\n            },\n            target = remember {\n                object : DragAndDropTarget {\n                    override fun onEntered(event: DragAndDropEvent) {\n                        onDragIn()\n                    }\n\n                    override fun onExited(event: DragAndDropEvent) {\n                        onDragDone()\n                    }\n\n                    override fun onDrop(event: DragAndDropEvent): Boolean {\n                        onDragDone()\n                        val items = (event.awtTransferable.getTransferData(DownloadItemListDataFlavor) as List<*>)\n                            .filterIsInstance<IDownloadItemState>()\n                        onItemsDropped(items.map { it.id })\n                        return true\n                    }\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/DownloadList.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home.sections\n\nimport com.abdownloadmanager.shared.util.DOUBLE_CLICK_DELAY\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.styled.MyStyledTableHeader\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.LocalMenuDisabledItemBehavior\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MenuDisabledItemBehavior\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.ShowOptionsInPopup\nimport ir.amirab.util.compose.action.MenuItem\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.draganddrop.dragAndDropSource\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draganddrop.DragAndDropTransferAction\nimport androidx.compose.ui.draganddrop.DragAndDropTransferData\nimport androidx.compose.ui.draganddrop.DragAndDropTransferable\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.*\nimport androidx.compose.ui.input.pointer.*\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.pages.home.DownloadItemTransferable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CustomCellRenderer\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.SortableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.rememberCategoryOf\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.downloader.monitor.*\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.isCtrlPressed\nimport ir.amirab.util.desktop.isShiftPressed\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.delay\n\n\nclass DownloadListContext(\n    val onNewSelection: (List<Long>) -> Unit,\n    val downloadList: List<IDownloadItemState>,\n    val isAllSelected: Boolean,\n) {\n    fun newSelection(ids: List<Long>, isSelected: Boolean) {\n        onNewSelection(ids.filter { isSelected })\n    }\n\n    fun changeAllSelection(isSelected: Boolean) {\n        newSelection(downloadList.map { it.id }, isSelected)\n    }\n}\n\nprivate val LocalDownloadListContext = compositionLocalOf<DownloadListContext> {\n    error(\"DownloadListContext not provided\")\n}\n\n@Composable\nfun DownloadList(\n    modifier: Modifier,\n    downloadList: List<IDownloadItemState>,\n    downloadOptions: MenuItem.SubMenu?,\n    onRequestOpenOption: (IDownloadItemState) -> Unit,\n    tableState: TableState<IDownloadItemState, DownloadListCells>,\n    onRequestCloseOption: () -> Unit,\n    selectionList: List<Long>,\n    onItemSelectionChange: (Long, Boolean) -> Unit,\n    onRequestOpenDownload: (Long) -> Unit,\n    onNewSelection: (List<Long>) -> Unit,\n    lastSelectedId: Long?,\n    fileIconProvider: FileIconProvider,\n    categoryManager: CategoryManager,\n    lazyListState: LazyListState\n) {\n    ShowDownloadOptions(\n        downloadOptions, onRequestCloseOption\n    )\n    val isALlSelected by derivedStateOf {\n        val list = downloadList\n        if (list.isEmpty()) {\n            false\n        } else {\n            list.map { it.id }.all {\n                it in selectionList\n            }\n        }\n    }\n\n    val listToBeDragged by rememberUpdatedState(\n        downloadList.filter { it.id in selectionList }\n    )\n\n    val tableInteractionSource = remember { MutableInteractionSource() }\n\n    fun newSelection(ids: List<Long>, isSelected: Boolean) {\n        onNewSelection(ids.filter { isSelected })\n    }\n\n    fun changeAllSelection(isSelected: Boolean) {\n        newSelection(downloadList.map { it.id }, isSelected)\n    }\n\n    val windowInfo = LocalWindowInfo.current\n    CompositionLocalProvider(\n        LocalDownloadListContext provides DownloadListContext(\n            onNewSelection,\n            downloadList,\n            isALlSelected,\n        )\n    ) {\n        val itemHorizontalPadding = 16.dp\n        Table(\n            tableState = tableState,\n            listState = lazyListState,\n            key = { it.id },\n            list = downloadList,\n            modifier = modifier\n                .onKeyEvent {\n                    if (it.key == Key.A && isCtrlPressed(windowInfo)) {\n                        changeAllSelection(true)\n                        true\n                    } else {\n                        false\n                    }\n                }\n                .onKeyEvent {\n                    if (it.key == Key.Escape) {\n                        changeAllSelection(false)\n                        true\n                    } else {\n                        false\n                    }\n                }\n                .clickable(\n                    indication = null,\n                    interactionSource = tableInteractionSource,\n                    onClick = {\n                        //deselect all on click empty area\n                        changeAllSelection(false)\n                    },\n                ),\n            drawOnEmpty = {\n                WithContentAlpha(0.75f) {\n                    Text(myStringResource(Res.string.list_is_empty), Modifier.align(Alignment.Center))\n                }\n            },\n            wrapHeader = {\n                MyStyledTableHeader(itemHorizontalPadding = itemHorizontalPadding, content = it)\n            },\n            wrapItem = { _, item, rowContent ->\n                val isSelected = selectionList.contains(item.id)\n                var shouldWaitForSecondClick by remember {\n                    mutableStateOf(false)\n                }\n                LaunchedEffect(shouldWaitForSecondClick) {\n                    delay(DOUBLE_CLICK_DELAY)\n                    if (shouldWaitForSecondClick) {\n                        shouldWaitForSecondClick = false\n                    }\n                }\n                val itemInteractionSource = remember { MutableInteractionSource() }\n                CompositionLocalProvider(\n                    LocalDownloadItemProperties provides DownloadItemProperties(\n                        isSelected,\n                        item,\n                    )\n                ) {\n                    val windowInfo = LocalWindowInfo.current\n                    WithContentAlpha(1f) {\n                        val shape = myShapes.defaultRounded\n                        Box(\n                            Modifier\n                                .widthIn(min = getTableSize().visibleWidth)\n                                .ifThen(isSelected) {\n                                    dragAndDropSource(\n                                        drawDragDecoration = {},\n                                        transferData = {\n                                            val selectedDownloads = listToBeDragged\n                                            if (selectedDownloads.isEmpty() || !isSelected) {\n                                                return@dragAndDropSource null\n                                            }\n                                            val shiftPressed = isShiftPressed(windowInfo)\n                                            val supportedActions = listOf(\n                                                if (shiftPressed) {\n                                                    DragAndDropTransferAction.Move\n                                                } else {\n                                                    DragAndDropTransferAction.Copy\n                                                }\n                                            )\n                                            DragAndDropTransferData(\n                                                transferable = DragAndDropTransferable(\n                                                    DownloadItemTransferable(selectedDownloads)\n                                                ),\n                                                supportedActions = supportedActions,\n                                            )\n                                        }\n                                    )\n                                }\n                                .onClick(\n                                    interactionSource = itemInteractionSource\n                                ) {\n                                    if (shouldWaitForSecondClick) {\n                                        onRequestOpenDownload(item.id)\n                                        shouldWaitForSecondClick = false\n                                    } else {\n                                        if (isCtrlPressed(windowInfo)) {\n                                            onItemSelectionChange(item.id, !isSelected)\n                                        } else {\n                                            changeAllSelection(false)\n                                            onItemSelectionChange(item.id, true)\n                                            shouldWaitForSecondClick = true\n                                        }\n                                    }\n                                }\n                                .onClick(\n                                    matcher = PointerMatcher.mouse(PointerButton.Secondary),\n                                ) {\n                                    onRequestOpenOption(item)\n                                }\n                                .onClick(\n                                    enabled = lastSelectedId != null,\n                                    keyboardModifiers = {\n                                        this.isShiftPressed\n                                    }\n                                ) {\n\n                                    val lastSelected = lastSelectedId ?: return@onClick\n                                    val currentId = item.id\n\n                                    val ids = tableState.getARangeOfItems(\n                                        list = downloadList,\n                                        id = { it.id },\n                                        fromItem = lastSelected,\n                                        toItem = currentId,\n                                    )\n                                    newSelection(ids, true)\n                                }\n                                .padding(vertical = 1.dp)\n                                .clip(shape)\n                                .indication(\n                                    interactionSource = itemInteractionSource,\n                                    indication = LocalIndication.current\n                                )\n                                .hoverable(itemInteractionSource)\n                                .focusable(\n                                    interactionSource = itemInteractionSource\n                                )\n                                .let {\n                                    if (isSelected) {\n                                        val selectionColor = myColors.onBackground\n                                        it\n                                            .border(\n                                                1.dp,\n                                                myColors.selectionGradient(0.10f, 0.05f, selectionColor),\n                                                shape\n                                            )\n                                            .background(myColors.selectionGradient(0.15f, 0.03f, selectionColor))\n                                    } else {\n                                        it.border(1.dp, Color.Transparent)\n                                    }\n                                }\n                                .padding(vertical = 6.dp, horizontal = itemHorizontalPadding)\n                        ) {\n                            rowContent()\n                        }\n                    }\n                }\n            }\n        ) { cell, item ->\n            when (cell) {\n                DownloadListCells.Check -> {\n                    CheckCell(\n                        onCheckedChange = { downloadId, isChecked ->\n                            val currentSelection = selectionList.find {\n                                downloadId == it\n                            }?.let { true } ?: false\n                            onItemSelectionChange(downloadId, !currentSelection)\n                        },\n                        dItemState = item\n                    )\n                }\n\n                DownloadListCells.Name -> {\n                    NameCell(\n                        itemState = item,\n                        category = categoryManager.rememberCategoryOf(item.id),\n                        fileIconProvider = fileIconProvider,\n                    )\n                }\n\n                DownloadListCells.DateAdded -> {\n                    DateAddedCell(item)\n                }\n\n                DownloadListCells.Size -> {\n                    SizeCell(item)\n                }\n\n                DownloadListCells.Speed -> {\n                    SpeedCell(item)\n                }\n\n                DownloadListCells.Status -> {\n                    StatusCell(item)\n                }\n\n                DownloadListCells.TimeLeft -> {\n                    TimeLeftCell(item)\n                }\n            }\n        }\n    }\n}\n\nsealed interface DownloadListCells : TableCell<IDownloadItemState> {\n    data object Check : DownloadListCells,\n        CustomCellRenderer {\n        override val id: String = \"#\"\n        override val name: StringSource = \"#\".asStringSource()\n        override val size: CellSize = CellSize.Fixed(26.dp)\n\n        @Composable\n        override fun drawHeader() {\n            val c = LocalDownloadListContext.current\n            CheckBox(\n                c.isAllSelected,\n                {\n                    c.changeAllSelection(it)\n                },\n                modifier = Modifier.size(12.dp)\n            )\n        }\n    }\n\n    data object Name : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy { it.name }\n\n        override val id: String = \"Name\"\n        override val name: StringSource = Res.string.name.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(50.dp..1000.dp, 200.dp)\n    }\n\n    data object Status : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy(\n            {\n                it.statusOrFinished().order\n            }, {\n                when (it) {\n                    is CompletedDownloadItemState -> 100\n                    is ProcessingDownloadItemState -> it.percent ?: 0\n                }\n            }\n        )\n\n        override val id: String = \"Status\"\n        override val name: StringSource = Res.string.status.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..140.dp, 120.dp)\n    }\n\n    data object Size : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy { it.contentLength }\n\n        override val id: String = \"Size\"\n        override val name: StringSource = Res.string.size.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 70.dp)\n    }\n\n    data object Speed : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy { it.speedOrNull() ?: 0L }\n\n        override val id: String = \"Speed\"\n        override val name: StringSource = Res.string.speed.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 80.dp)\n    }\n\n    data object TimeLeft : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy { it.remainingOrNull() ?: Long.MAX_VALUE }\n\n        override val id: String = \"Time Left\"\n        override val name: StringSource = Res.string.time_left.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(70.dp..150.dp, 100.dp)\n    }\n\n    data object DateAdded : DownloadListCells,\n        SortableCell<IDownloadItemState> {\n        override fun comparator(): Comparator<IDownloadItemState> = compareBy { it.dateAdded }\n\n        override val id: String = \"Date Added\"\n        override val name: StringSource = Res.string.date_added.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(90.dp..150.dp, 100.dp)\n    }\n}\n\n@Composable\nfun ShowDownloadOptions(\n    options: MenuItem.SubMenu?,\n    onDismiss: () -> Unit,\n) {\n    if (options != null) {\n        CompositionLocalProvider(\n            LocalMenuDisabledItemBehavior provides MenuDisabledItemBehavior.LowerOpacity\n        ) {\n            ShowOptionsInPopup(options, onDismiss)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/Filters.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home.sections\n\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.animation.*\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldIcon\nimport com.abdownloadmanager.shared.ui.widget.MyTextFieldWithIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun SearchBox(\n    text: String,\n    onTextChange: (String) -> Unit,\n    textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    placeholder: String = myStringResource(Res.string.search_in_the_list),\n    modifier: Modifier,\n) {\n    val shape = myShapes.defaultRounded\n    val textSize = myTextSizes.base\n    MyTextField(\n        text = text,\n        fontSize = textSize,\n        onTextChange = onTextChange,\n        shape = shape,\n        textPadding = textPadding,\n        interactionSource = interactionSource,\n        start = {\n            WithContentAlpha(\n                animateFloatAsState(if (text.isBlank()) 0.9f else 1f).value\n            ) {\n                MyIcon(\n                    MyIcons.search,\n                    myStringResource(Res.string.search),\n                    Modifier\n                        .padding(start = 8.dp)\n                        .size(mySpacings.iconSize)\n                )\n            }\n        },\n        end = {\n            AnimatedVisibility(text.isNotBlank()) {\n                MyTextFieldIcon(\n                    icon = MyIcons.clear,\n                    enabled = true,\n                    contentDescription = myStringResource(Res.string.clear),\n                    onClick = {\n                        onTextChange(\"\")\n                    }\n                )\n            }\n        },\n        modifier = modifier,\n        placeholder = placeholder\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/TableDownloadItem.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.abdownloadmanager.desktop.pages.home.sections\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.*\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.Category\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.resources.MyStringResource\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.datetime.*\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.Instant\n\nval LocalDownloadItemProperties =\n    compositionLocalOf<DownloadItemProperties> { error(\"not provided download properties\") }\n\n\ndata class DownloadItemProperties(\n    val isSelected: Boolean,\n    val iDownloadItemState: IDownloadItemState,\n)\n\n\n@Composable\nprivate fun isSelected(): Boolean {\n    return LocalDownloadItemProperties.current.isSelected\n}\n\n\n@Composable\nfun CheckCell(\n    onCheckedChange: (Long, Boolean) -> Unit,\n    dItemState: IDownloadItemState,\n) {\n    val isChecked = isSelected()\n    CheckBox(\n        value = isChecked,\n        onValueChange = {\n            onCheckedChange(dItemState.id, it)\n        },\n        modifier = Modifier.focusProperties {\n            canFocus = false\n        },\n        size = 12.dp,\n    )\n}\n\n@Composable\nfun NameCell(\n    itemState: IDownloadItemState,\n    category: Category?,\n    fileIconProvider: FileIconProvider,\n) {\n    val fileIcon = fileIconProvider.rememberIcon(itemState.name)\n    Row(\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        MyIcon(\n            icon = fileIcon,\n            modifier = Modifier.size(16.dp),\n            contentDescription = null,\n//            tint = LocalContentColor.current / 75\n        )\n        Spacer(Modifier.width(6.dp))\n        Column {\n            Text(\n                text = itemState.name,\n                maxLines = 1,\n                fontSize = myTextSizes.base,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                category?.name ?: myStringResource(Res.string.general), maxLines = 1, fontSize = myTextSizes.xs,\n                color = LocalContentColor.current / 50\n            )\n        }\n    }\n\n}\n\n@Composable\nfun TimeLeftCell(\n    itemState: IDownloadItemState,\n) {\n    (itemState as? ProcessingDownloadItemState)?.remainingTime?.let { remaining ->\n        Text(\n            text = convertTimeRemainingToHumanReadable(remaining, TimeNames.ShortNames),\n            maxLines = 1,\n            fontSize = myTextSizes.base,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\n@Composable\nfun DateAddedCell(\n    itemState: IDownloadItemState,\n) {\n    var dateAddedString by remember { mutableStateOf(\"\") }\n    val useRelativeDateTime = LocalUseRelativeDateTime.current\n\n    LaunchedEffect(\n        itemState.dateAdded,\n        useRelativeDateTime,\n    ) {\n        val instant = Instant.fromEpochMilliseconds(itemState.dateAdded)\n        if (useRelativeDateTime) {\n            while (isActive) {\n                val now = Clock.System.now()\n                val period = now.periodUntil(instant, TimeZone.UTC)\n                val relativeTime = prettifyRelativeTime(period)\n                dateAddedString = relativeTime\n                delay(1000)\n            }\n        } else {\n            val dateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())\n            dateAddedString = dateTime.format(MyDateAndTimeFormats.fullDateTime)\n        }\n    }\n    Text(\n        text = dateAddedString,\n        maxLines = 1,\n        fontSize = myTextSizes.base,\n        overflow = TextOverflow.Ellipsis,\n    )\n\n}\n\n@Composable\nfun SpeedCell(\n    itemState: IDownloadItemState,\n) {\n    (itemState as? ProcessingDownloadItemState)?.speed?.let { remaining ->\n        if (itemState.status == DownloadJobStatus.Downloading) {\n            Text(\n                text = convertPositiveSpeedToHumanReadable(\n                    remaining,\n                    LocalSpeedUnit.current,\n                ),\n                maxLines = 1,\n                fontSize = myTextSizes.base,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n}\n\n@Composable\nfun SizeCell(\n    item: IDownloadItemState,\n) {\n    item.contentLength.let {\n        Text(\n            convertPositiveSizeToHumanReadable(\n                it,\n                LocalSizeUnit.current\n            ).rememberString(),\n            maxLines = 1,\n            fontSize = myTextSizes.base,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\n@Composable\nfun StatusCell(\n    itemState: IDownloadItemState,\n) {\n    when (itemState) {\n        is ProcessingDownloadItemState -> {\n            when (val status = itemState.status) {\n                is DownloadJobStatus.Canceled -> {\n                    ProgressAndPercent(\n                        itemState.percent,\n                        if (ExceptionUtils.isNormalCancellation(status.e)) {\n                            if (!itemState.gotAnyProgress) {\n                                DownloadProgressStatus.Added\n                            } else {\n                                DownloadProgressStatus.Paused\n                            }\n                        } else {\n                            DownloadProgressStatus.Error\n                        },\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                DownloadJobStatus.IDLE -> {\n                    ProgressAndPercent(\n                        itemState.percent,\n                        if (!itemState.gotAnyProgress) {\n                            DownloadProgressStatus.Added\n                        } else {\n                            DownloadProgressStatus.Paused\n                        },\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                DownloadJobStatus.Downloading -> {\n                    ProgressAndPercent(\n                        itemState.percent,\n                        DownloadProgressStatus.Downloading,\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                is DownloadJobStatus.PreparingFile -> {\n                    ProgressAndPercent(\n                        status.percent,\n                        DownloadProgressStatus.CreatingFile,\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                is DownloadJobStatus.Resuming -> {\n                    ProgressAndPercent(\n                        itemState.percent,\n                        DownloadProgressStatus.Resuming,\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                is DownloadJobStatus.Retrying -> {\n                    ProgressAndPercent(\n                        itemState.percent,\n                        DownloadProgressStatus.Retrying,\n                        itemState.gotAnyProgress,\n                        itemState.isWaiting,\n                    )\n                }\n\n                DownloadJobStatus.Finished,\n                    -> SimpleStatus(\n                    myStringResource(itemState.status.toStringResource()),\n                    myColors.success,\n                )\n            }\n        }\n\n        is CompletedDownloadItemState -> {\n            SimpleStatus(\n                myStringResource(Res.string.finished),\n                myColors.success,\n            )\n        }\n    }\n\n}\n\n@Composable\nprivate fun DownloadJobStatus.toStringResource(): MyStringResource {\n    return when (this) {\n        is DownloadJobStatus.Canceled -> {\n            Res.string.canceled\n        }\n\n        DownloadJobStatus.Downloading -> {\n            Res.string.downloading\n        }\n\n        DownloadJobStatus.Finished -> {\n            Res.string.finished\n        }\n\n        DownloadJobStatus.IDLE -> {\n            Res.string.idle\n        }\n\n        is DownloadJobStatus.PreparingFile -> {\n            Res.string.preparing_file\n        }\n\n        DownloadJobStatus.Resuming -> {\n            Res.string.resuming\n        }\n\n        is DownloadJobStatus.Retrying -> {\n            Res.string.retrying\n        }\n    }\n}\n\nprivate fun DownloadProgressStatus.toStringResource(): MyStringResource {\n    return when (this) {\n        DownloadProgressStatus.Added -> {\n            Res.string.added\n        }\n\n        DownloadProgressStatus.Error -> {\n            Res.string.error\n        }\n\n        DownloadProgressStatus.Paused -> {\n            Res.string.paused\n        }\n\n        DownloadProgressStatus.CreatingFile -> {\n            Res.string.creating_file\n        }\n\n        DownloadProgressStatus.Resuming -> {\n            Res.string.resuming\n        }\n\n        DownloadProgressStatus.Downloading -> {\n            Res.string.downloading\n        }\n\n        DownloadProgressStatus.Retrying -> {\n            Res.string.retrying\n        }\n    }\n}\n\n@Composable\nprivate fun SimpleStatus(\n    string: String,\n    color: Color = LocalContentColor.current\n) {\n    Text(\n        text = string,\n        maxLines = 1,\n        fontSize = myTextSizes.base,\n        overflow = TextOverflow.Ellipsis,\n        color = color,\n    )\n}\n\nprivate enum class DownloadProgressStatus {\n    Added, Error, Paused, CreatingFile, Resuming, Downloading, Retrying\n}\n\n@Composable\nprivate fun ProgressAndPercent(\n    percent: Int?,\n    status: DownloadProgressStatus,\n    gotAnyProgress: Boolean,\n    isWaiting: Boolean,\n) {\n    val background = when (status) {\n        DownloadProgressStatus.Error -> myColors.errorGradient\n        DownloadProgressStatus.Paused, DownloadProgressStatus.Added -> myColors.warningGradient\n        DownloadProgressStatus.CreatingFile -> myColors.infoGradient\n        DownloadProgressStatus.Resuming -> myColors.infoGradient\n        DownloadProgressStatus.Downloading -> myColors.primaryGradient\n        DownloadProgressStatus.Retrying -> myColors.errorGradient\n    }\n    val statusString = myStringResource(\n        if (isWaiting) {\n            Res.string.waiting\n        } else {\n            status.toStringResource()\n        }\n    )\n    Column {\n        val statusText = if (gotAnyProgress) {\n            \"${percent ?: \".\"}% $statusString\"\n        } else {\n            statusString\n        }\n        SimpleStatus(statusText, LocalContentColor.current)\n        if (status != DownloadProgressStatus.Added) {\n            Spacer(Modifier.height(2.5.dp))\n            ProgressStatus(\n                percent, background\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ProgressStatus(\n    percent: Int?,\n    background: Brush = myColors.primaryGradient,\n) {\n    Box(\n        Modifier\n            .fillMaxWidth()\n            .clip(CircleShape)\n            .border(Dp.Hairline, myColors.onSurface / 0.1f, CircleShape)\n            .background(myColors.surface)\n    ) {\n        if (percent != null) {\n            val w = (percent / 100f).coerceIn(0f..1f)\n            Spacer(\n                Modifier\n                    .height(5.dp)\n                    .fillMaxWidth(\n                        animateFloatAsState(\n                            w,\n                            tween(100)\n                        ).value\n                    )\n                    .background(background)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/category/Categories.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home.sections.category\n\nimport androidx.compose.animation.*\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.PointerMatcher\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.onClick\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.pages.home.dropDownloadItemsHere\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.shared.ui.widget.DelayedTooltipPopup\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.rememberIconPainter\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\nimport sh.calvin.reorderable.ReorderableColumn\nimport sh.calvin.reorderable.ReorderableListItemScope\n\n\n@Composable\nprivate fun ReorderableListItemScope.CategoryFilterItem(\n    modifier: Modifier,\n    category: Category,\n    isSelected: Boolean,\n    onItemsDropped: (ids: List<Long>) -> Unit,\n    onClick: () -> Unit,\n    isDragging: Boolean,\n) {\n    var isDraggingOnMe by remember { mutableStateOf(false) }\n    val interactionSource = remember { MutableInteractionSource() }\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    val shouldShowDragIcon = isHovered && !isDraggingOnMe || isDragging\n    Box(\n        modifier\n            .dropDownloadItemsHere(\n                onDragIn = { isDraggingOnMe = true },\n                onDragDone = { isDraggingOnMe = false },\n                onItemsDropped = onItemsDropped,\n            )\n            .hoverable(interactionSource)\n            .background(\n                if (isSelected) {\n                    myColors.onBackground / 0.05f\n                } else Color.Transparent\n            )\n            .ifThen(isDraggingOnMe) {\n                val infiniteTransition = rememberInfiniteTransition()\n                val color by infiniteTransition.animateColor(\n                    initialValue = myColors.primary,\n                    targetValue = myColors.secondary,\n                    animationSpec = infiniteRepeatable(\n                        animation = tween(1000, easing = LinearEasing),\n                        repeatMode = RepeatMode.Reverse\n                    )\n                )\n                border(1.dp, color)\n            }\n            .selectable(\n                selected = isSelected,\n                onClick = onClick\n            ),\n    ) {\n        if (isDraggingOnMe) {\n            DelayedTooltipPopup(\n                {},\n                myStringResource(Res.string.move_to_this_category),\n            )\n        }\n        Row(\n            modifier = Modifier\n                .padding(start = 24.dp)\n                .padding(horizontal = 4.dp, vertical = 6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                val iconPainter = category.rememberIconPainter()\n                MyIcon(\n                    iconPainter ?: MyIcons.folder,\n                    null,\n                    Modifier.size(16.dp),\n                )\n                Spacer(Modifier.width(4.dp))\n                Text(\n                    category.name,\n                    Modifier.weight(1f),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.base\n                )\n                AnimatedVisibility(\n                    visible = shouldShowDragIcon,\n                ) {\n                    MyIcon(\n                        MyIcons.grip,\n                        null,\n                        Modifier\n                            .draggableHandle()\n                            .size(16.dp)\n                            .alpha(\n                                if (isDragging) {\n                                    1f\n                                } else {\n                                    0.5f\n                                }\n                            )\n                    )\n                }\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier.align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n    }\n}\n\n@Composable\nfun StatusFilterItem(\n    isExpanded: Boolean,\n    onRequestExpand: (Boolean) -> Unit,\n    currentTypeCategoryFilter: Category?,\n    currentStatusCategoryFilter: DownloadStatusCategoryFilter?,\n    statusFilter: DownloadStatusCategoryFilter,\n    categories: List<Category>,\n    onCategoryReorderRequest: (index: Int, delta: Int) -> Unit,\n    onItemsDroppedInCategory: (category: Category, downloadIds: List<Long>) -> Unit,\n    onFilterChange: (\n        typeFilter: Category?,\n    ) -> Unit,\n    onRequestOpenOptionMenu: (Category?) -> Unit,\n) {\n    val isStatusSelected = currentStatusCategoryFilter == statusFilter\n    val isSelected = isStatusSelected && currentTypeCategoryFilter == null\n    ExpandableItem(\n        modifier = Modifier\n            .onClick(\n                matcher = PointerMatcher.mouse(PointerButton.Secondary),\n            ) {\n                onRequestOpenOptionMenu(null)\n            },\n        isExpanded = isExpanded,\n        header = {\n            Box(\n                Modifier\n                    .height(IntrinsicSize.Max)\n                    .background(\n                        if (isSelected) {\n                            myColors.onBackground / 0.05f\n                        } else Color.Transparent\n                    )\n                    .selectable(\n                        selected = isSelected,\n                        onClick = {\n                            if (!isExpanded) {\n                                onRequestExpand(true)\n                            }\n                            onFilterChange(null)\n                        }\n                    )\n            ) {\n                Row(\n                    Modifier.padding(vertical = 4.dp)\n                        .padding(start = 16.dp)\n                        .padding(end = 2.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                        MyIcon(\n                            statusFilter.icon,\n                            null,\n                            Modifier.size(mySpacings.iconSize)\n                        )\n                        Spacer(Modifier.width(4.dp))\n                        Text(\n                            statusFilter.name.rememberString(),\n                            Modifier.weight(1f),\n                            fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                            fontSize = myTextSizes.lg,\n                            overflow = TextOverflow.Ellipsis,\n                            maxLines = 1,\n                        )\n                        MyIcon(\n                            MyIcons.up, null, Modifier\n                                .fillMaxHeight().wrapContentHeight()\n                                .clip(CircleShape)\n                                .size(24.dp)\n                                .clickable {\n                                    onRequestExpand(!isExpanded)\n                                }\n                                .padding(6.dp)\n                                .width(16.dp)\n                                .rotate(if (isExpanded) 0f else 180f))\n                    }\n                }\n                AnimatedVisibility(\n                    isSelected,\n                    modifier = Modifier.align(Alignment.CenterStart),\n                    enter = scaleIn(),\n                    exit = scaleOut(),\n                ) {\n                    Spacer(\n                        Modifier\n                            .height(16.dp)\n                            .width(3.dp)\n                            .clip(\n                                RoundedCornerShape(\n                                    topStart = 0.dp,\n                                    bottomStart = 0.dp,\n                                    bottomEnd = 12.dp,\n                                    topEnd = 12.dp,\n                                )\n                            )\n                            .background(myColors.primary)\n                    )\n                }\n            }\n        },\n        body = {\n            ReorderableColumn(\n                list = categories,\n                onSettle = { from, to ->\n                    onCategoryReorderRequest(from, to - from)\n                },\n            ) { index, category, isDragging ->\n                key(category.id) {\n                    ReorderableItem {\n                        CategoryFilterItem(\n                            modifier = Modifier\n                                .onClick(\n                                    matcher = PointerMatcher.mouse(PointerButton.Secondary),\n                                ) {\n                                    onRequestOpenOptionMenu(category)\n                                },\n                            category = category,\n                            isSelected = isStatusSelected && currentTypeCategoryFilter == category,\n                            onItemsDropped = {\n                                onItemsDroppedInCategory(category, it)\n                            },\n                            onClick = {\n                                onFilterChange(category)\n                            },\n                            isDragging = isDragging,\n                        )\n                    }\n                    Spacer(Modifier.height(2.dp))\n                }\n            }\n        }\n    )\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/sections/queue/Queues.kt",
    "content": "package com.abdownloadmanager.desktop.pages.home.sections.queue\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateColor\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.PointerMatcher\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.layout.wrapContentHeight\nimport androidx.compose.foundation.onClick\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\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.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.pages.home.HomeComponent\nimport com.abdownloadmanager.shared.pages.home.queue.QueueActions\nimport com.abdownloadmanager.desktop.pages.home.dropDownloadItemsHere\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.DelayedTooltipPopup\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.ShowOptionsInPopup\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\ninternal fun QueuesSection(\n    modifier: Modifier,\n    component: HomeComponent,\n) {\n\n    val currentSelectedQueue = component.filterState.queueFilter\n    val queues by component.queueManager.queues.collectAsState()\n    val clipShape = myShapes.defaultRounded\n    val showQueueOption by component.queueActions.collectAsState()\n\n    fun showQueueOption(downloadQueue: DownloadQueue?) {\n        component.showCategoryOptions(downloadQueue)\n    }\n\n    fun closeQueueOptions() {\n        component.closeQueueOptions()\n    }\n\n    val (isExpanded, setExpanded) = remember { mutableStateOf(true) }\n    Column(\n        modifier\n            .padding(start = 16.dp)\n            .clip(clipShape)\n            .border(1.dp, myColors.surface, clipShape)\n            .padding(1.dp),\n    ) {\n        ExpandableItem(\n            isExpanded = isExpanded,\n            modifier = Modifier\n                .onClick(\n                    matcher = PointerMatcher.mouse(PointerButton.Secondary),\n                ) {\n                    showQueueOption(null)\n                },\n            header = {\n                Box(\n                    Modifier\n                        .height(IntrinsicSize.Max)\n                        .clickable(\n                            onClick = {\n                                setExpanded(!isExpanded)\n                            }\n                        )\n                ) {\n                    Row(\n                        Modifier.padding(vertical = 4.dp)\n                            .padding(start = 16.dp)\n                            .padding(end = 2.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        WithContentAlpha(0.75f) {\n                            MyIcon(\n                                MyIcons.queue,\n                                null,\n                                Modifier.size(mySpacings.iconSize)\n                            )\n                            Spacer(Modifier.width(4.dp))\n                            Text(\n                                myStringResource(Res.string.queues),\n                                Modifier.weight(1f),\n                                fontWeight = FontWeight.Normal,\n                                fontSize = myTextSizes.lg,\n                                overflow = TextOverflow.Ellipsis,\n                                maxLines = 1,\n                            )\n                            MyIcon(\n                                MyIcons.up, null, Modifier\n                                    .fillMaxHeight().wrapContentHeight()\n                                    .clip(CircleShape)\n                                    .size(24.dp)\n                                    .clickable {\n                                        setExpanded(!isExpanded)\n                                    }\n                                    .padding(6.dp)\n                                    .width(16.dp)\n                                    .rotate(if (isExpanded) 0f else 180f))\n                        }\n                    }\n                }\n            },\n            body = {\n                Column {\n                    queues.forEachIndexed { index, queue ->\n                        key(queue.id) {\n                            QueueFilterItem(\n                                modifier = Modifier\n                                    .onClick(\n                                        matcher = PointerMatcher.mouse(PointerButton.Secondary),\n                                    ) {\n                                        showQueueOption(queue)\n                                    },\n                                isSelected = currentSelectedQueue?.id == queue.id,\n                                onSelect = {\n                                    component.onQueueFilterChange(queue.queueModel.value)\n                                },\n                                onItemsDroppedInQueue = { downloadIds ->\n                                    component.moveItemsToQueue(queue, downloadIds)\n                                },\n                                queueModel = queue.queueModel.collectAsState().value,\n                                isActive = queue.activeFlow.collectAsState().value,\n                                parentShape = clipShape,\n                                isLast = queues.lastIndex == index\n                            )\n                        }\n                    }\n                }\n            },\n        )\n    }\n    showQueueOption?.let {\n        QueueOption(\n            queueOptionMenuState = it,\n            onDismiss = {\n                closeQueueOptions()\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun QueueFilterItem(\n    isSelected: Boolean,\n    onSelect: () -> Unit,\n    onItemsDroppedInQueue: (List<Long>) -> Unit,\n    queueModel: QueueModel,\n    isActive: Boolean,\n    modifier: Modifier = Modifier,\n    // I add this to properly create border on drag when the item is in the last position\n    isLast: Boolean,\n    parentShape: RoundedCornerShape,\n) {\n    var isDraggingOnMe by remember { mutableStateOf(false) }\n    Box(\n        modifier\n            .dropDownloadItemsHere(\n                onDragIn = { isDraggingOnMe = true },\n                onDragDone = { isDraggingOnMe = false },\n                onItemsDropped = onItemsDroppedInQueue,\n            )\n            .background(\n                if (isSelected) {\n                    myColors.onBackground / 0.05f\n                } else Color.Transparent\n            )\n            .ifThen(isDraggingOnMe) {\n                val infiniteTransition = rememberInfiniteTransition()\n                val color by infiniteTransition.animateColor(\n                    initialValue = myColors.primary,\n                    targetValue = myColors.secondary,\n                    animationSpec = infiniteRepeatable(\n                        animation = tween(1000, easing = LinearEasing),\n                        repeatMode = RepeatMode.Reverse\n                    )\n                )\n                val shape = RoundedCornerShape(0.dp).let {\n                    when {\n                        isLast -> it.copy(\n                            bottomStart = parentShape.bottomStart,\n                            bottomEnd = parentShape.bottomEnd,\n                        )\n\n                        else -> it\n                    }\n                }\n                border(1.dp, color, shape)\n            }\n            .selectable(\n                selected = isSelected,\n                onClick = {\n                    onSelect()\n                }\n            )\n    ) {\n        if (isDraggingOnMe) {\n            DelayedTooltipPopup(\n                {},\n                myStringResource(Res.string.move_to_this_queue),\n            )\n        }\n        Row(\n            Modifier\n                .padding(start = 24.dp)\n                .padding(horizontal = 4.dp, vertical = 6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                MyIcon(\n                    MyIcons.folder,\n                    null,\n                    Modifier.size(mySpacings.iconSize)\n                )\n                Spacer(Modifier.width(4.dp))\n                Text(\n                    queueModel.name,\n                    Modifier.weight(1f),\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.lg,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                )\n                val counterColor = animateColorAsState(\n                    if (isActive) {\n                        myColors.success\n                    } else {\n                        LocalContentColor.current / LocalContentAlpha.current\n                    }\n                ).value\n                Text(\n                    text = \"${queueModel.queueItems.size}\",\n                    modifier = Modifier.padding(horizontal = 6.dp),\n                    color = counterColor\n                )\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier.align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun QueueOption(\n    queueOptionMenuState: QueueActions,\n    onDismiss: () -> Unit,\n) {\n    ShowOptionsInPopup(\n        MenuItem.SubMenu(\n            icon = MyIcons.queue,\n            title = queueOptionMenuState.mainQueueModel?.name?.asStringSource() ?: Res.string.queues.asStringSource(),\n            items = queueOptionMenuState.menu,\n        ),\n        onDismiss\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/newQueue/NewQueueDialog.kt",
    "content": "package com.abdownloadmanager.desktop.pages.newQueue\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun NewQueueDialog(\n    appComponent: AppComponent,\n) {\n    if (appComponent.showCreateQueueDialog.collectAsState().value){\n        CustomWindow(\n            state = rememberWindowState(\n                size = DpSize(width = 300.dp, height = 130.dp)\n                    .applyUiScale(LocalUiScale.current),\n                position = WindowPosition.Aligned(Alignment.Center),\n            ),\n            resizable = false,\n            onRequestToggleMaximize = null,\n            onRequestMinimize = null,\n            alwaysOnTop = true,\n            onCloseRequest = {\n                appComponent.closeNewQueueDialog()\n            }\n        ) {\n            NewQueue(\n                onQueueCreate = {\n                    appComponent.closeNewQueueDialog()\n                    appComponent.createNewQueue(it)\n                },\n                onCloseRequest = {\n                    appComponent.closeNewQueueDialog()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/newQueue/NewQueuePage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.newQueue\n\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun NewQueue(\n    onQueueCreate: (String) -> Unit,\n    onCloseRequest: () -> Unit,\n) {\n    WindowTitle(myStringResource(Res.string.add_new_queue))\n    var name by remember {\n        mutableStateOf(\"\")\n    }\n    val focusRequester= remember { FocusRequester() }\n    LaunchedEffect(Unit){\n        focusRequester.requestFocus()\n    }\n    Column(Modifier) {\n        Spacer(Modifier.height(8.dp))\n        MyTextField(\n            text = name,\n            onTextChange = {\n                name = it\n            },\n            modifier = Modifier\n                .focusRequester(focusRequester)\n                .padding(horizontal = 8.dp)\n                .widthIn(max = 400.dp),\n            placeholder = myStringResource(Res.string.queue_name),\n        )\n        Spacer(Modifier.height(8.dp))\n        Spacer(Modifier.weight(1f))\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 8.dp)\n                .padding(bottom = 8.dp),\n            horizontalArrangement = Arrangement.End,\n        ) {\n            ActionButton(\n                text = myStringResource(Res.string.add),\n                onClick = {\n                    onQueueCreate(name)\n                }\n            )\n            Spacer(Modifier.width(4.dp))\n            ActionButton(\n                text = myStringResource(Res.string.cancel),\n                onClick = {\n                    onCloseRequest()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/perhostsettings/DesktopPerHostSettingsComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.perhostsettings\n\nimport com.abdownloadmanager.shared.pages.perhostsettings.BasePerHostSettingsComponent\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.CoroutineScope\n\nclass DesktopPerHostSettingsComponent(\n    ctx: ComponentContext,\n    perHostSettingsManager: PerHostSettingsManager,\n    appRepository: BaseAppRepository,\n    appScope: CoroutineScope,\n    closeRequested: () -> Unit,\n) : BasePerHostSettingsComponent(\n    ctx = ctx,\n    perHostSettingsManager = perHostSettingsManager,\n    appRepository = appRepository,\n    appScope = appScope,\n    closeRequested = closeRequested,\n) {\n    data class Config(\n        override val openedHost: String?\n    ) : BasePerHostSettingsComponent.Config\n\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    sealed interface Effects : BasePerHostSettingsComponent.Effects.Platform {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/perhostsettings/PerHostSettingsPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.perhostsettings\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\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.Color\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.pages.home.sections.SearchBox\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pages.perhostsettings.PerHostSettingsItemWithId\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.*\n\n@Composable\nfun PerHostSettingsPage(component: DesktopPerHostSettingsComponent) {\n    val perHostSettings by component.editedPerHostSettings.collectAsState()\n    val selectedItemId by component.selectedId.collectAsState()\n    WindowTitle(myStringResource(Res.string.settings_per_host_settings))\n    Column {\n        Row(\n            Modifier.weight(1f)\n        ) {\n            HostList(\n                modifier = Modifier\n                    .padding(8.dp)\n                    .width(220.dp)\n                    .fillMaxHeight(),\n                hosts = perHostSettings,\n                selectedId = selectedItemId,\n                setSelected = { id ->\n                    component.onIdSelected(id)\n                },\n                component = component\n            )\n            val configurableList = component.selectedItemConfigurableList.collectAsState().value\n            if (configurableList != null) {\n                RenderPerHostSettingsItem(\n                    modifier = Modifier\n                        .padding(8.dp)\n                        .weight(1f),\n                    itemId = configurableList.id,\n                    configurableList = configurableList.configurableGroups,\n                )\n            } else {\n                Text(\n                    myStringResource(Res.string.settings_per_host_settings_not_selected),\n                    Modifier.fillMaxSize().wrapContentSize()\n                )\n            }\n        }\n        Actions(component)\n    }\n}\n\n@Composable\nprivate fun Actions(\n    component: DesktopPerHostSettingsComponent,\n) {\n    val canSave by component.canSave.collectAsState()\n    val scope = rememberCoroutineScope()\n    Row(\n        Modifier.fillMaxWidth()\n            .wrapContentWidth(Alignment.End)\n            .padding(horizontal = 16.dp)\n            .padding(vertical = 16.dp),\n    ) {\n        val space = @Composable {\n            Spacer(Modifier.width(4.dp))\n        }\n        ActionButton(\n            text = myStringResource(\n                Res.string.update\n            ),\n            modifier = Modifier,\n            enabled = canSave,\n            onClick = {\n                scope.launch {\n                    component.saveAndClose()\n                }\n            }\n        )\n        space()\n        ActionButton(\n            text = myStringResource(Res.string.cancel),\n            modifier = Modifier,\n            onClick = {\n                component.close()\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun RenderPerHostSettingsItem(\n    modifier: Modifier,\n    itemId: String,\n    configurableList: List<ConfigurableGroup>,\n) {\n    val fm = LocalFocusManager.current\n    //remove focus to prevent accidentally change config in different queue\n    LaunchedEffect(itemId) {\n        fm.clearFocus()\n    }\n    Column(modifier) {\n        val pageModifier = Modifier\n            .fillMaxSize()\n        RenderPerHostSettingsConfigurableGroup(pageModifier, configurableList)\n    }\n}\n\n@Composable\nprivate fun RenderPerHostSettingsConfigurableGroup(\n    modifier: Modifier,\n    configurableGroups: List<ConfigurableGroup>,\n) {\n    Column(\n        modifier\n            .verticalScroll(rememberScrollState())\n    ) {\n        for ((index, cfgGroup) in configurableGroups.withIndex()) {\n            RenderConfigurableGroup(\n                cfgGroup,\n                Modifier\n            )\n            if (index != configurableGroups.lastIndex) {\n                Spacer(Modifier.height(8.dp))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HostList(\n    modifier: Modifier,\n    hosts: List<PerHostSettingsItemWithId>,\n    selectedId: String?,\n    setSelected: (String) -> Unit,\n    component: DesktopPerHostSettingsComponent,\n) {\n    val shape = myShapes.defaultRounded\n    val borderColor = myColors.surface / 0.5f\n    var search by remember { mutableStateOf(\"\") }\n    val defaultEmptyName = myStringResource(Res.string.settings_per_host_settings_new_host)\n    val filteredHosts = remember(hosts, search) {\n        hosts.ifThen(search.isNotEmpty()) {\n            filter {\n                it.perHostSettingsItem.host.contains(search, true)\n            }\n        }\n    }\n    Column(\n        modifier\n            .border(1.dp, borderColor, shape)\n            .clip(shape)\n    ) {\n        Box(\n            Modifier\n                .weight(1f)\n                .fillMaxWidth()\n        ) {\n            LazyColumn {\n                items(filteredHosts, key = { it.id }) { s ->\n                    val isSelected = selectedId == s.id\n                    SideBarItem(\n                        isSelected = isSelected,\n                        onClick = { setSelected(s.id) },\n                        name = s.perHostSettingsItem.host.takeIf { it.isNotBlank() } ?: defaultEmptyName,\n                        modifier = Modifier.animateItem(),\n                    )\n                }\n            }\n            if (filteredHosts.isEmpty()) {\n                WithContentAlpha(0.75f) {\n                    Text(\n                        myStringResource(Res.string.list_is_empty),\n                        modifier = Modifier.align(Alignment.Center)\n                    )\n                }\n            }\n        }\n\n        val spacer = @Composable { Spacer(Modifier.width(4.dp)) }\n        Spacer(\n            Modifier\n                .background(borderColor)\n                .fillMaxWidth()\n                .height(1.dp)\n        )\n        Row(\n            modifier = Modifier\n                .padding(vertical = 4.dp)\n                .padding(horizontal = 8.dp)\n                .height(IntrinsicSize.Max)\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.End\n        ) {\n            SearchBox(\n                search,\n                onTextChange = {\n                    search = it\n                },\n                placeholder = myStringResource(Res.string.search),\n                modifier = Modifier.weight(1f).fillMaxHeight(),\n            )\n            spacer()\n            IconActionButton(\n                icon = MyIcons.add,\n                contentDescription = Res.string.add.asStringSource(),\n                onClick = {\n                    component.onRequestAddNewHostSettingsItem()\n                }\n            )\n            spacer()\n            IconActionButton(\n                icon = MyIcons.remove,\n                contentDescription = Res.string.remove.asStringSource(),\n                enabled = selectedId != null,\n                onClick = {\n                    selectedId?.let {\n                        component.onRequestDeleteConfig(it)\n                    }\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SideBarItem(\n    name: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Box(\n        modifier\n            .height(IntrinsicSize.Max)\n            .ifThen(isSelected) {\n                background(myColors.onBackground / 0.05f)\n            }\n            .selectable(\n                selected = isSelected,\n                onClick = onClick\n            )\n    ) {\n        Row(\n            Modifier\n                .padding(vertical = 8.dp)\n                .padding(start = 16.dp)\n                .padding(end = 2.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                Text(\n                    name,\n                    Modifier.weight(1f),\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.lg,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                )\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier\n                .align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n        if (isSelected) {\n            listOf(\n                Alignment.TopCenter,\n                Alignment.BottomCenter,\n            ).forEach {\n                Spacer(\n                    Modifier\n                        .align(it)\n                        .fillMaxWidth()\n                        .height(1.dp)\n                        .background(\n                            Brush.horizontalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    myColors.onBackground / 0.1f,\n                                    myColors.onBackground / 0.1f,\n                                    Color.Transparent,\n                                )\n                            )\n                        )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/perhostsettings/PerHostSettingsWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.perhostsettings\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.pages.perhostsettings.BasePerHostSettingsComponent\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.rememberChild\n\n@Composable\nfun PerHostSettingsWindow(\n    appComponent: AppComponent\n) {\n    val component = appComponent.perHostSettingsSlot.rememberChild()\n    if (component != null) {\n        val windowState = rememberWindowState(\n            size = DpSize(\n                600.dp,\n                400.dp,\n            ),\n            position = WindowPosition.Aligned(Alignment.Center)\n        )\n        CustomWindow(\n            state = windowState,\n            onCloseRequest = appComponent::closePerHostSettings,\n        ) {\n            HandleEffects(component) {\n                when (it) {\n                    is BasePerHostSettingsComponent.Effects.Platform -> {\n                        when (it as DesktopPerHostSettingsComponent.Effects) {\n                            DesktopPerHostSettingsComponent.Effects.BringToFront -> {\n                                windowState.isMinimized = false\n                                window.toFront()\n                            }\n                        }\n                    }\n                }\n            }\n            PerHostSettingsPage(component)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/poweractionalert/PowerActionAlertWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.poweractionalert\n\nimport androidx.compose.animation.core.animateFloatAsState\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.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\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.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.LoadingIndicatorWithBrush\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.rememberChild\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun PowerActionAlert(appComponent: AppComponent) {\n    appComponent.openedPowerAction.rememberChild()?.let {\n        PowerActionAlertWindow(it)\n    }\n}\n\n@Composable\nprivate fun PowerActionAlertWindow(\n    component: PowerActionComponent\n) {\n    val uiScale = LocalUiScale.current\n    val windowState = rememberWindowState(\n        position = WindowPosition.Aligned(Alignment.Center),\n        size = DpSize(\n            width = 450.dp,\n            height = 200.dp,\n        ).applyUiScale(uiScale),\n    )\n    CustomWindow(\n        onCloseRequest = component::performCancel,\n        state = windowState,\n        alwaysOnTop = true,\n        resizable = false,\n        onRequestMinimize = null,\n        onRequestToggleMaximize = null,\n    ) {\n        PowerActionAlertPage(component)\n    }\n}\n\n@Composable\nprivate fun PowerActionAlertPage(component: PowerActionComponent) {\n    val totalTime = component.totalDelay\n    val remainingTime by component.remainingDelay.collectAsState()\n    val powerActionError = component.powerActionError.collectAsState().value\n    val cancel = component::performCancel\n    val performPowerActionNow = component::performPowerAction\n    val remainingSeconds = remainingTime / 1000\n    WindowTitle(myStringResource(Res.string.shutdown_alert))\n    Column {\n        Row(\n            Modifier\n                .weight(1f)\n                .padding(\n                    horizontal = 16.dp,\n                    vertical = 8.dp\n                )\n                .wrapContentHeight(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Box {\n                val progress = animateFloatAsState(\n                    (remainingTime.toFloat() / totalTime)\n                ).value\n                val strokeWidth = 4.dp\n                Row(\n                    Modifier\n                        .align(Alignment.Center)\n                        .padding(strokeWidth * 4),\n                    verticalAlignment = Alignment.Bottom,\n                ) {\n                    if (powerActionError == null) {\n                        Text(\n                            text = remainingSeconds.toString().padStart(2, '0'),\n                            fontSize = myTextSizes.x3l,\n                            modifier = Modifier,\n                        )\n                        Text(\n                            \"s\",\n                            fontSize = myTextSizes.xl,\n                        )\n                    } else {\n                        Text(\n                            text = myStringResource(Res.string.error),\n                            fontSize = myTextSizes.x3l,\n                            modifier = Modifier,\n                        )\n                    }\n                }\n                LoadingIndicatorWithBrush(\n                    Modifier.matchParentSize()\n                        .aspectRatio(1f),\n                    brush = if (powerActionError == null) {\n                        myColors.primaryGradient\n                    } else {\n                        myColors.errorGradient\n                    },\n                    progress = if (powerActionError == null) {\n                        progress\n                    } else {\n                        1f\n                    },\n                )\n            }\n            Spacer(Modifier.width(16.dp))\n            Column {\n                Text(\n                    if (powerActionError == null) {\n                        myStringResource(Res.string.system_shutdown_soon)\n                    } else {\n                        myStringResource(Res.string.system_shutdown_failed)\n                    },\n                    fontSize = myTextSizes.x3l,\n                    fontWeight = FontWeight.Bold,\n                    color = if (powerActionError == null) {\n                        myColors.warning\n                    } else {\n                        myColors.error\n                    },\n                )\n                Column(\n                    Modifier.padding(end = 8.dp)\n                ) {\n                    Spacer(Modifier.height(4.dp))\n                    val description = if (powerActionError == null) {\n                        myStringResource(Res.string.system_shutdown_soon_description)\n                    } else {\n                        (powerActionError\n                            .localizedMessage\n                            .takeIf { it.isNotBlank() }\n                            ?.asStringSource()\n                            ?: Res.string.unknown_error.asStringSource()\n                                )\n                            .rememberString()\n                    }\n                    Text(\n                        description\n                    )\n                    component\n                        .powerActionReason?.let {\n                            Spacer(Modifier.height(4.dp))\n                            Text(\n                                it.message.rememberString(),\n                                color = when (it.type) {\n                                    PowerActionComponent.PowerActionReason.Type.Success -> myColors.success\n                                    PowerActionComponent.PowerActionReason.Type.Warning -> myColors.warning\n                                    PowerActionComponent.PowerActionReason.Type.Error -> myColors.error\n                                }\n                            )\n                        }\n                }\n            }\n        }\n        Actions(\n            modifier = Modifier,\n            isShuttingDown = component.isShuttingDown.collectAsState().value,\n            cancel = cancel,\n            performPowerActionNow = performPowerActionNow,\n        )\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier,\n    isShuttingDown: Boolean,\n    cancel: () -> Unit,\n    performPowerActionNow: () -> Unit,\n) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.End,\n        ) {\n            ActionButton(\n                Res.string.shutdown_now.asStringSource().rememberString(),\n                enabled = !isShuttingDown,\n                modifier = Modifier,\n                onClick = performPowerActionNow,\n            )\n            Spacer(Modifier.width(8.dp))\n            ActionButton(\n                myStringResource(Res.string.cancel),\n                modifier = Modifier,\n                onClick = cancel,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/poweractionalert/PowerActionComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.poweractionalert\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nclass PowerActionComponent(\n    val ctx: ComponentContext,\n    val powerActionConfig: PowerActionConfig,\n    val powerActionReason: PowerActionReason?,\n    private val powerActionDelay: Long,\n    private val close: () -> Unit,\n    private val onBeforePowerAction: suspend () -> Unit,\n) : BaseComponent(\n    ctx,\n), KoinComponent {\n    val applicationScope by inject<CoroutineScope>()\n    val totalDelay = this@PowerActionComponent.powerActionDelay\n    private val _remainingDelay = MutableStateFlow(totalDelay)\n    val remainingDelay = _remainingDelay.asStateFlow()\n    private val _isPerformingPowerAction = MutableStateFlow(false)\n    val isShuttingDown = _isPerformingPowerAction.asStateFlow()\n\n    private val _powerActionError = MutableStateFlow(null as Throwable?)\n    val powerActionError = _powerActionError.asStateFlow()\n\n    init {\n        start()\n    }\n\n    fun start() {\n        scope.launch {\n            var remaining = this@PowerActionComponent.powerActionDelay\n            val eachStep = 1000 / 33L\n            while (remaining >= 0) {\n                delay(eachStep)\n                remaining = (remaining - eachStep)\n                _remainingDelay.value = remaining.coerceAtLeast(0)\n            }\n            performPowerAction()\n        }\n    }\n\n    fun performCancel() {\n        close()\n    }\n\n    fun performPowerAction() {\n        applicationScope.launch {\n            _isPerformingPowerAction.value = true\n            val success = try {\n                doPowerAction()\n            } catch (e: Exception) {\n                _powerActionError.value = e\n                e.printStackTrace()\n                _isPerformingPowerAction.value = false\n                false\n            }\n            if (success) {\n                withContext(Dispatchers.Main) {\n                    close()\n                }\n            }\n        }\n    }\n\n    private suspend fun doPowerAction(): Boolean {\n        onBeforePowerAction()\n        delay(1000)\n        return DesktopUtils.powerAction().initiate(powerActionConfig)\n    }\n\n    data class Config(\n        val powerActionConfig: PowerActionConfig,\n        val powerActionDelay: Long = 30_000,\n        val powerActionReason: PowerActionReason? = null,\n    )\n\n    enum class PowerActionReason(\n        val message: StringSource,\n        val type: Type,\n    ) {\n        QueueWorkFinished(Res.string.system_shutdown_reason_queue_completed.asStringSource(), Type.Success),\n        QueueEndTimeReached(Res.string.system_shutdown_reason_queue_end_time_reached.asStringSource(), Type.Success),\n        DownloadFinished(Res.string.system_shutdown_download_finished.asStringSource(), Type.Success),\n        Unknown(Res.string.unknown.asStringSource(), Type.Error);\n\n        enum class Type {\n            Success, Warning, Error,\n        }\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/queue/QueueInfoComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.queue\n\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.flow.mapStateFlow\nimport com.abdownloadmanager.shared.util.newScopeBasedOn\nimport androidx.compose.runtime.toMutableStateList\nimport com.abdownloadmanager.desktop.storage.DesktopExtraQueueSettings\nimport com.abdownloadmanager.shared.storage.ExtraQueueSettingsStorage\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.DayOfWeekConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.TimeConfigurable\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.createMutableStateFlowFromFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.collections.map\n\nclass QueueInfoComponent(\n    ctx: ComponentContext,\n    id: Long,\n) : BaseComponent(ctx),\n    KoinComponent {\n    private val downloadMonitor: IDownloadMonitor by inject()\n    private val queueManager: QueueManager by inject()\n    val downloadQueue = queueManager.queues.value.find {\n        it.id == id\n    }!!\n\n\n    val selectedListItems = MutableStateFlow(emptyList<Long>())\n    private val lastSelectedItem = MutableStateFlow(null as Long?)\n\n    val extraQueueSettingsStorage by inject<ExtraQueueSettingsStorage<DesktopExtraQueueSettings>>()\n    val extraDownloadItemSettingsFlow = createMutableStateFlowFromFlow(\n        flow = extraQueueSettingsStorage.getExternalQueueSettingsAsFlow(\n            id = id,\n            initialEmit = false,\n        ),\n        initialValue = extraQueueSettingsStorage.getExtraQueueSettings(id),\n        updater = {\n            scope.launch {\n                extraQueueSettingsStorage.setExtraQueueSettings(it)\n            }\n        },\n        scope = scope,\n    )\n\n    init {\n        downloadQueue.queueModel.map {\n            it.queueItems\n        }.onEach { l ->\n            selectedListItems.value = selectedListItems.value.filter {\n                it in l\n            }\n        }.launchIn(scope)\n    }\n\n    fun selectAll() {\n        val all = downloadQueueItems.value.map {\n            it.id\n        }\n        selectedListItems.value = all\n        lastSelectedItem.value = all.last()\n    }\n\n    fun clearSelection() {\n        selectedListItems.value = emptyList()\n        lastSelectedItem.value = null\n    }\n\n    fun setSelectedItem(\n        id: Long,\n        selected: Boolean,\n        ctrlPressed: Boolean,\n        shiftPressed: Boolean,\n    ) {\n        val selectedIds = selectedListItems.value\n        val availableItems = downloadQueueItems.value\n\n        selectedListItems.value = selectedIds.let { selectedIds ->\n            if (ctrlPressed) {\n                lastSelectedItem.value = id\n                selectedIds.toMutableStateList().also { mutableList ->\n                    val contains = mutableList.contains(id)\n                    if (contains && !selected) {\n                        mutableList.remove(id)\n                    } else if (!contains && selected) {\n                        mutableList.add(id)\n                    }\n                }.toList()\n            } else if (shiftPressed) {\n                val lastSelected = lastSelectedItem.value\n                val fromIndex = lastSelected?.let { lastSelectedId ->\n                    availableItems.indexOfFirst { itemState ->\n                        itemState.id == lastSelectedId\n                    }.takeIf { it != -1 }\n                }\n                val toIndex = availableItems.indexOfFirst { itemState ->\n                    itemState.id == id\n                }.takeIf { it != -1 }\n                if (fromIndex != null && toIndex != null) {\n                    availableItems.map { it.id }.subList(\n                        minOf(fromIndex, toIndex),\n                        maxOf(fromIndex, toIndex) + 1,\n                    )\n                } else {\n                    lastSelectedItem.value = id\n                    listOf(id)\n                }\n            } else {\n                if (selected) {\n                    lastSelectedItem.value = id\n                    listOf(id)\n                } else {\n                    lastSelectedItem.value = null\n                    emptyList()\n                }\n            }\n        }\n    }\n\n\n    val configurations: List<ConfigurableGroup> =\n        createConfigurableList(downloadQueue, scope)\n\n\n    private fun createConfigurableList(\n        downloadQueue: DownloadQueue, parentScope: CoroutineScope,\n    ): List<ConfigurableGroup> {\n        val scope = newScopeBasedOn(parentScope)\n        val enabledStartTimeFlow = downloadQueue.queueModel.mapStateFlow() {\n            it.scheduledTimes.enabledStartTime\n        }\n        val enabledEndTimeFlow = downloadQueue.queueModel.mapStateFlow() {\n            it.scheduledTimes.enabledEndTime\n        }\n        val enabledSchedulerFlow = combineStateFlows(enabledStartTimeFlow, enabledEndTimeFlow) { start, end ->\n            start || end\n        }\n        return listOf(\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.general.asStringSource()),\n                nestedConfigurable = listOf(\n                    StringConfigurable(\n                        Res.string.name.asStringSource(),\n                        Res.string.queue_name_help.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.name\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setName(newValue)\n                            },\n                        ),\n                        validate = {\n                            it.length in 1..32\n                        },\n                        describe = {\n                            Res.string.queue_name_describe\n                                .asStringSourceWithARgs(\n                                    Res.string.queue_name_describe_createArgs(\n                                        value = it\n                                    )\n                                )\n                        },\n                    ),\n                    IntConfigurable(\n                        Res.string.queue_max_concurrent_download.asStringSource(),\n                        Res.string.queue_max_concurrent_download_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.maxConcurrent\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setMaxConcurrent(newValue)\n                            },\n                        ),\n                        describe = { \"$it\".asStringSource() },\n                        range = 1..32,\n                        renderMode = IntConfigurable.RenderMode.TextField,\n                    ),\n                ),\n            ),\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.on_completion.asStringSource()),\n                nestedConfigurable = listOf(\n                    BooleanConfigurable(\n                        Res.string.queue_automatic_stop.asStringSource(),\n                        Res.string.queue_automatic_stop_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.stopQueueOnEmpty\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setStopQueueOnEmpty(newValue)\n                            },\n                        ),\n                        describe = {\n                            if (it) Res.string.enabled.asStringSource()\n                            else Res.string.disabled.asStringSource()\n                        },\n                    ),\n                    BooleanConfigurable(\n                        title = Res.string.queue_shutdown_on_completion.asStringSource(),\n                        description = Res.string.queue_shutdown_on_completion_description.asStringSource(),\n                        backedBy = extraDownloadItemSettingsFlow.mapTwoWayStateFlow(\n                            map = {\n                                it.powerActionTypeOnFinish != null\n                            },\n                            unMap = {\n                                copy(\n                                    powerActionTypeOnFinish = when (it) {\n                                        true -> PowerActionConfig.Type.Shutdown\n                                        false -> null\n                                    },\n                                )\n                            },\n                        ),\n                        describe = {\n                            if (it) Res.string.enabled.asStringSource()\n                            else Res.string.disabled.asStringSource()\n                        },\n                    )\n                ),\n            ),\n            ConfigurableGroup(\n                groupTitle = MutableStateFlow(Res.string.queue_scheduler.asStringSource()),\n                nestedVisible = enabledSchedulerFlow,\n                mainConfigurable = BooleanConfigurable(\n                    Res.string.queue_enable_scheduler.asStringSource(),\n                    description = \"\".asStringSource(),\n                    describe = { \"\".asStringSource() },\n                    backedBy = createMutableStateFlowFromStateFlow(\n                        flow = enabledSchedulerFlow,\n                        scope = scope,\n                        updater = { newValue ->\n                            downloadQueue.setScheduledTimes {\n                                copy(\n                                    enabledStartTime = newValue,\n                                    enabledEndTime = newValue,\n                                )\n                            }\n                        }\n                    ),\n                ),\n                nestedConfigurable = listOf(\n                    DayOfWeekConfigurable(\n                        Res.string.queue_active_days.asStringSource(),\n                        Res.string.queue_active_days_description.asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.daysOfWeek\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(daysOfWeek = newValue)\n                                }\n                            },\n                        ),\n                        validate = {\n                            it.isNotEmpty()\n                        },\n                        describe = { \"\".asStringSource() },\n                    ),\n                    BooleanConfigurable(\n                        Res.string.queue_scheduler_enable_auto_start_time.asStringSource(),\n                        description = \"\".asStringSource(),\n                        describe = { \"\".asStringSource() },\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = enabledStartTimeFlow,\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(enabledStartTime = newValue)\n                                }\n                            },\n                        ),\n                    ),\n                    TimeConfigurable(\n                        Res.string.queue_scheduler_auto_start_time.asStringSource(),\n                        \"\".asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.startTime\n                            },\n                            updater = {\n                                downloadQueue.setScheduledTimes {\n                                    copy(startTime = it)\n                                }\n                            },\n                        ),\n                        describe = { \"\".asStringSource() },\n                        visible = enabledStartTimeFlow,\n                    ),\n                    BooleanConfigurable(\n                        Res.string.queue_scheduler_enable_auto_stop_time.asStringSource(),\n                        description = \"\".asStringSource(),\n                        describe = { \"\".asStringSource() },\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = enabledEndTimeFlow,\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(enabledEndTime = newValue)\n                                }\n                            },\n                        ),\n                    ),\n                    TimeConfigurable(\n                        Res.string.queue_scheduler_auto_stop_time.asStringSource(),\n                        \"\".asStringSource(),\n                        backedBy = createMutableStateFlowFromStateFlow(\n                            scope = scope,\n                            flow = downloadQueue.queueModel.mapStateFlow() {\n                                it.scheduledTimes.endTime\n                            },\n                            updater = { newValue ->\n                                downloadQueue.setScheduledTimes {\n                                    copy(endTime = newValue)\n                                }\n                            },\n                        ),\n                        describe = { \"\".asStringSource() },\n                        visible = enabledEndTimeFlow,\n                    ),\n                )\n            ),\n        )\n    }\n\n\n    private val dls = downloadMonitor.downloadListFlow\n        .stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    val downloadQueueItems = merge(\n        downloadQueue.queueModel\n            .map { it.queueItems }\n            .distinctUntilChanged(),\n        dls,\n    ).map {\n        getQueueItemsAsDownloadItem()\n    }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    private fun getQueueItemsAsDownloadItem(\n    ): List<IDownloadItemState> {\n        return downloadQueue.queueModel.value.queueItems.mapNotNull { dlId ->\n            dls.value.find {\n                it.id == dlId\n            }\n        }\n    }\n\n    fun deleteItems() {\n        downloadQueue.removeFromQueue(selectedListItems.value)\n    }\n\n    fun moveDownItems() {\n        downloadQueue.moveDown(selectedListItems.value)\n    }\n\n    fun moveUpItems() {\n        downloadQueue.moveUp(selectedListItems.value)\n    }\n\n    fun swapItem(fromIndex: Int, toIndex: Int) {\n        //maybe removed by queue itself during download completion\n        val currentDraggingItem = runCatching {\n            downloadQueue.getQueueItemFromOrder(fromIndex)\n        }.getOrNull()\n        val listOfIds = selectedListItems.value\n            .let {\n                if (currentDraggingItem != null && !it.contains(currentDraggingItem)) {\n                    it.plus(currentDraggingItem)\n                } else {\n                    it\n                }\n            }\n\n\n        downloadQueue.move(\n            listOfIds, toIndex - fromIndex\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/queue/QueueWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.queue\n\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.window.rememberWindowState\n\n@Composable\nfun QueuesWindow(queuesComponent: QueuesComponent) {\n    val state = rememberWindowState()\n    CustomWindow(\n        state = state,\n        onCloseRequest = queuesComponent.close\n    ) {\n        HandleEffects(queuesComponent) {\n            if (it == QueuesComponentEffects.ToFront) {\n                state.isMinimized = false\n                window.toFront()\n            }\n        }\n        QueuePage(queuesComponent)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/queue/QueuesComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.queue\n\nimport com.abdownloadmanager.desktop.actions.newQueueAction\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport androidx.compose.runtime.*\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.asState\nimport com.abdownloadmanager.shared.util.subscribeAsStateFlow\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.decompose.router.slot.SlotNavigation\nimport com.arkivanov.decompose.router.slot.childSlot\nimport com.arkivanov.decompose.router.slot.navigate\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nsealed interface QueuesComponentEffects{\n    data object ToFront:QueuesComponentEffects\n}\nclass QueuesComponent(\n    ctx: ComponentContext,\n    val close: () -> Unit,\n) : BaseComponent(ctx),\n    ContainsEffects<QueuesComponentEffects> by supportEffects(),\n    KoinComponent {\n    val downloadSystem: DownloadSystem by inject()\n    val queueManager: QueueManager by inject()\n    private val queues = queueManager.queues\n\n\n    val queuesState by queues.asState(scope)\n    var selectedItemIndex by mutableStateOf(0)\n    fun getNearest(lastIndex: Int): Int {\n        return lastIndex.coerceIn(queuesState.indices)\n    }\n\n    val selectedItem by derivedStateOf {\n        queuesState.get(getNearest(selectedItemIndex))\n    }\n\n    fun onQueueSelected(queueId: Long) {\n        val foundIndex = queuesState.indexOfFirst {\n            it.id == queueId\n        }\n        if (foundIndex == -1) {\n            return\n        }\n        selectedItemIndex = foundIndex\n    }\n\n    fun addQueue() {\n        newQueueAction()\n//        scope.launch {\n//            queueManager.addQueue(\"New Queue\")\n//        }\n    }\n\n    fun canDeleteThisQueue(queueId: Long): Boolean {\n        return queueManager.canDelete(queueId)\n    }\n\n    fun requestDeleteQueue(id: Long) {\n        scope.launch {\n            downloadSystem.deleteQueue(id)\n        }\n    }\n\n    fun bringToFront() {\n        sendEffect(QueuesComponentEffects.ToFront)\n    }\n\n    init {\n        queues.map {\n            it.size\n        }\n            .distinctUntilChanged()\n            .onEach {\n                selectedItemIndex = getNearest(selectedItemIndex)\n            }.launchIn(scope)\n    }\n\n    data class QueueInfoNavigationConfig(\n        val queueId:Long,\n    )\n    val queueInfoNavigation = SlotNavigation<QueueInfoNavigationConfig>()\n    val queueInfoComponent = childSlot(\n        queueInfoNavigation,\n        serializer = null,\n        initialConfiguration = {\n            QueueInfoNavigationConfig(selectedItem.id)\n        },\n        childFactory = {config,ctx->\n            QueueInfoComponent(ctx,config.queueId)\n        }\n    ).subscribeAsStateFlow()\n\n    init {\n        snapshotFlow {\n            selectedItem\n        }.onEach {q->\n            queueInfoNavigation.navigate {\n                if (it?.queueId==q.id){\n                    it\n                }else{\n                    QueueInfoNavigationConfig(\n                        q.id\n                    )\n                }\n            }\n        }.launchIn(scope)\n\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/queue/QueuesPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.queue\n\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\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.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyItemScope\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\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.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.isCtrlPressed\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.isCtrlPressed\nimport ir.amirab.util.desktop.isShiftPressed\nimport kotlinx.coroutines.*\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.ReorderableLazyListState\nimport sh.calvin.reorderable.rememberReorderableLazyListState\n\n\n@Composable\nfun QueuePage(component: QueuesComponent) {\n    val queues = component.queuesState\n    val activeItem: DownloadQueue = component.selectedItem\n    WindowTitle(myStringResource(Res.string.queues))\n    val borderShape = myShapes.defaultRounded\n    val borderColor = myColors.surface\n    Column {\n        Row(\n            Modifier.weight(1f)\n        ) {\n            QueueListSection(\n                modifier = Modifier\n                    .padding(horizontal = 8.dp)\n                    .width(200.dp)\n                    .padding(2.dp)\n                    .border(1.dp, borderColor, borderShape)\n                    .clip(borderShape)\n                    .padding(1.dp)\n                    .fillMaxHeight(),\n                queues = queues,\n                selectedItem = component.selectedItem.id,\n                setSelected = { id ->\n                    component.onQueueSelected(id)\n                },\n                component = component\n            )\n            QueueInfo(\n                modifier = Modifier\n                    .weight(1f)\n                    .padding(2.dp)\n                    .border(1.dp, borderColor, borderShape)\n                    .padding(1.dp)\n                    .clip(borderShape),\n                item = activeItem,\n                component = component.queueInfoComponent.collectAsState().value.child!!.instance,\n                borderColor = borderColor,\n            )\n        }\n        Actions(component, activeItem)\n    }\n}\n\n@Composable\nprivate fun Actions(\n    component: QueuesComponent,\n    selectedItem: DownloadQueue,\n) {\n    val isActive by selectedItem.activeFlow.collectAsState()\n    val scope = rememberCoroutineScope()\n    Row(\n        Modifier.fillMaxWidth()\n            .wrapContentWidth(Alignment.End)\n            .padding(horizontal = 16.dp)\n            .padding(vertical = 16.dp),\n    ) {\n        val space = @Composable {\n            Spacer(Modifier.width(4.dp))\n        }\n        ActionButton(\n            text = myStringResource(\n                if (isActive) {\n                    Res.string.stop_queue\n                } else {\n                    Res.string.start_queue\n                }\n            ),\n            modifier = Modifier,\n            onClick = {\n                scope.launch {\n                    if (isActive) {\n                        selectedItem.stop()\n                    } else {\n                        selectedItem.start()\n                    }\n                }\n            }\n        )\n        space()\n        ActionButton(\n            text = myStringResource(Res.string.close),\n            modifier = Modifier,\n            onClick = {\n                component.close()\n            }\n        )\n    }\n}\n\nenum class QueueInfoPages(val title: StringSource, val icon: IconSource) {\n    Config(Res.string.config.asStringSource(), MyIcons.settings),\n    Items(Res.string.items.asStringSource(), MyIcons.queue),\n}\n\n@Composable\nprivate fun QueueInfo(\n    modifier: Modifier,\n    item: DownloadQueue,\n    component: QueueInfoComponent,\n    borderColor: Color,\n) {\n    val fm = LocalFocusManager.current\n    //remove focus to prevent accidentally change config in different queue\n    LaunchedEffect(item) {\n        fm.clearFocus()\n    }\n    var currentPage by remember {\n        mutableStateOf(QueueInfoPages.Config)\n    }\n    Column(modifier) {\n        Column(\n            Modifier\n        ) {\n            MyTabRow {\n                QueueInfoPages.entries.forEach {\n                    MyTab(\n                        selected = it == currentPage,\n                        onClick = { currentPage = it },\n                        icon = it.icon,\n                        title = it.title,\n                    )\n                }\n            }\n            Spacer(Modifier.fillMaxWidth().height(1.dp).background(borderColor))\n            val pageModifier = Modifier\n                .fillMaxSize()\n                .padding(4.dp)\n            when (currentPage) {\n                QueueInfoPages.Config -> RenderQueueConfig(pageModifier, component)\n                QueueInfoPages.Items -> RenderQueueItems(pageModifier, component)\n            }\n        }\n    }\n}\n\n@Composable\nfun RenderQueueItems(\n    modifier: Modifier,\n    component: QueueInfoComponent,\n) {\n    val windowInfo = LocalWindowInfo.current\n    val downloadItems by component.downloadQueueItems.collectAsState()\n    val selectedIds by component.selectedListItems.collectAsState()\n    val lazyListState = rememberLazyListState()\n    val state = rememberReorderableLazyListState(\n        lazyListState,\n        onMove = { from, to ->\n            component.swapItem(from.index, to.index)\n        }\n    )\n    val listInteractionSource = remember { MutableInteractionSource() }\n    Column(modifier) {\n        LazyColumn(\n            state = lazyListState,\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n            modifier = Modifier\n                .weight(1f)\n                .clickable(\n                    indication = null,\n                    interactionSource = listInteractionSource\n                ) {\n                    component.clearSelection()\n                }\n                .onKeyEvent {\n                    if (it.type != KeyEventType.KeyDown) {\n                        return@onKeyEvent false\n                    }\n                    when (it.key) {\n                        Key.A if it.isCtrlPressed -> {\n                            component.selectAll()\n                            true\n                        }\n\n                        Key.Escape -> {\n                            component.clearSelection()\n                            true\n                        }\n\n                        Key.Delete -> {\n                            component.deleteItems()\n                            true\n                        }\n\n                        Key.DirectionUp -> {\n                            component.moveUpItems()\n                            true\n                        }\n\n                        Key.DirectionDown -> {\n                            component.moveDownItems()\n                            true\n                        }\n\n                        else -> {\n                            false\n                        }\n                    }\n                }\n        ) {\n            itemsIndexed(downloadItems,\n                key = { _, item -> item.id }\n            ) { index, downloadItem ->\n                RenderQueueItem(\n                    state = state,\n                    value = downloadItem,\n                    isSelected = selectedIds.contains(downloadItem.id),\n                    setSelected = { selected ->\n                        component.setSelectedItem(\n                            id = downloadItem.id,\n                            selected = selected,\n                            ctrlPressed = isCtrlPressed(windowInfo),\n                            shiftPressed = isShiftPressed(windowInfo),\n                        )\n                    },\n                    index = index\n                )\n            }\n        }\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 5)\n        )\n        Row(Modifier.padding(8.dp)) {\n            val hasSelections = selectedIds.isNotEmpty()\n            val space = 4.dp\n            IconActionButton(\n                icon = MyIcons.remove,\n                contentDescription = Res.string.remove.asStringSource(),\n                onClick = {\n                    component.deleteItems()\n                },\n                enabled = hasSelections,\n            )\n            Spacer(Modifier.weight(1f))\n            IconActionButton(\n                icon = MyIcons.down,\n                contentDescription = Res.string.move_down.asStringSource(),\n                onClick = {\n                    component.moveDownItems()\n                },\n                enabled = hasSelections,\n            )\n            Spacer(Modifier.width(space))\n            IconActionButton(\n                icon = MyIcons.up,\n                contentDescription = Res.string.move_up.asStringSource(),\n                onClick = {\n                    component.moveUpItems()\n                },\n                enabled = hasSelections,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun LazyItemScope.RenderQueueItem(\n    state: ReorderableLazyListState,\n    value: IDownloadItemState,\n    isSelected: Boolean,\n    setSelected: (Boolean) -> Unit,\n    index: Int\n) {\n    ReorderableItem(\n        state, key = value.id\n    ) { dragging ->\n        Box(\n            modifier = Modifier\n                .draggableHandle()\n        ) {\n            NavigateableItem(\n                isSelected = isSelected,\n                onClick = {\n                    setSelected(!isSelected)\n                },\n                content = {\n                    val isActive = if (value.statusOrFinished() is DownloadJobStatus.IsActive) {\n                        true\n                    } else {\n                        false\n                    }\n                    Row {\n                        Text(\n                            \"${index + 1}. \",\n                            fontSize = myTextSizes.base,\n                            maxLines = 1,\n                            fontWeight = FontWeight.Bold,\n                            color = (if (isActive) {\n                                myColors.success\n                            } else {\n                                LocalContentColor.current\n                            }) / LocalContentAlpha.current,\n                            modifier = Modifier\n                                .border(1.dp, myColors.onBackground / 5)\n                                .padding(1.dp)\n                        )\n                        Text(\n                            value.name,\n                            fontSize = myTextSizes.base,\n                            maxLines = 1,\n                        )\n                    }\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderQueueConfig(\n    modifier: Modifier,\n    component: QueueInfoComponent,\n) {\n    val configurables: List<ConfigurableGroup> = component.configurations\n    Column(\n        modifier\n            .verticalScroll(rememberScrollState())\n    ) {\n        for ((index, cfgGroup) in configurables.withIndex()) {\n            RenderConfigurableGroup(\n                cfgGroup,\n                Modifier\n            )\n            if (index != configurables.lastIndex) {\n                Spacer(Modifier.height(4.dp))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun QueueListSection(\n    modifier: Modifier,\n    queues: List<DownloadQueue>,\n    selectedItem: Long,\n    setSelected: (Long) -> Unit,\n    component: QueuesComponent,\n) {\n    Column(modifier) {\n        Column(\n            Modifier\n                .padding(top = 12.dp)\n                .padding(horizontal = 8.dp)\n                .verticalScroll(rememberScrollState())\n                .weight(1f)\n        ) {\n            for (s in queues) {\n                val queueModel by s.queueModel.collectAsState()\n                val isQueueActive by s.activeFlow.collectAsState()\n                val isSelected = selectedItem == s.id\n                NavigateableItem(\n                    isSelected = isSelected,\n                    onClick = { setSelected(s.id) }\n                ) {\n                    MyIcon(\n                        MyIcons.folder,\n                        null,\n                        Modifier.size(16.dp)\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Text(\n                        queueModel.name,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                        modifier = Modifier.weight(1f)\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Spacer(Modifier\n                        .size(8.dp)\n                        .clip(CircleShape)\n                        .background(\n                            if (isQueueActive) {\n                                myColors.success\n                            } else {\n                                myColors.onSurface / 50\n                            }\n                        )\n                    )\n                }\n            }\n        }\n        val spacer = @Composable { Spacer(Modifier.width(4.dp)) }\n        Spacer(\n            Modifier\n                .background(myColors.onBackground / 5)\n                .fillMaxWidth()\n                .height(1.dp)\n        )\n        Row(\n            modifier = Modifier\n                .padding(vertical = 4.dp)\n                .padding(horizontal = 8.dp)\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.End\n        ) {\n            IconActionButton(\n                icon = MyIcons.add,\n                contentDescription = Res.string.add_new_queue.asStringSource(),\n                onClick = {\n                    component.addQueue()\n                }\n            )\n            spacer()\n            IconActionButton(\n                icon = MyIcons.remove,\n                contentDescription = Res.string.remove_queue.asStringSource(),\n                enabled = component.canDeleteThisQueue(selectedItem),\n                onClick = {\n                    component.requestDeleteQueue(selectedItem)\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/DesktopSettings.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.desktop.storage.AppSettingsStorage\nimport com.abdownloadmanager.desktop.ui.configurable.platform.item.FontConfigurable\nimport com.abdownloadmanager.desktop.utils.renderapi.CustomRenderApi\nimport com.abdownloadmanager.desktop.utils.renderapi.RenderApi\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.EnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyMode\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isMac\nimport kotlinx.coroutines.CoroutineScope\n\nobject DesktopSettings {\n    fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_compact_top_bar.asStringSource(),\n            description = Res.string.settings_compact_top_bar_description.asStringSource(),\n            backedBy = appSettings.mergeTopBarWithTitleBar,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useNativeMenuBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable? {\n        if (Platform.Companion.isMac().not()) return null\n        return BooleanConfigurable(\n            title = Res.string.settings_use_native_menu_bar.asStringSource(),\n            description = Res.string.settings_use_native_menu_bar_description.asStringSource(),\n            backedBy = appSettings.useNativeMenuBar,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useSystemTray(appSettings: AppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_use_system_tray.asStringSource(),\n            description = Res.string.settings_use_system_tray_description.asStringSource(),\n            backedBy = appSettings.useSystemTray,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun fontConfig(\n        fontManager: FontManager,\n        scope: CoroutineScope,\n    ): FontConfigurable {\n        return FontConfigurable(\n            title = Res.string.settings_font.asStringSource(),\n            description = Res.string.settings_font_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = fontManager.currentFontInfo,\n                updater = { font ->\n                    fontManager.setFont(font.id)\n                },\n                scope = scope,\n            ),\n            possibleValues = fontManager.selectableFonts.value,\n            describe = {\n                it.name\n            }\n        )\n    }\n\n    fun renderApi(\n        customRenderApi: CustomRenderApi,\n    ): EnumConfigurable<RenderApi?> {\n        return EnumConfigurable(\n            title = \"Render API\".asStringSource(),\n            description = \"Configures the Render API backend used by the application. A restart is required for the change to take effect.\".asStringSource(),\n            backedBy = customRenderApi.data,\n            possibleValues = buildList {\n                add(null)\n                addAll(customRenderApi.getSupportedRenderApiForThisPlatform())\n            },\n            describe = {\n                it?.prettyName?.asStringSource()?: Res.string.default.asStringSource()\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/DesktopSettingsComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport com.abdownloadmanager.desktop.pages.settings.SettingSection.*\nimport com.abdownloadmanager.desktop.repository.AppRepository\nimport com.abdownloadmanager.desktop.storage.AppSettingsStorage\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.desktop.storage.PageStatesStorage\nimport com.abdownloadmanager.desktop.utils.renderapi.CustomRenderApi\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.settings.BaseSettingsComponent\nimport com.abdownloadmanager.shared.settings.CommonSettings\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.*\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.flow.*\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nsealed class SettingSection(\n    val icon: IconSource,\n    val name: StringSource,\n) {\n    data object Appearance :\n        SettingSection(MyIcons.appearance, Res.string.appearance.asStringSource())\n\n    //    TODO ADD Network section (proxy , etc..)\n    //    data object Network : SettingSections(MyIcons.network, \"Network\")\n    data object DownloadEngine :\n        SettingSection(MyIcons.downloadEngine, Res.string.download_engine.asStringSource())\n\n    data object BrowserIntegration :\n        SettingSection(MyIcons.network, Res.string.browser_integration.asStringSource())\n}\n\ninterface SettingSectionGetter {\n    operator fun get(key: SettingSection): List<ConfigurableGroup>\n}\n\nclass DesktopSettingsComponent(\n    ctx: ComponentContext,\n    val perHostSettingsPageManager: PerHostSettingsPageManager,\n) : BaseSettingsComponent(ctx),\n    KoinComponent {\n    private val appSettings by inject<AppSettingsStorage>()\n    private val pageStorage by inject<PageStatesStorage>()\n    private val appRepository by inject<AppRepository>()\n    private val proxyManager by inject<ProxyManager>()\n    private val themeManager by inject<ThemeManager>()\n    private val languageManager by inject<LanguageManager>()\n    private val fontManager by inject<FontManager>()\n    private val customRenderApi by inject<CustomRenderApi>()\n    private val allConfigs = object : SettingSectionGetter {\n        override operator fun get(key: SettingSection): List<ConfigurableGroup> {\n            return when (key) {\n                Appearance -> listOf(\n                    ConfigurableGroup(\n                        mainConfigurable = CommonSettings.themeConfig(themeManager, scope),\n                        nestedVisible = themeManager.currentThemeInfo.mapStateFlow {\n                            it.id == ThemeManager.systemThemeInfo.id\n                        },\n                        nestedConfigurable = listOfNotNull(\n                            CommonSettings.defaultDarkThemeConfig(themeManager, scope),\n                            CommonSettings.defaultLightThemeConfig(themeManager, scope),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.languageConfig(languageManager, scope),\n                            DesktopSettings.fontConfig(fontManager, scope),\n                            CommonSettings.uiScaleConfig(appSettings),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOfNotNull(\n                            DesktopSettings.useNativeMenuBarConfig(appSettings),\n                            DesktopSettings.mergeTopBarWithTitleBarConfig(appSettings),\n                            CommonSettings.showIconLabels(appSettings),\n                            CommonSettings.useRelativeDateTime(appSettings),\n                            CommonSettings.playSoundNotification(appSettings),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.autoStartConfig(appSettings),\n                            DesktopSettings.useSystemTray(appSettings),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.sizeUnit(appRepository, scope),\n                            CommonSettings.speedUnit(appRepository, scope),\n                            CommonSettings.useAverageSpeedConfig(appRepository),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.autoShowDownloadProgressWindow(appSettings),\n                            CommonSettings.showDownloadFinishWindow(appSettings),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            DesktopSettings.renderApi(customRenderApi),\n                        )\n                    )\n                )\n\n//                Network -> listOf()\n                BrowserIntegration -> listOf(\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.browserIntegrationEnabled(appRepository),\n                            CommonSettings.browserIntegrationPort(appRepository)\n                        )\n                    )\n                )\n\n                DownloadEngine -> listOf(\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.defaultDownloadFolderConfig(appSettings),\n                            CommonSettings.useCategoryByDefault(appSettings),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.speedLimitConfig(appRepository),\n                            CommonSettings.threadCountConfig(appRepository),\n                            CommonSettings.maxConcurrentDownloads(appRepository),\n                            CommonSettings.maxDownloadRetryCount(appRepository),\n                            CommonSettings.dynamicPartDownloadConfig(appRepository),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.perHostSettings(perHostSettingsPageManager),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.proxyConfig(proxyManager),\n                            CommonSettings.userAgent(appSettings),\n                            CommonSettings.ignoreSSLCertificates(appSettings),\n                            CommonSettings.useServerLastModified(appRepository),\n                        )\n                    ),\n                    ConfigurableGroup(\n                        nestedConfigurable = listOf(\n                            CommonSettings.trackDeletedFilesOnDisk(appRepository),\n                            CommonSettings.appendExtensionToIncompleteDownloads(appRepository),\n                            CommonSettings.deletePartialFileOnDownloadCancellation(appSettings),\n                            CommonSettings.useSparseFileAllocation(appRepository),\n                        )\n                    ),\n                )\n            }\n        }\n    }\n\n    fun toFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    val settingsPageStateToPersist = MutableStateFlow(pageStorage.settingsPageStorage.value)\n    private val _windowSize = settingsPageStateToPersist.mapTwoWayStateFlow(\n        map = {\n            it.windowSize.let { (x, y) ->\n                DpSize(x.dp, y.dp)\n            }\n        },\n        unMap = {\n            copy(\n                windowSize = it.width.value to it.height.value\n            )\n        }\n    )\n    val windowSize = _windowSize.asStateFlow()\n    fun setWindowSize(dpSize: DpSize) {\n        _windowSize.value = dpSize\n    }\n\n    init {\n        settingsPageStateToPersist\n            .debounce(500)\n            .onEach { newValue ->\n                pageStorage.settingsPageStorage.update { newValue }\n            }.launchIn(scope)\n    }\n\n    var pages = listOf(\n        Appearance,\n//        Network,\n        DownloadEngine,\n        BrowserIntegration,\n    )\n    private val _currentPage: MutableStateFlow<SettingSection> = MutableStateFlow(Appearance)\n    val currentPage: StateFlow<SettingSection> = _currentPage.asStateFlow()\n    fun setCurrentPage(section: SettingSection) {\n        _currentPage.value = section\n        _configurables.value = allConfigs[section]\n    }\n\n    private val _configurables = MutableStateFlow(\n        allConfigs[currentPage.value]\n    )\n    override val configurables = _configurables.asStateFlow()\n\n    sealed interface Effects : BaseSettingsComponent.Effects.Platform {\n        data object BringToFront : Effects\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/FontManager.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.text.ExperimentalTextApi\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.platform.FileFont\nimport androidx.compose.ui.text.platform.ResourceFont\nimport com.abdownloadmanager.desktop.storage.AppSettingsStorage\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.contants.FILE_PROTOCOL\nimport ir.amirab.util.compose.contants.RESOURCE_PROTOCOL\nimport ir.amirab.util.compose.contants.SYSTEM_PROTOCOL\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.guardedEntry\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport java.awt.GraphicsEnvironment\nimport java.net.URI\nimport kotlin.io.path.toPath\n\n\nclass FontManager(\n    private val appSettings: AppSettingsStorage,\n) {\n    companion object {\n        private const val DEFAULT_FONT_ID = \"default\"\n        val defaultFontInfo = FontInfo(\n            id = DEFAULT_FONT_ID,\n            uri = \"\",\n            name = Res.string.default.asStringSource(),\n            fontFamily = FontFamily.Default,\n        )\n\n        fun getUsableFontFamilyNamesOfSystem(): List<String> {\n            return GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames\n                .toList()\n        }\n    }\n\n    private val _availableFonts = MutableStateFlow(emptyList<FontInfo>())\n    val availableFonts = _availableFonts.asStateFlow()\n\n    private fun getFontByUri(uri: String): FontFamily? {\n        return runCatching {\n            FontFamilyUtil.fromUri(URI.create(uri))\n        }\n            .onFailure {\n                it.printStackTrace()\n            }\n            .getOrNull()\n    }\n\n    val selectableFonts = availableFonts.mapStateFlow {\n        buildList {\n            add(defaultFontInfo)\n            addAll(it)\n        }\n    }\n\n    val currentFontInfo = combineStateFlows(\n        appSettings.font,\n        selectableFonts,\n    ) { fontId, possibleFonts ->\n        val fontId = fontId ?: DEFAULT_FONT_ID\n        possibleFonts.find {\n            it.id == fontId\n        } ?: possibleFonts.find {\n            it.id == DEFAULT_FONT_ID\n        }!!\n    }\n\n    val currentFontFamily = currentFontInfo.mapStateFlow {\n        it.fontFamily\n    }\n\n    fun setFont(fontId: String?) {\n        synchronized(this) {\n            val fontId = fontId ?: DEFAULT_FONT_ID\n            val font = availableFonts.value.find { it.id == fontId }\n                ?: defaultFontInfo\n\n            appSettings.font.value = font.takeIf {\n                it != defaultFontInfo\n            }?.uri\n        }\n    }\n\n    private val booted = guardedEntry()\n\n    fun boot() {\n        booted.action {\n            val systemFontFamilies = getUsableFontFamilyNamesOfSystem()\n                .mapNotNull { fontFamilyName ->\n                    val uri = runCatching {\n                        URI(SYSTEM_PROTOCOL, fontFamilyName, null).toString()\n                    }.onFailure { throwable ->\n                        // it seems that some fonts has empty name, which causes URI creation to fail\n                        // in order to not break the app, we will just ignore those fonts\n                        println(\"system font family with name:\\\"$fontFamilyName\\\" can't be used: $throwable\")\n                    }.getOrNull()\n                    if (uri == null) {\n                        return@mapNotNull null\n                    }\n                    val fontFamily = getFontByUri(uri)\n                    if (fontFamily == null) {\n                        return@mapNotNull null\n                    }\n                    FontInfo(\n                        id = uri,\n                        uri = uri,\n                        name = fontFamilyName.asStringSource(),\n                        fontFamily = fontFamily,\n                    )\n                }\n\n            _availableFonts.update {\n                it.plus(systemFontFamilies)\n            }\n            setFont(appSettings.font.value)\n        }\n    }\n\n}\n\n/**\n * This is for demonstration purposes of a font\n */\n@Immutable\ndata class FontInfo(\n    val id: String,\n    val uri: String,\n    val name: StringSource,\n    val fontFamily: FontFamily,\n)\n\nprivate object FontFamilyUtil {\n    @OptIn(ExperimentalTextApi::class)\n    fun fromUri(uri: URI): FontFamily {\n        return when (uri.scheme) {\n            FILE_PROTOCOL -> {\n                return FontFamily(\n                    FileFont(uri.toPath().toFile())\n                )\n            }\n\n            RESOURCE_PROTOCOL -> {\n                val path = uri.schemeSpecificPart\n                require(path.isNotEmpty())\n                FontFamily(\n                    ResourceFont(uri.schemeSpecificPart)\n                )\n            }\n\n            SYSTEM_PROTOCOL -> {\n                // This is a system font, we can use it directly\n                val name = uri.schemeSpecificPart\n                require(name.isNotEmpty())\n                FontFamily(name)\n            }\n\n            else -> throw IllegalArgumentException(\"Unsupported font URI scheme: ${uri.scheme}\")\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingPageStateToPersist.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport arrow.optics.Lens\nimport com.abdownloadmanager.desktop.pages.home.HomePageStateToPersist\nimport ir.amirab.util.config.*\nimport kotlinx.serialization.Serializable\nimport org.koin.core.component.KoinComponent\n\n@Serializable\ndata class SettingPageStateToPersist(\n    val windowSize: Pair<Float, Float> = 800f to 400f\n) {\n    class ConfigLens(prefix: String) : Lens<MapConfig, SettingPageStateToPersist>,\n        KoinComponent {\n\n        class Keys(prefix: String) {\n            val windowWidth = floatKeyOf(\"${prefix}window.width\")\n            val windowHeight = floatKeyOf(\"${prefix}window.height\")\n        }\n\n        private val keys = Keys(prefix)\n        override fun get(source: MapConfig): SettingPageStateToPersist {\n            val default by lazy { HomePageStateToPersist() }\n            return SettingPageStateToPersist(\n                windowSize = run {\n                    val width = source.get(keys.windowWidth)\n                    val height = source.get(keys.windowHeight)\n                    if (height != null && width != null) {\n                        width to height\n                    } else {\n                        default.windowSize\n                    }\n                }\n            )\n        }\n\n        override fun set(source: MapConfig, focus: SettingPageStateToPersist): MapConfig {\n            source.put(keys.windowWidth, focus.windowSize.first)\n            source.put(keys.windowHeight, focus.windowSize.second)\n            return source\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingWindow.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.window.WindowPlacement\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.settings.BaseSettingsComponent\n\n@Composable\nfun SettingWindow(\n    settingsComponent: DesktopSettingsComponent,\n    onRequestCloseWindow: () -> Unit,\n) {\n    val windowState = rememberWindowState(\n        size = settingsComponent.windowSize.value,\n        position = WindowPosition.Aligned(Alignment.Center),\n    )\n    LaunchedEffect(windowState.size) {\n        if (!windowState.isMinimized && windowState.placement == WindowPlacement.Floating) {\n            settingsComponent.setWindowSize(windowState.size)\n        }\n    }\n    CustomWindow(windowState, {\n        onRequestCloseWindow()\n    }) {\n        HandleEffects(settingsComponent) {\n            when (it) {\n                is BaseSettingsComponent.Effects.Platform -> {\n                    when (it as DesktopSettingsComponent.Effects) {\n                        DesktopSettingsComponent.Effects.BringToFront -> {\n                            windowState.isMinimized = false\n                            window.toFront()\n                        }\n                    }\n                }\n            }\n        }\n//        Spacer(Modifier.fillMaxWidth().height(1.dp).background(myColors.surface))\n        SettingsPage(settingsComponent, onRequestCloseWindow)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.settings\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.desktop.window.custom.WindowIcon\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.ui.widget.Handle\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.*\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.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurableGroup\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.needScroll\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport io.github.oikvpqya.compose.fastscroller.rememberScrollbarAdapter\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.ifThen\n\n@Composable\nprivate fun SideBar(\n    settingsComponent: DesktopSettingsComponent,\n    modifier: Modifier = Modifier,\n) {\n    val shape = myShapes.defaultRounded\n    Column(\n        modifier\n            .fillMaxHeight()\n            .border(1.dp, myColors.surface / 0.5f, shape)\n            .clip(shape)\n    ) {\n//        var searchText by remember { mutableStateOf(\"\") }\n//        SearchBox(\n//            searchText,\n//            onTextChange = { searchText = it },\n//            modifier = Modifier.height(38.dp),\n//        )\n        val collectAsState by settingsComponent.currentPage.collectAsState()\n        for (i in settingsComponent.pages) {\n            SideBarItem(\n                icon = i.icon,\n                name = i.name.rememberString(),\n                isSelected = collectAsState == i,\n                onClick = {\n                    settingsComponent.setCurrentPage(i)\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SideBarItem(icon: IconSource, name: String, isSelected: Boolean, onClick: () -> Unit) {\n    Box(\n        Modifier\n            .height(IntrinsicSize.Max)\n            .ifThen(isSelected) {\n                background(myColors.onBackground / 0.05f)\n            }\n            .selectable(\n                selected = isSelected,\n                onClick = onClick\n            )\n    ) {\n        Row(\n            Modifier\n                .padding(vertical = 8.dp)\n                .padding(start = 16.dp)\n                .padding(end = 2.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(if (isSelected) 1f else 0.75f) {\n                MyIcon(\n                    icon,\n                    null,\n                    Modifier.size(16.dp)\n                )\n                Spacer(Modifier.width(4.dp))\n                Text(\n                    name,\n                    Modifier.weight(1f),\n                    fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,\n                    fontSize = myTextSizes.lg,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                )\n            }\n        }\n        AnimatedVisibility(\n            isSelected,\n            modifier = Modifier\n                .align(Alignment.CenterStart),\n            enter = scaleIn(),\n            exit = scaleOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .height(16.dp)\n                    .width(3.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            bottomStart = 0.dp,\n                            bottomEnd = 12.dp,\n                            topEnd = 12.dp,\n                        )\n                    )\n                    .background(myColors.primary)\n            )\n        }\n        if (isSelected) {\n            listOf(\n                Alignment.TopCenter,\n                Alignment.BottomCenter,\n            ).forEach {\n                Spacer(\n                    Modifier\n                        .align(it)\n                        .fillMaxWidth()\n                        .height(1.dp)\n                        .background(\n                            Brush.horizontalGradient(\n                                listOf(\n                                    Color.Transparent,\n                                    myColors.onBackground / 0.1f,\n                                    myColors.onBackground / 0.1f,\n                                    Color.Transparent,\n                                )\n                            )\n                        )\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun SettingsPage(\n    settingsComponent: DesktopSettingsComponent,\n    onDismissRequest: () -> Unit,\n) {\n    WindowTitle(myStringResource(Res.string.settings))\n//    WindowIcon(MyIcons.settings)\n    WindowIcon(MyIcons.appIcon)\n    Column {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.surface)\n        )\n        Row {\n            var sideBarWidth by remember { mutableStateOf(250.dp) }\n            SideBar(\n                settingsComponent,\n                Modifier\n                    .fillMaxHeight()\n                    .width(sideBarWidth)\n                    .padding(8.dp)\n            )\n            val currentConfigurables by settingsComponent.configurables.collectAsState()\n            Handle(\n                Modifier.width(5.dp).fillMaxHeight(),\n                orientation = Orientation.Horizontal\n            ) {\n                sideBarWidth = (sideBarWidth + it).coerceIn(150.dp..300.dp)\n            }\n            AnimatedContent(currentConfigurables) { configurableGroups ->\n                val scrollState = rememberScrollState()\n                val scrollbarAdapter = rememberScrollbarAdapter(scrollState)\n                Row {\n                    Column(\n                        Modifier\n                            .weight(1f)\n                            .verticalScroll(scrollState)\n                            .padding(\n                                horizontal = 8.dp,\n                                vertical = 8.dp\n                            ),\n                        verticalArrangement = Arrangement.spacedBy(16.dp)\n                    ) {\n                        for (cfgGroup in configurableGroups) {\n                            RenderConfigurableGroup(\n                                cfgGroup,\n                                Modifier,\n                                itemPadding = PaddingValues(\n                                    vertical = 8.dp,\n                                    horizontal = 16.dp\n                                )\n                            )\n                        }\n                    }\n                    if (scrollbarAdapter.needScroll()) {\n                        MultiplatformVerticalScrollbar(\n                            adapter = scrollbarAdapter,\n                            modifier = Modifier\n                                .padding(vertical = 8.dp)\n                                .padding(end = 2.dp),\n                        )\n                    }\n                }\n            }\n\n        }\n    }\n}\n\n\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/CompletedDownloadPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.singleDownloadPage\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.draganddrop.dragAndDropSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draganddrop.DragAndDropTransferAction\nimport androidx.compose.ui.draganddrop.DragAndDropTransferData\nimport androidx.compose.ui.draganddrop.DragAndDropTransferable\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.pages.home.DownloadItemTransferable\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.IconActionButton\nimport com.abdownloadmanager.shared.ui.widget.Tooltip\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.isShiftPressed\n\n@Composable\nfun CompletedDownloadPage(\n    component: DesktopSingleDownloadComponent,\n    completedDownloadItemState: CompletedDownloadItemState,\n) {\n    Column {\n        Row(\n            Modifier\n                .padding(\n                    horizontal = 16.dp,\n                    vertical = 8.dp\n                )\n        ) {\n            RenderFileIconAndSize(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                component = component,\n                itemState = completedDownloadItemState,\n            )\n            Spacer(Modifier.width(16.dp))\n            RenderName(\n                Modifier.weight(1f),\n                completedDownloadItemState.name,\n            )\n        }\n        Spacer(Modifier.weight(1f))\n        Actions(Modifier, component)\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier,\n    component: DesktopSingleDownloadComponent,\n) {\n    val iDownloadItemState by component.itemStateFlow.collectAsState()\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            ActionButton(\n                myStringResource(Res.string.open),\n                modifier = Modifier,\n                onClick = {\n                    component.openFile()\n                },\n            )\n            Spacer(Modifier.width(8.dp))\n            ActionButton(\n                myStringResource(Res.string.open_folder),\n                modifier = Modifier,\n                onClick = {\n                    component.openFolder()\n                },\n            )\n            Spacer(Modifier.width(8.dp))\n            val dragTheFileDescription = Res.string.drag_the_file_to_another_app.asStringSource()\n            val windowInfo = LocalWindowInfo.current\n            Tooltip(dragTheFileDescription) {\n                IconActionButton(\n                    icon = MyIcons.dragAndDrop,\n                    contentDescription = dragTheFileDescription,\n                    modifier = Modifier\n                        .dragAndDropSource(\n                            drawDragDecoration = {},\n                            transferData = {\n                                val completedDownloadItemState =\n                                    iDownloadItemState as? CompletedDownloadItemState\n                                        ?: return@dragAndDropSource null\n\n                                val shiftPressed = isShiftPressed(windowInfo)\n                                val supportedActions = listOf(\n                                    if (shiftPressed) {\n                                        DragAndDropTransferAction.Move\n                                    } else {\n                                        DragAndDropTransferAction.Copy\n                                    }\n                                )\n                                DragAndDropTransferData(\n                                    transferable = DragAndDropTransferable(\n                                        DownloadItemTransferable(\n                                            listOf(completedDownloadItemState)\n                                        )\n                                    ),\n                                    supportedActions = supportedActions,\n                                )\n                            }\n                        ),\n                    onClick = {},\n                )\n            }\n\n            Spacer(Modifier.weight(1f))\n            ActionButton(\n                myStringResource(Res.string.close),\n                modifier = Modifier,\n                onClick = component::close,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderName(\n    modifier: Modifier,\n    name: String,\n) {\n    Column(\n        modifier = modifier\n    ) {\n        WithContentColor(\n            myColors.success\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                MyIcon(\n                    MyIcons.check, null,\n                    Modifier.size(24.dp)\n                )\n                Spacer(Modifier.width(4.dp))\n                Text(\n                    myStringResource(Res.string.download_page_download_completed),\n                    fontWeight = FontWeight.Bold,\n                    fontSize = myTextSizes.lg,\n                )\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        Text(\n            text = name,\n            maxLines = 1,\n            modifier = Modifier.basicMarquee(\n                iterations = Int.MAX_VALUE\n            )\n        )\n    }\n}\n\n@Composable\nprivate fun RenderFileIconAndSize(\n    modifier: Modifier,\n    component: DesktopSingleDownloadComponent,\n    itemState: CompletedDownloadItemState,\n) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        MyIcon(\n            icon = component.fileIconProvider.rememberIcon(itemState.name),\n            contentDescription = null,\n            modifier = Modifier.size(24.dp),\n        )\n        Spacer(Modifier.height(4.dp))\n        Text(\n            text = convertPositiveSizeToHumanReadable(\n                itemState.contentLength,\n                LocalSizeUnit.current,\n            ).rememberString(),\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/DesktopSingleDownloadPageComponent.kt",
    "content": "package com.abdownloadmanager.desktop.pages.singleDownloadPage\n\nimport arrow.optics.copy\nimport com.abdownloadmanager.desktop.storage.DesktopExtraDownloadItemSettings\nimport com.abdownloadmanager.desktop.storage.PageStatesStorage\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.singledownloadpage.BaseSingleDownloadComponent\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.util.*\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.update\nimport org.koin.core.component.get\nimport kotlin.getValue\n\nclass DesktopSingleDownloadComponent(\n    ctx: ComponentContext,\n    downloadItemOpener: DownloadItemOpener,\n    onDismiss: () -> Unit,\n    downloadId: Long,\n    extraDownloadSettingsStorage: ExtraDownloadSettingsStorage<DesktopExtraDownloadItemSettings>,\n    downloadSystem: DownloadSystem,\n    appSettings: BaseAppSettingsStorage,\n    appRepository: BaseAppRepository,\n    applicationScope: CoroutineScope,\n    fileIconProvider: FileIconProvider,\n) : BaseSingleDownloadComponent<DesktopExtraDownloadItemSettings>(\n    ctx = ctx,\n    downloadItemOpener = downloadItemOpener,\n    onDismiss = onDismiss,\n    downloadId = downloadId,\n    extraDownloadSettingsStorage = extraDownloadSettingsStorage,\n    downloadSystem = downloadSystem,\n    appSettings = appSettings,\n    appRepository = appRepository,\n    applicationScope = applicationScope,\n    fileIconProvider = fileIconProvider,\n) {\n    private val singleDownloadPageStateToPersist by lazy {\n        get<PageStatesStorage>().singleDownloadPageState\n    }\n    override val defaultShowPartInfo: Boolean = singleDownloadPageStateToPersist.value.showPartInfo\n\n    override fun setShowPartInfo(value: Boolean) {\n        super.setShowPartInfo(value)\n        singleDownloadPageStateToPersist.update {\n            it.copy {\n                SingleDownloadPageStateToPersist.showPartInfo.set(value)\n            }\n        }\n    }\n\n    sealed interface Effects : BaseSingleDownloadComponent.Effects.Platform {\n        data object BringToFront : Effects\n    }\n\n    fun bringToFront() {\n        sendEffect(Effects.BringToFront)\n    }\n\n    val onCompletion by lazy {\n        listOf(\n            BooleanConfigurable(\n                title = Res.string.download_item_settings_shutdown_on_completion.asStringSource(),\n                description = Res.string.download_item_settings_shutdown_on_completion_description.asStringSource(),\n                backedBy = extraDownloadItemSettingsFlow.mapTwoWayStateFlow(\n                    map = {\n                        it.powerActionTypeOnFinish != null\n                    },\n                    unMap = {\n                        copy(\n                            powerActionTypeOnFinish = when (it) {\n                                true -> PowerActionConfig.Type.Shutdown\n                                false -> null\n                            },\n                        )\n                    }\n                ),\n                describe = {\n                    when (it) {\n                        true -> Res.string.enabled\n                        false -> Res.string.disabled\n                    }.asStringSource()\n                },\n            ),\n            BooleanConfigurable(\n                title = Res.string.download_item_settings_show_download_completion_dialog.asStringSource(),\n                description = Res.string.download_item_settings_show_download_completion_dialog_description.asStringSource(),\n                backedBy = itemShouldShowCompletionDialog.mapTwoWayStateFlow(\n                    map = {\n                        it ?: globalShowCompletionDialog.value\n                    },\n                    unMap = { it }\n                ),\n                describe = {\n                    when (it) {\n                        true -> Res.string.enabled\n                        false -> Res.string.disabled\n                    }.asStringSource()\n                },\n            ),\n        )\n    }\n\n    data class Config(\n        override val id: Long\n    ) : BaseSingleDownloadComponent.Config\n}\n\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/ProgressDownloadPage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.singleDownloadPage\n\nimport com.abdownloadmanager.shared.ui.configurable.RenderConfigurable\nimport com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadPageSections.*\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsDraggedAsState\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\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.Color\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\nimport com.abdownloadmanager.shared.ui.widget.*\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.CellSize\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.Table\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableCell\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableState\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.singledownloadpage.SingleDownloadPagePropertyItem\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.ui.useIsInDebugMode\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.*\nimport ir.amirab.downloader.part.PartDownloadStatus\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\n\nenum class SingleDownloadPageSections(\n    val title: StringSource,\n    val icon: IconSource,\n) {\n    Info(\n        Res.string.info.asStringSource(),\n        MyIcons.info\n    ),\n    Settings(\n        Res.string.speed.asStringSource(),\n        MyIcons.fast,\n    ),\n    OnCompletion(\n        Res.string.on_completion.asStringSource(),\n        MyIcons.flag\n    ),\n}\n\nprivate val tabs = entries.toList()\n\n@Composable\nfun ProgressDownloadPage(\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n    itemState: ProcessingDownloadItemState\n) {\n    var selectedTab by remember { mutableStateOf(Info) }\n    val showPartInfo by singleDownloadComponent.showPartInfo.collectAsState()\n    val setShowPartInfo = singleDownloadComponent::setShowPartInfo\n    val resizingState = LocalSingleDownloadPageSizing.current\n    val horizontalPadding = 16.dp\n\n    LaunchedEffect(resizingState.resizingPartInfo) {\n        if (resizingState.partInfoHeight <= 0.dp) {\n            setShowPartInfo(false)\n        }\n    }\n    Column(\n        Modifier.fillMaxSize()\n    ) {\n        Column(\n            Modifier\n                .clip(myShapes.defaultRounded)\n                .padding(1.dp),\n        ) {\n            //tabs\n            MyTabRow {\n                for (tab in tabs) {\n                    MyTab(\n                        selected = tab == selectedTab,\n                        onClick = {\n                            selectedTab = tab\n                        },\n                        icon = tab.icon,\n                        title = tab.title,\n                    )\n                }\n            }\n            val scrollState = rememberScrollState()\n            //info / settings ...\n            val tabContentModifier = Modifier\n\n            Spacer(Modifier.fillMaxWidth().height(1.dp).background(myColors.surface))\n            Box(\n                Modifier\n                    .height(150.dp)\n                    .background(myColors.background)\n                    .verticalScroll(scrollState)\n            ) {\n                when (selectedTab) {\n                    Info -> RenderInfo(\n                        tabContentModifier,\n                        horizontalPadding,\n                        singleDownloadComponent,\n                    )\n\n                    Settings -> RenderSettings(\n                        modifier = tabContentModifier.padding(end = 12.dp),\n                        horizontalPadding = horizontalPadding,\n                        singleDownloadComponent = singleDownloadComponent,\n                    )\n\n                    OnCompletion -> RenderOnCompletion(\n                        modifier = tabContentModifier.padding(end = 12.dp),\n                        horizontalPadding = horizontalPadding,\n                        singleDownloadComponent = singleDownloadComponent,\n                    )\n                }\n                MultiplatformVerticalScrollbar(\n                    adapter = rememberScrollbarAdapter(scrollState),\n                    modifier = Modifier.matchParentSize().wrapContentWidth(Alignment.End),\n                )\n            }\n        }\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Column(\n            Modifier\n                .weight(1f)\n                .background(myColors.surface / 0.5f)\n        ) {\n            Column(\n                Modifier\n                    .padding(horizontal = horizontalPadding)\n            ) {\n                Spacer(Modifier.size(8.dp))\n                RenderProgressBar(itemState)\n                Spacer(Modifier.size(8.dp))\n                RenderActions(itemState, singleDownloadComponent, showPartInfo, setShowPartInfo)\n                Spacer(Modifier.size(8.dp))\n            }\n            if (showPartInfo) {\n                RenderPartInfo(\n                    modifier = Modifier.weight(1f),\n                    itemState = itemState,\n                    horizontalPadding = horizontalPadding,\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderSettings(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n) {\n    Column(modifier) {\n        for (configurable in singleDownloadComponent.settings) {\n            RenderConfigurable(\n                configurable,\n                ConfigurableUiProps(\n                    modifier = Modifier\n                        // I'm using Configurable object which their renderer by default uses 8.dp, we want 16.dp, so I only add 8.dp here 16-8 == 8\n                        // I may improve this later\n                        .padding(horizontal = (horizontalPadding - 8.dp).coerceAtLeast(0.dp))\n                )\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderOnCompletion(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n) {\n    Column(modifier) {\n        for (configurable in singleDownloadComponent.onCompletion) {\n            RenderConfigurable(\n                configurable, ConfigurableUiProps(\n                    modifier = Modifier\n                        // I'm using Configurable object which their renderer by default uses 8.dp, we want 16.dp, so I only add 8.dp here 16-8 == 8\n                        // I may improve this later\n                        .padding(horizontal = (horizontalPadding - 8.dp).coerceAtLeast(0.dp))\n                )\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderProgressBar(itemState: IDownloadItemState) {\n    val progress = when (itemState) {\n        is CompletedDownloadItemState -> 100\n        is ProcessingDownloadItemState -> when (val status = itemState.status) {\n            is DownloadJobStatus.PreparingFile -> status.percent\n            else -> itemState.percent\n        }\n    }?.let {\n        it / 100f\n    }\n\n    val status = itemState.statusOrFinished()\n    val background = when (status) {\n        is DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.Canceled -> if (ExceptionUtils.isNormalCancellation(status.e)) {\n            myColors.warningGradient\n        } else {\n            myColors.errorGradient\n        }\n\n        DownloadJobStatus.IDLE -> myColors.warningGradient\n        is DownloadJobStatus.Retrying -> myColors.errorGradient\n        DownloadJobStatus.Finished -> myColors.successGradient\n        is DownloadJobStatus.PreparingFile -> myColors.infoGradient\n        DownloadJobStatus.Resuming,\n        DownloadJobStatus.Downloading,\n            -> myColors.primaryGradient\n    }\n\n    Box(\n        Modifier\n            .fillMaxWidth()\n            .clip(myShapes.defaultRounded)\n            .height(14.dp)\n            .background(myColors.onBackground / 15)\n    ) {\n        progress?.let { progress ->\n            Box(\n                Modifier\n                    .background(background)\n                    .fillMaxHeight()\n                    .fillMaxWidth(\n                        animateFloatAsState(\n                            progress,\n                            tween(100, easing = LinearEasing)\n                        ).value\n                    )\n            ) {\n                if (progress == 1f) {\n                    MyIcon(\n                        MyIcons.check,\n                        null,\n                        Modifier\n                            .padding(1.dp)\n                            .clip(CircleShape)\n                            .background(myColors.onBackground)\n                            .padding(1.dp)\n                            .fillMaxHeight()\n                            .align(Alignment.CenterEnd),\n                        tint = myColors.background,\n                    )\n                }\n            }\n        }\n        if (progress == null && status is DownloadJobStatus.IsActive) {\n            val anim = rememberInfiniteTransition()\n            val l = 2000\n            val endPos by anim.animateFloat(\n                0f,\n                1f,\n                infiniteRepeatable(tween(l), RepeatMode.Restart)\n            )\n            val width by anim.animateFloat(\n                6f, 16f, infiniteRepeatable(\n                    keyframes {\n                        durationMillis = l\n                        0f atFraction 0f\n                        0.75f atFraction 0.25f\n                        0f atFraction 1f\n                    },\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n            Box(\n                Modifier\n                    .fillMaxHeight()\n                    .fillMaxWidth(endPos)\n            ) {\n                Box(\n                    Modifier\n                        .background(background)\n                        .fillMaxHeight()\n                        .align(Alignment.CenterEnd)\n                        .fillMaxWidth(width)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderPartInfo(\n    modifier: Modifier,\n    itemState: ProcessingDownloadItemState,\n    horizontalPadding: Dp,\n) {\n    Column(modifier) {\n        val singleDownloadPageSizing = LocalSingleDownloadPageSizing.current\n        val mutableInteractionSource = remember { MutableInteractionSource() }\n        val isDraggingHandle by mutableInteractionSource.collectIsDraggedAsState()\n        LaunchedEffect(isDraggingHandle) {\n            singleDownloadPageSizing.resizingPartInfo = isDraggingHandle\n        }\n        Column(\n            Modifier.weight(1f)\n        ) {\n            RenderParts(\n                itemState.parts,\n                Modifier\n                    .padding(horizontal = horizontalPadding)\n                    .height(4.dp)\n                    .clip(myShapes.defaultRounded)\n                    .background(myColors.onBackground / 15)\n            )\n            Box(\n                Modifier.weight(1f)\n            ) {\n                val (onlyActiveParts, setOnlyActiveParts) = rememberSaveable {\n                    mutableStateOf(true)\n                }\n                val listToShow = remember(itemState, onlyActiveParts) {\n                    itemState.parts\n                        .let { parts ->\n                            if (onlyActiveParts) {\n                                parts.filter {\n                                    when (it.status) {\n                                        is PartDownloadStatus.Canceled -> true\n                                        PartDownloadStatus.Completed -> false\n                                        PartDownloadStatus.IDLE -> false\n                                        PartDownloadStatus.ReceivingData -> true\n                                        PartDownloadStatus.Connecting -> true\n                                    }\n                                }\n                            } else {\n                                parts\n                            }\n                        }\n                        .withIndex()\n                        .toList()\n                }\n                Table(\n                    list = listToShow,\n                    key = {\n                        it.value.id\n                    },\n                    modifier = Modifier\n                        .fillMaxSize(),\n                    wrapHeader = {\n                        WithContentAlpha(0.75f) {\n                            Box(\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = horizontalPadding)\n                                    .padding(vertical = 4.dp)\n                            ) {\n                                it()\n                            }\n                        }\n                    },\n                    tableState = remember {\n                        TableState(\n                            cells = PartInfoCells.all()\n                        )\n                    },\n                    wrapItem = { _, _, content ->\n                        WithContentAlpha(1f) {\n                            val interactionSource = remember { MutableInteractionSource() }\n                            val isHovered by interactionSource.collectIsHoveredAsState()\n                            Box(\n                                Modifier\n                                    .padding(horizontal = horizontalPadding)\n                                    .hoverable(interactionSource)\n                                    .background(\n                                        if (isHovered) myColors.onSurface / 10\n                                        else Color.Transparent\n                                    )\n                            ) {\n                                content()\n                            }\n                        }\n                    }\n                ) { cell, it ->\n                    when (cell) {\n                        PartInfoCells.Number -> {\n                            SimpleCellText(\"${it.index + 1}\")\n                        }\n\n                        PartInfoCells.Status -> {\n                            SimpleCellText(prettifyStatus(it.value.status).rememberString())\n                        }\n\n                        PartInfoCells.Downloaded -> {\n                            SimpleCellText(\n                                convertPositiveSizeToHumanReadable(\n                                    it.value.howMuchProceed,\n                                    LocalSizeUnit.current\n                                ).rememberString()\n                            )\n                        }\n\n                        PartInfoCells.Total -> {\n                            SimpleCellText(\n                                it.value.length?.let { length ->\n                                    convertPositiveSizeToHumanReadable(length, LocalSizeUnit.current).rememberString()\n                                } ?: myStringResource(Res.string.unknown),\n                            )\n                        }\n                    }\n                }\n                if (useIsInDebugMode()) {\n                    Row(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .padding(bottom = 8.dp, end = 8.dp)\n                            .onClick { setOnlyActiveParts(!onlyActiveParts) },\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\"Only Actives\")\n                        Spacer(Modifier.width(4.dp))\n                        CheckBox(onlyActiveParts, { setOnlyActiveParts(it) })\n                    }\n                }\n            }\n        }\n        Handle(\n            Modifier.fillMaxWidth().height(8.dp),\n            orientation = Orientation.Vertical,\n            interactionSource = mutableInteractionSource\n        ) {\n            singleDownloadPageSizing.partInfoHeight += it\n        }\n    }\n}\n\nprivate fun prettifyStatus(status: PartDownloadStatus): StringSource {\n    return when (status) {\n        is PartDownloadStatus.Canceled -> Res.string.disconnected\n        PartDownloadStatus.IDLE -> Res.string.idle\n        PartDownloadStatus.Completed -> Res.string.finished\n        PartDownloadStatus.ReceivingData -> Res.string.receiving_data\n        PartDownloadStatus.Connecting -> Res.string.connecting\n    }.asStringSource()\n}\n\n@Composable\nprivate fun SimpleCellText(text: String) {\n    Text(text, fontSize = myTextSizes.base, maxLines = 1)\n}\n\nsealed class PartInfoCells : TableCell<IndexedValue<UiPart>> {\n    data object Number : PartInfoCells() {\n        override val id: String = \"#\"\n        override val name: StringSource = \"#\".asStringSource()\n        override val size: CellSize = CellSize.Fixed(32.dp)\n    }\n\n    data object Status : PartInfoCells() {\n        override val id: String = \"Status\"\n        override val name: StringSource = Res.string.status.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(100.dp..200.dp)\n    }\n\n    data object Downloaded : PartInfoCells() {\n        override val id: String = \"Downloaded\"\n        override val name: StringSource = Res.string.parts_info_downloaded_size.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)\n    }\n\n    data object Total : PartInfoCells() {\n        override val id: String = \"Total\"\n        override val name: StringSource = Res.string.parts_info_total_size.asStringSource()\n        override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)\n    }\n\n    companion object {\n        fun all(): List<PartInfoCells> {\n            return listOf(\n                Number,\n                Status,\n                Downloaded,\n                Total,\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {\n    val title = propertyItem.name\n    val value = propertyItem.value\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier.fillMaxWidth()\n    ) {\n        WithContentAlpha(0.75f) {\n            Text(\n                text = \"${title.rememberString()}:\",\n                modifier = Modifier.weight(0.3f),\n                maxLines = 1,\n                fontSize = myTextSizes.base\n            )\n        }\n        WithContentAlpha(1f) {\n            Text(\n                text = value.rememberString(),\n                modifier = Modifier\n                    .basicMarquee(\n                        iterations = Int.MAX_VALUE\n                    )\n                    .weight(0.7f),\n                maxLines = 1,\n                fontSize = myTextSizes.base,\n                color = when (propertyItem.valueState) {\n                    SingleDownloadPagePropertyItem.ValueType.Normal -> LocalContentColor.current\n                    SingleDownloadPagePropertyItem.ValueType.Error -> myColors.error\n                    SingleDownloadPagePropertyItem.ValueType.Success -> myColors.success\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RenderInfo(\n    modifier: Modifier,\n    horizontalPadding: Dp,\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n) {\n    Column(\n        modifier\n            .padding(horizontal = horizontalPadding)\n            .padding(top = 8.dp)\n    ) {\n        for (propertyItem in singleDownloadComponent.extraDownloadProgressInfo.collectAsState().value) {\n            Spacer(Modifier.height(2.dp))\n            RenderPropertyItem(propertyItem)\n        }\n    }\n}\n\n@Composable\nprivate fun RenderActions(\n    itemState: ProcessingDownloadItemState,\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n    showingPartInfo: Boolean,\n    onRequestShowPartInfo: (show: Boolean) -> Unit,\n) {\n    Row {\n        PartInfoButton(showingPartInfo, onRequestShowPartInfo)\n        Spacer(Modifier.weight(1f))\n        ToggleButton(\n            itemState = itemState,\n            toggle = singleDownloadComponent::toggle,\n            pause = singleDownloadComponent::pause,\n        )\n        Spacer(Modifier.width(8.dp))\n        CancelButton(\n            cancel = singleDownloadComponent::cancel,\n            icon = if (singleDownloadComponent.deletePartialFileOnDownloadCancellation.collectAsState().value) {\n                MyIcons.stop\n            } else {\n                null\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun PartInfoButton(\n    showing: Boolean,\n    onClick: (Boolean) -> Unit,\n) {\n    val partsInfoTitle = Res.string.parts_info.asStringSource()\n    Tooltip(partsInfoTitle) {\n        IconActionButton(\n            onClick = {\n                onClick(!showing)\n            },\n            contentDescription = partsInfoTitle,\n            icon = if (showing) {\n                MyIcons.up\n            } else {\n                MyIcons.down\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun SingleDownloadPageButton(\n    onClick: () -> Unit,\n    text: String,\n    color: Color = LocalContentColor.current,\n    icon: IconSource? = null,\n) {\n    ActionButton(\n        text = text,\n        start = {\n            icon?.let {\n                Row {\n                    MyIcon(it, null, Modifier.size(16.dp))\n                    Spacer(Modifier.width(4.dp))\n                }\n            }\n        },\n        contentPadding = PaddingValues(vertical = 6.dp, horizontal = 12.dp),\n        contentColor = color,\n        onClick = onClick,\n    )\n}\n\n@Composable\nprivate fun CancelButton(\n    cancel: () -> Unit,\n    icon: IconSource?,\n) {\n    SingleDownloadPageButton(\n        {\n            cancel()\n        },\n        icon = icon,\n        text = myStringResource(Res.string.cancel)\n    )\n}\n\n@Composable\nprivate fun ToggleButton(\n    itemState: ProcessingDownloadItemState,\n    toggle: () -> Unit,\n    pause: () -> Unit,\n) {\n    var showPromptOnNonePresumablePause by remember(itemState.status is DownloadJobStatus.IsActive) {\n        mutableStateOf(false)\n    }\n\n    val isResumeSupported = itemState.supportResume == true\n    val (icon, text) = when {\n        itemState.canBeResumed() -> {\n            MyIcons.resume to Res.string.resume\n        }\n\n        itemState.canBePaused() -> {\n            MyIcons.pause to Res.string.pause\n        }\n\n        else -> return\n    }\n\n    Box {\n        SingleDownloadPageButton(\n            {\n                if (isResumeSupported) {\n                    toggle()\n                } else {\n                    if (itemState.status is DownloadJobStatus.IsActive) {\n                        showPromptOnNonePresumablePause = true\n                    } else {\n                        toggle()\n                    }\n                }\n            },\n            icon = icon,\n            text = myStringResource(text),\n            color = if (isResumeSupported) {\n                LocalContentColor.current\n            } else {\n                if (itemState.status is DownloadJobStatus.IsActive) {\n                    myColors.error\n                } else {\n                    LocalContentColor.current\n                }\n            },\n        )\n        if (showPromptOnNonePresumablePause) {\n            val shape = myShapes.defaultRounded\n            val closePopup = {\n                showPromptOnNonePresumablePause = false\n            }\n            Popup(\n                popupPositionProvider = rememberMyComponentRectPositionProvider(\n                    offset = DpOffset.Zero,\n                    anchor = Alignment.TopEnd,\n                    alignment = Alignment.TopStart,\n                ),\n                onDismissRequest = closePopup\n            ) {\n                Column(\n                    Modifier\n                        .clip(shape)\n                        .border(2.dp, myColors.onBackground / 10, shape)\n                        .background(\n                            Brush.linearGradient(\n                                listOf(\n                                    myColors.surface,\n                                    myColors.background,\n                                )\n                            )\n                        )\n                        .padding(16.dp)\n                        .widthIn(max = 140.dp)\n                ) {\n                    Text(buildAnnotatedString {\n                        withStyle(SpanStyle(color = myColors.warning)) {\n                            append(\"${myStringResource(Res.string.warning)}:\\n\")\n                        }\n                        append(myStringResource(Res.string.unsupported_resume_warning))\n                    })\n                    Spacer(Modifier.height(8.dp))\n                    ActionButton(\n                        myStringResource(Res.string.stop_anyway),\n                        onClick = {\n                            closePopup()\n                            pause()\n                        },\n                        contentColor = myColors.error\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderParts(parts: List<UiPart>, modifier: Modifier) {\n    Row(\n        modifier\n            .fillMaxWidth()\n    ) {\n        if (parts.isNotEmpty()) {\n            val sortedParts = remember(parts) {\n                parts.sortedBy {\n                    it.id\n                }\n            }\n            for (p in sortedParts) {\n                val partSpace = p.partSpace\n                if (partSpace <= 0f) continue\n                key(p.id) {\n                    RenderPart(\n                        p,\n                        Modifier\n                            .fillMaxHeight()\n                            .weight(partSpace)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderPart(part: UiPart, modifier: Modifier) {\n    val partProgress = part.percent?.let {\n        it / 100f\n    } ?: 0f\n\n    val foregroundColor = when (part.status) {\n        is PartDownloadStatus.Canceled -> myColors.error\n        PartDownloadStatus.Completed -> myColors.info\n        PartDownloadStatus.IDLE -> myColors.info / 25\n        PartDownloadStatus.ReceivingData -> myColors.success\n        PartDownloadStatus.Connecting -> myColors.warning\n    }\n    Row(modifier) {\n        Box(\n            Modifier\n                .fillMaxSize()\n        ) {\n            Box(\n                Modifier\n                    .align(Alignment.CenterStart)\n                    .fillMaxWidth(partProgress)\n                    .fillMaxHeight()\n                    .background(foregroundColor)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/ShowDownloadDialogs.kt",
    "content": "package com.abdownloadmanager.desktop.pages.singleDownloadPage\n\nimport com.abdownloadmanager.desktop.DesktopDownloadDialogManager\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowIcon\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.FrameWindowScope\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.WindowState\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.singledownloadpage.BaseSingleDownloadComponent\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.Dimension\nimport java.awt.Taskbar\nimport java.awt.Window\n\n@Composable\nprivate fun getDownloadTitle(itemState: IDownloadItemState): String {\n    return buildString {\n        if (itemState is ProcessingDownloadItemState && itemState.percent != null) {\n            append(\"${itemState.percent}%\")\n            append(\" \")\n        }\n        append(itemState.name)\n    }\n}\n\nval LocalSingleDownloadPageSizing =\n    compositionLocalOf<SingleProgressDownloadPageSizing> { error(\"LocalSingleBoxSizing not provided\") }\n\n@Stable\nclass SingleProgressDownloadPageSizing {\n    var resizingPartInfo by mutableStateOf(false)\n    var partInfoHeight by mutableStateOf(150.dp)\n}\n\n@Composable\nfun ShowDownloadDialogs(component: DesktopDownloadDialogManager) {\n    val openedDownloadDialogs = component.openedDownloadDialogs.collectAsState().value\n    for (singleDownloadComponent in openedDownloadDialogs) {\n        key(singleDownloadComponent.downloadId) {\n            ShowDownloadDialog(singleDownloadComponent)\n        }\n    }\n}\n\n@Composable\nprivate fun ShowDownloadDialog(singleDownloadComponent: DesktopSingleDownloadComponent) {\n    val itemState by singleDownloadComponent.itemStateFlow.collectAsState()\n    itemState?.let {\n        when (it) {\n            is CompletedDownloadItemState -> {\n                CompletedWindow(\n                    singleDownloadComponent,\n                    it,\n                )\n            }\n\n            is ProcessingDownloadItemState -> {\n                ProgressWindow(\n                    singleDownloadComponent = singleDownloadComponent,\n                    itemState = it,\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun FrameWindowScope.CommonContent(\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n    state: WindowState,\n    itemState: IDownloadItemState,\n) {\n    HandleEffects(singleDownloadComponent) {\n        when (it) {\n            is BaseSingleDownloadComponent.Effects.Platform -> {\n                it as DesktopSingleDownloadComponent.Effects\n                when (it) {\n                    DesktopSingleDownloadComponent.Effects.BringToFront -> {\n                        state.isMinimized = false\n                        window.toFront()\n                    }\n                }\n            }\n        }\n    }\n    WindowTitle(getDownloadTitle(itemState))\n    WindowIcon(MyIcons.appIcon)\n    UpdateTaskBar(window, itemState)\n}\n\n@Composable\nprivate fun CompletedWindow(\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n    itemState: CompletedDownloadItemState,\n) {\n    val onRequestClose = {\n        singleDownloadComponent.close()\n    }\n    val defaultHeight = 160f\n    val defaultWidth = 450f\n    val uiScale = LocalUiScale.current\n    val state = rememberWindowState(\n        size = DpSize(\n            height = defaultHeight.dp,\n            width = defaultWidth.dp\n        ).applyUiScale(uiScale),\n        position = WindowPosition(Alignment.Center)\n    )\n    CustomWindow(\n        state = state,\n        onRequestToggleMaximize = null,\n        resizable = false,\n        alwaysOnTop = true,\n        onCloseRequest = onRequestClose,\n    ) {\n        CommonContent(\n            singleDownloadComponent = singleDownloadComponent,\n            state = state,\n            itemState = itemState,\n        )\n        LaunchedEffect(Unit) {\n            window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt())\n        }\n        var h = defaultHeight\n        var w = defaultWidth\n        LaunchedEffect(w, h) {\n            state.size = DpSize(\n                width = w.dp,\n                height = h.dp\n            ).applyUiScale(uiScale)\n        }\n        CompletedDownloadPage(\n            singleDownloadComponent,\n            itemState,\n        )\n    }\n}\n\n@Composable\nprivate fun ProgressWindow(\n    singleDownloadComponent: DesktopSingleDownloadComponent,\n    itemState: ProcessingDownloadItemState,\n) {\n    val onRequestClose = {\n        singleDownloadComponent.close()\n    }\n    val uiScale = LocalUiScale.current\n    val defaultHeight = 290f.applyUiScale(uiScale)\n    val defaultWidth = 450f.applyUiScale(uiScale)\n\n    val showPartInfo by singleDownloadComponent.showPartInfo.collectAsState()\n    val singleDownloadPageSizing = remember(showPartInfo) { SingleProgressDownloadPageSizing() }\n    var h = defaultHeight\n    var w = defaultWidth\n    if (showPartInfo) {\n        h += singleDownloadPageSizing.partInfoHeight.value\n            .applyUiScale(uiScale)\n    }\n    val state = rememberWindowState(\n        height = h.dp,\n        width = w.dp,\n        position = WindowPosition(Alignment.Center)\n    )\n    CustomWindow(\n        state = state,\n        onRequestToggleMaximize = null,\n        resizable = false,\n        onCloseRequest = onRequestClose,\n    ) {\n        CommonContent(\n            singleDownloadComponent = singleDownloadComponent,\n            state = state,\n            itemState = itemState,\n        )\n        LaunchedEffect(Unit) {\n            window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt())\n        }\n        LaunchedEffect(w, h) {\n            state.size = DpSize(\n                width = w.dp,\n                height = h.dp\n            )\n        }\n        CompositionLocalProvider(\n            LocalSingleDownloadPageSizing provides singleDownloadPageSizing\n        ) {\n            ProgressDownloadPage(\n                singleDownloadComponent,\n                itemState,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UpdateTaskBar(\n    window: Window,\n    state: IDownloadItemState,\n) {\n    val percent = state.getPercent()\n    val status = state.statusOrFinished()\n    LaunchedEffect(percent, status, window) {\n        if (!Taskbar.isTaskbarSupported()) return@LaunchedEffect\n        runCatching {\n            val taskbar = Taskbar.getTaskbar()\n            percent?.let {\n                taskbar.setWindowProgressValue(\n                    window,\n                    percent\n                )\n            }\n            taskbar.setWindowProgressState(\n                window,\n                when (status) {\n                    is DownloadJobStatus.Canceled -> {\n                        if (ExceptionUtils.isNormalCancellation(status.e)) {\n                            Taskbar.State.PAUSED\n                        } else {\n                            Taskbar.State.ERROR\n                        }\n                    }\n\n                    DownloadJobStatus.Downloading,\n                    is DownloadJobStatus.Retrying -> {\n                        if (percent != null) {\n                            Taskbar.State.NORMAL\n                        } else {\n                            Taskbar.State.INDETERMINATE\n                        }\n                    }\n\n                    DownloadJobStatus.Resuming -> {\n                        Taskbar.State.INDETERMINATE\n                    }\n\n                    DownloadJobStatus.Finished -> {\n                        Taskbar.State.OFF\n                    }\n\n                    DownloadJobStatus.IDLE -> {\n                        Taskbar.State.OFF\n                    }\n\n                    is DownloadJobStatus.PreparingFile -> {\n                        Taskbar.State.INDETERMINATE\n                    }\n                }\n            )\n        }\n    }\n}\n\n\nprivate fun IDownloadItemState.getPercent(): Int? {\n    return when (this) {\n        is CompletedDownloadItemState -> 100\n        is ProcessingDownloadItemState -> percent\n    }\n}\n\nprivate fun IDownloadItemState.isActive(): Boolean {\n    return when (this) {\n        is CompletedDownloadItemState -> false\n        is ProcessingDownloadItemState -> status is DownloadJobStatus.IsActive\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/singleDownloadPage/SingleDownloadPageStateToPersist.kt",
    "content": "package com.abdownloadmanager.desktop.pages.singleDownloadPage\n\nimport arrow.optics.Lens\nimport arrow.optics.optics\nimport ir.amirab.util.config.MapConfig\nimport ir.amirab.util.config.booleanKeyOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.serialization.Serializable\nimport org.koin.core.component.KoinComponent\n\ninterface SingleDownloadPageStateStorage {\n    val singleDownloadPageState: MutableStateFlow<SingleDownloadPageStateToPersist>\n}\n@optics\n@Serializable\ndata class SingleDownloadPageStateToPersist(\n    val showPartInfo: Boolean = false,\n) {\n    class ConfigLens(prefix: String) : Lens<MapConfig, SingleDownloadPageStateToPersist>,\n        KoinComponent {\n        class Keys(prefix: String) {\n            val showPartInfo = booleanKeyOf(\"${prefix}showPartInfo\")\n        }\n\n        private val keys = Keys(prefix)\n        override fun get(source: MapConfig): SingleDownloadPageStateToPersist {\n            val default by lazy { SingleDownloadPageStateToPersist() }\n            return SingleDownloadPageStateToPersist(\n                showPartInfo = source.get(keys.showPartInfo) ?: default.showPartInfo,\n            )\n        }\n\n        override fun set(source: MapConfig, focus: SingleDownloadPageStateToPersist): MapConfig {\n            source.put(keys.showPartInfo, focus.showPartInfo)\n            return source\n        }\n    }\n\n    companion object\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/updater/NewUpdatePage.kt",
    "content": "package com.abdownloadmanager.desktop.pages.updater\n\nimport androidx.compose.animation.animateColor\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.*\nimport com.abdownloadmanager.desktop.window.custom.WindowIcon\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.BlurredEdgeTreatment\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.theme.myMarkdownColors\nimport com.abdownloadmanager.shared.ui.theme.myMarkdownTypography\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.LocalMultiplatformScrollbarStyle\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport io.github.z4kn4fein.semver.Version\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport com.abdownloadmanager.shared.util.ui.needScroll\nimport com.mikepenz.markdown.compose.Markdown\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun NewUpdatePage(\n    newVersionInfo: UpdateInfo,\n    currentVersion: Version,\n    update: () -> Unit,\n    cancel: () -> Unit,\n) {\n    WindowTitle(myStringResource(Res.string.update_updater))\n    WindowIcon(MyIcons.refresh)\n    val contentHorizontalPadding = 16.dp\n    Box {\n        BackgroundEffects()\n        Column(\n            Modifier\n                .fillMaxSize()\n        ) {\n            Column(\n                Modifier\n                    .padding(\n                        top = 8.dp\n                    )\n                    .weight(1f)\n            ) {\n                Column(\n                    Modifier\n                        .padding(horizontal = contentHorizontalPadding)\n                ) {\n                    Row(verticalAlignment = Alignment.CenterVertically) {\n                        Text(\n                            text = myStringResource(Res.string.update_available),\n                            fontSize = myTextSizes.xl,\n                            fontWeight = FontWeight.Bold\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        Text(\n                            text = myStringResource(\n                                Res.string.version_n, Res.string.version_n_createArgs(\n                                    newVersionInfo.version.toString()\n                                )\n                            ),\n                            fontSize = myTextSizes.xl,\n                            fontWeight = FontWeight.Bold,\n                            color = myColors.success,\n                        )\n                    }\n                    Spacer(Modifier.height(8.dp))\n                    Text(\n                        text = myStringResource(Res.string.update_available_suggest_to_to_update),\n                        fontSize = myTextSizes.base,\n                    )\n                    Spacer(Modifier.height(8.dp))\n                }\n                RenderChangeLog(\n                    Modifier\n                        .fillMaxWidth()\n                        .weight(1f),\n                    newVersionInfo.changeLog,\n                    horizontalPadding = contentHorizontalPadding,\n                )\n            }\n            Actions(\n                Modifier.fillMaxWidth(),\n                update,\n                cancel\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun BoxScope.BackgroundEffects() {\n    Box(\n        Modifier\n            .align(Alignment.TopCenter)\n            .offset(y = (-148).dp)\n            .fillMaxWidth(0.5f)\n            .height(200.dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.primary / 0.15f\n            )\n    )\n    Box(\n        Modifier\n            .align(Alignment.BottomEnd)\n            .size(180.dp)\n            .offset(x = 32.dp, y = (-32).dp)\n            .blur(\n                56.dp,\n                edgeTreatment = BlurredEdgeTreatment.Unbounded\n            )\n            .clip(CircleShape)\n            .background(\n                myColors.secondary / 0.15f\n            )\n    )\n}\n\n@Composable\nfun Actions(modifier: Modifier, update: () -> Unit, cancel: () -> Unit) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp)\n                .padding(vertical = 16.dp),\n            horizontalArrangement = Arrangement.End\n        ) {\n            UpdateButton(Modifier, update)\n            Spacer(Modifier.width(8.dp))\n            CancelButton(Modifier, cancel)\n        }\n    }\n}\n\n@Composable\nfun UpdateButton(\n    modifier: Modifier,\n    update: () -> Unit,\n) {\n    val backgroundColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 30\n        }\n    )\n    val borderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors\n    )\n    val disabledBorderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 50\n        }\n    )\n    ActionButton(\n        text = myStringResource(Res.string.update),\n        modifier = modifier,\n        onClick = update,\n        backgroundColor = backgroundColor,\n        disabledBackgroundColor = backgroundColor,\n        borderColor = borderColor,\n        disabledBorderColor = disabledBorderColor,\n    )\n}\n\n@Composable\nfun CancelButton(\n    modifier: Modifier,\n    cancel: () -> Unit,\n) {\n    ActionButton(\n        text = myStringResource(Res.string.cancel),\n        modifier = modifier,\n        onClick = cancel,\n    )\n}\n\n@Composable\nprivate fun RenderChangeLog(\n    modifier: Modifier,\n    changeLog: String,\n    horizontalPadding: Dp,\n) {\n    val trimmedChangelog = remember(changeLog) {\n        changeLog\n            .lines()\n            .filterNot { it.isBlank() }\n            .joinToString(\"\\n\")\n    }\n    Column(modifier) {\n        Text(\n            text = myStringResource(Res.string.update_release_notes),\n            modifier = Modifier.padding(horizontal = horizontalPadding),\n            fontWeight = FontWeight.Bold,\n            fontSize = myTextSizes.lg,\n        )\n        Spacer(Modifier.height(8.dp))\n        Column(\n            Modifier.background(myColors.surface / 75)\n        ) {\n            val transition = rememberInfiniteTransition()\n            val topBorderColors = listOf(\n                myColors.primary to myColors.secondaryVariant,\n                myColors.secondary to myColors.primaryVariant,\n                myColors.primaryVariant to myColors.secondary,\n                myColors.secondaryVariant to myColors.primary,\n            )\n            val animatedTopBorderColors = topBorderColors.map {\n                transition.animateColor(\n                    it.first, it.second, infiniteRepeatable(\n                        animation = tween(durationMillis = 3000, easing = LinearEasing),\n                        repeatMode = RepeatMode.Reverse\n                    )\n                )\n            }\n            Spacer(\n                Modifier.fillMaxWidth()\n                    .height(2.dp)\n                    .background(\n                        Brush.horizontalGradient(\n                            animatedTopBorderColors.map { it.value }\n                        )\n                    )\n            )\n            val scrollState = rememberScrollState()\n            val scrollbarAdapter = rememberScrollbarAdapter(scrollState)\n            Row(\n                Modifier\n                    .fillMaxSize()\n            ) {\n                Markdown(\n                    modifier = Modifier\n                        .weight(1f)\n                        .verticalScroll(scrollState)\n                        .padding(\n                            horizontal = horizontalPadding,\n                            vertical = 8.dp\n                        ),\n                    content = trimmedChangelog,\n                    colors = myMarkdownColors(),\n                    typography = myMarkdownTypography()\n                )\n                if (scrollbarAdapter.needScroll()) {\n                    MultiplatformVerticalScrollbar(\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .padding(\n                                vertical = 4.dp,\n                                horizontal = 4.dp\n                            ),\n                        adapter = scrollbarAdapter\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderKeyValue(\n    key: String,\n    value: String,\n) {\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        WithContentAlpha(0.50f) {\n            Text(\n                key,\n                fontSize = myTextSizes.base,\n                maxLines = 1,\n            )\n        }\n        Spacer(Modifier.width(8.dp))\n        Text(\n            value,\n            fontSize = myTextSizes.base,\n            maxLines = 1,\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/updater/UpdaterDialog.kt",
    "content": "package com.abdownloadmanager.desktop.pages.updater\n\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.pages.updater.RenderUpdateNotifications\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport ir.amirab.util.desktop.screen.applyUiScale\n\n@Composable\nfun ShowUpdaterDialog(updaterComponent: UpdateComponent) {\n    val showUpdate = updaterComponent.showNewUpdate.collectAsState().value\n    val newVersion = updaterComponent.newVersionData.collectAsState().value\n    val closeUpdatePage = {\n        updaterComponent.requestClose()\n    }\n    RenderUpdateNotifications(updaterComponent)\n    if (showUpdate && newVersion != null) {\n        val uiScale = LocalUiScale.current\n        CustomWindow(\n            state = rememberWindowState(\n                size = DpSize(500.dp, 400.dp).applyUiScale(uiScale),\n                position = WindowPosition.Aligned(Alignment.Center)\n            ),\n            onCloseRequest = closeUpdatePage,\n        ) {\n            NewUpdatePage(\n                newVersionInfo = newVersion,\n                currentVersion = updaterComponent.currentVersion,\n                cancel = closeUpdatePage,\n                update = {\n                    updaterComponent.performUpdate()\n                    closeUpdatePage()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/repository/AppRepository.kt",
    "content": "package com.abdownloadmanager.desktop.repository\n\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.DownloadSettings\nimport com.abdownloadmanager.integration.Integration\nimport com.abdownloadmanager.integration.IntegrationResult\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.util.autoremove.RemovedDownloadsFromDiskTracker\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\n\nclass AppRepository(\n    scope: CoroutineScope,\n    appSettings: BaseAppSettingsStorage,\n    proxyManager: ProxyManager,\n    downloadSystem: DownloadSystem,\n    downloadSettings: DownloadSettings,\n    removedDownloadsFromDiskTracker: RemovedDownloadsFromDiskTracker,\n    categoryManager: CategoryManager,\n    private val integration: Integration,\n) : BaseAppRepository(\n    scope = scope,\n    appSettings = appSettings,\n    proxyManager = proxyManager,\n    downloadSystem = downloadSystem,\n    downloadSettings = downloadSettings,\n    removedDownloadsFromDiskTracker = removedDownloadsFromDiskTracker,\n    categoryManager = categoryManager,\n) {\n    init {\n        integrationPort\n            .debounce(500)\n            .onEach {\n                if (integrationEnabled.value) {\n                    integration.enable(it)\n                }\n            }.launchIn(scope)\n        integrationEnabled\n            .debounce(500)\n            .onEach { isEnabled ->\n                if (isEnabled) {\n                    integration.enable(integrationPort.value)\n                } else {\n                    integration.disable()\n                }\n            }.launchIn(scope)\n        integration.integrationStatus.onEach { result ->\n            //if there is an error in connection disable integration\n            if (result is IntegrationResult.Fail) {\n                integrationEnabled.update { false }\n            }\n        }.launchIn(scope)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/AppSettingsStorage.kt",
    "content": "package com.abdownloadmanager.desktop.storage\n\nimport androidx.datastore.core.DataStore\nimport arrow.optics.Lens\nimport arrow.optics.optics\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.IAppSettingsModel\nimport com.abdownloadmanager.shared.storage.SupportedSizeUnits\nimport com.abdownloadmanager.shared.ui.theme.ThemeSettingsStorage\nimport com.abdownloadmanager.shared.util.downloadlocation.PlatformDownloadLocationProvider\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByMapConfig\nimport com.abdownloadmanager.shared.util.SystemDownloadLocationProvider\nimport com.abdownloadmanager.shared.util.ui.theme.DEFAULT_UI_SCALE\nimport ir.amirab.util.compose.localizationmanager.LanguageStorage\nimport ir.amirab.util.config.*\nimport ir.amirab.util.enumValueOrNull\nimport kotlinx.serialization.Serializable\nimport org.koin.core.component.KoinComponent\n\n@optics([arrow.optics.OpticsTarget.LENS])\n@Serializable\ndata class AppSettingsModel(\n    override val theme: String = \"dark\",\n    override val defaultDarkTheme: String = \"dark\",\n    override val defaultLightTheme: String = \"light\",\n    override val language: String? = null,\n    override val font: String? = null,\n    override val uiScale: Float? = null,\n    val mergeTopBarWithTitleBar: Boolean = true,\n    val useNativeMenuBar: Boolean = false,\n    override val showIconLabels: Boolean = true,\n    override val useRelativeDateTime: Boolean = true,\n    val useSystemTray: Boolean = true,\n    override val threadCount: Int = 8,\n    override val maxConcurrentDownloads: Int = 0,\n    override val maxDownloadRetryCount: Int = 3,\n    override val dynamicPartCreation: Boolean = true,\n    override val useServerLastModifiedTime: Boolean = false,\n    override val appendExtensionToIncompleteDownloads: Boolean = false,\n    override val useSparseFileAllocation: Boolean = true,\n    override val useAverageSpeed: Boolean = true,\n    override val showDownloadProgressDialog: Boolean = true,\n    override val showDownloadCompletionDialog: Boolean = true,\n    override val speedLimit: Long = 0,\n    override val autoStartOnBoot: Boolean = true,\n    override val notificationSound: Boolean = true,\n    override val defaultDownloadFolder: String = PlatformDownloadLocationProvider\n        .instance.getDownloadLocation()\n        .resolve(\"ABDM\")\n        .canonicalFile.absolutePath,\n    override val browserIntegrationEnabled: Boolean = true,\n    override val browserIntegrationPort: Int = 15151,\n    override val trackDeletedFilesOnDisk: Boolean = false,\n    override val deletePartialFileOnDownloadCancellation: Boolean = false,\n    override val sizeUnit: SupportedSizeUnits = SupportedSizeUnits.BinaryBytes,\n    override val speedUnit: SupportedSizeUnits = SupportedSizeUnits.BinaryBytes,\n    override val ignoreSSLCertificates: Boolean = false,\n    override val useCategoryByDefault: Boolean = true,\n    override val userAgent: String = \"\",\n) : IAppSettingsModel {\n    companion object {\n        val default: AppSettingsModel get() = AppSettingsModel()\n    }\n\n    object ConfigLens : Lens<MapConfig, AppSettingsModel>, KoinComponent {\n        object Keys {\n            val theme = stringKeyOf(\"theme\")\n            val defaultDarkTheme = stringKeyOf(\"defaultDarkTheme\")\n            val defaultLightTheme = stringKeyOf(\"defaultLightTheme\")\n            val language = stringKeyOf(\"language\")\n            val font = stringKeyOf(\"font\")\n            val uiScale = floatKeyOf(\"uiScale\")\n            val mergeTopBarWithTitleBar = booleanKeyOf(\"mergeTopBarWithTitleBar\")\n            val useNativeMenuBar = booleanKeyOf(\"useNativeMenuBar\")\n            val showIconLabels = booleanKeyOf(\"showIconLabels\")\n            val useRelativeDateTime = booleanKeyOf(\"useRelativeDateTime\")\n            val useSystemTray = booleanKeyOf(\"useSystemTray\")\n            val threadCount = intKeyOf(\"threadCount\")\n            val maxConcurrentDownloads = intKeyOf(\"maxConcurrentDownloads\")\n            val maxDownloadRetryCount = intKeyOf(\"maxDownloadRetryCount\")\n            val dynamicPartCreation = booleanKeyOf(\"dynamicPartCreation\")\n            val useServerLastModifiedTime = booleanKeyOf(\"useServerLastModifiedTime\")\n            val appendExtensionToIncompleteDownloads = booleanKeyOf(\"appendExtensionToIncompleteDownloads\")\n            val useSparseFileAllocation = booleanKeyOf(\"useSparseFileAllocation\")\n            val useAverageSpeed = booleanKeyOf(\"useAverageSpeed\")\n            val showDownloadProgressDialog = booleanKeyOf(\"showDownloadProgressDialog\")\n            val showDownloadCompletionDialog = booleanKeyOf(\"showDownloadCompletionDialog\")\n            val speedLimit = longKeyOf(\"speedLimit\")\n            val autoStartOnBoot = booleanKeyOf(\"autoStartOnBoot\")\n            val notificationSound = booleanKeyOf(\"notificationSound\")\n            val defaultDownloadFolder = stringKeyOf(\"defaultDownloadFolder\")\n            val browserIntegrationEnabled = booleanKeyOf(\"browserIntegrationEnabled\")\n            val browserIntegrationPort = intKeyOf(\"browserIntegrationPort\")\n            val trackDeletedFilesOnDisk = booleanKeyOf(\"trackDeletedFilesOnDisk\")\n            val deletePartialFileOnDownloadCancellation = booleanKeyOf(\"deletePartialFileOnDownloadCancellation\")\n            val sizeUnit = stringKeyOf(\"sizeUnit\")\n            val speedUnit = stringKeyOf(\"speedUnit\")\n            val ignoreSSLCertificates = booleanKeyOf(\"ignoreSSLCertificates\")\n            val useCategoryByDefault = booleanKeyOf(\"useCategoryByDefault\")\n            val userAgent = stringKeyOf(\"userAgent\")\n        }\n\n\n        override fun get(source: MapConfig): AppSettingsModel {\n            val default by lazy { AppSettingsModel.default }\n            // for nullable types we don't get default value\n            return AppSettingsModel(\n                theme = source.get(Keys.theme) ?: default.theme,\n                defaultDarkTheme = source.get(Keys.defaultDarkTheme) ?: default.defaultDarkTheme,\n                defaultLightTheme = source.get(Keys.defaultLightTheme) ?: default.defaultLightTheme,\n                language = source.get(Keys.language),\n                font = source.get(Keys.font),\n                uiScale = source.get(Keys.uiScale),\n                mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,\n                useNativeMenuBar = source.get(Keys.useNativeMenuBar) ?: default.useNativeMenuBar,\n                showIconLabels = source.get(Keys.showIconLabels) ?: default.showIconLabels,\n                useRelativeDateTime = source.get(Keys.useRelativeDateTime) ?: default.useRelativeDateTime,\n                useSystemTray = source.get(Keys.useSystemTray) ?: default.useSystemTray,\n                threadCount = source.get(Keys.threadCount) ?: default.threadCount,\n                maxConcurrentDownloads = source.get(Keys.maxConcurrentDownloads) ?: default.maxConcurrentDownloads,\n                maxDownloadRetryCount = source.get(Keys.maxDownloadRetryCount) ?: default.maxDownloadRetryCount,\n                dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,\n                useServerLastModifiedTime = source.get(Keys.useServerLastModifiedTime)\n                    ?: default.useServerLastModifiedTime,\n                appendExtensionToIncompleteDownloads = source.get(Keys.appendExtensionToIncompleteDownloads)\n                    ?: default.appendExtensionToIncompleteDownloads,\n                useSparseFileAllocation = source.get(Keys.useSparseFileAllocation) ?: default.useSparseFileAllocation,\n                useAverageSpeed = source.get(Keys.useAverageSpeed) ?: default.useAverageSpeed,\n                showDownloadProgressDialog = source.get(Keys.showDownloadProgressDialog)\n                    ?: default.showDownloadProgressDialog,\n                showDownloadCompletionDialog = source.get(Keys.showDownloadCompletionDialog)\n                    ?: default.showDownloadCompletionDialog,\n                speedLimit = source.get(Keys.speedLimit) ?: default.speedLimit,\n                autoStartOnBoot = source.get(Keys.autoStartOnBoot) ?: default.autoStartOnBoot,\n                notificationSound = source.get(Keys.notificationSound) ?: default.notificationSound,\n                defaultDownloadFolder = source.get(Keys.defaultDownloadFolder) ?: default.defaultDownloadFolder,\n                browserIntegrationEnabled = source.get(Keys.browserIntegrationEnabled)\n                    ?: default.browserIntegrationEnabled,\n                browserIntegrationPort = source.get(Keys.browserIntegrationPort) ?: default.browserIntegrationPort,\n                trackDeletedFilesOnDisk = source.get(Keys.trackDeletedFilesOnDisk) ?: default.trackDeletedFilesOnDisk,\n                deletePartialFileOnDownloadCancellation = source.get(Keys.deletePartialFileOnDownloadCancellation)\n                    ?: default.deletePartialFileOnDownloadCancellation,\n                sizeUnit = source.get(Keys.sizeUnit)?.enumValueOrNull<SupportedSizeUnits>() ?: default.sizeUnit,\n                speedUnit = source.get(Keys.speedUnit)?.enumValueOrNull<SupportedSizeUnits>() ?: default.speedUnit,\n                ignoreSSLCertificates = source.get(Keys.ignoreSSLCertificates) ?: default.ignoreSSLCertificates,\n                useCategoryByDefault = source.get(Keys.useCategoryByDefault) ?: default.useCategoryByDefault,\n                userAgent = source.get(Keys.userAgent) ?: default.userAgent,\n            )\n        }\n\n        override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {\n            return source.apply {\n                put(Keys.theme, focus.theme)\n                put(Keys.defaultDarkTheme, focus.defaultDarkTheme)\n                put(Keys.defaultLightTheme, focus.defaultLightTheme)\n                putNullable(Keys.language, focus.language)\n                putNullable(Keys.font, focus.font)\n                putNullable(Keys.uiScale, focus.uiScale)\n                put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)\n                put(Keys.useNativeMenuBar, focus.useNativeMenuBar)\n                put(Keys.showIconLabels, focus.showIconLabels)\n                put(Keys.useRelativeDateTime, focus.useRelativeDateTime)\n                put(Keys.useSystemTray, focus.useSystemTray)\n                put(Keys.threadCount, focus.threadCount)\n                put(Keys.maxConcurrentDownloads, focus.maxConcurrentDownloads)\n                put(Keys.maxDownloadRetryCount, focus.maxDownloadRetryCount)\n                put(Keys.dynamicPartCreation, focus.dynamicPartCreation)\n                put(Keys.useServerLastModifiedTime, focus.useServerLastModifiedTime)\n                put(Keys.appendExtensionToIncompleteDownloads, focus.appendExtensionToIncompleteDownloads)\n                put(Keys.useSparseFileAllocation, focus.useSparseFileAllocation)\n                put(Keys.useAverageSpeed, focus.useAverageSpeed)\n                put(Keys.showDownloadProgressDialog, focus.showDownloadProgressDialog)\n                put(Keys.showDownloadCompletionDialog, focus.showDownloadCompletionDialog)\n                put(Keys.speedLimit, focus.speedLimit)\n                put(Keys.autoStartOnBoot, focus.autoStartOnBoot)\n                put(Keys.notificationSound, focus.notificationSound)\n                put(Keys.defaultDownloadFolder, focus.defaultDownloadFolder)\n                put(Keys.browserIntegrationEnabled, focus.browserIntegrationEnabled)\n                put(Keys.browserIntegrationPort, focus.browserIntegrationPort)\n                put(Keys.trackDeletedFilesOnDisk, focus.trackDeletedFilesOnDisk)\n                put(Keys.deletePartialFileOnDownloadCancellation, focus.deletePartialFileOnDownloadCancellation)\n                put(Keys.sizeUnit, focus.sizeUnit.name)\n                put(Keys.speedUnit, focus.speedUnit.name)\n                put(Keys.ignoreSSLCertificates, focus.ignoreSSLCertificates)\n                put(Keys.useCategoryByDefault, focus.useCategoryByDefault)\n                put(Keys.userAgent, focus.userAgent)\n            }\n        }\n    }\n}\n\nprivate val fontLens: Lens<AppSettingsModel, String?>\n    get() = Lens(\n        get = {\n            it.font\n        },\n        set = { s, f ->\n            s.copy(font = f)\n        }\n    )\n\n// use null for default scale!\nprivate val uiScaleLens: Lens<AppSettingsModel, Float>\n    get() = Lens(\n        get = {\n            it.uiScale ?: DEFAULT_UI_SCALE\n        },\n        set = { s, f ->\n            s.copy(uiScale = f.takeIf { it != DEFAULT_UI_SCALE })\n        }\n    )\nprivate val languageLens: Lens<AppSettingsModel, String?>\n    get() = Lens(\n        get = {\n            it.language\n        },\n        set = { s, f ->\n            s.copy(language = f)\n        }\n    )\n\nclass AppSettingsStorage(\n    settings: DataStore<MapConfig>,\n) : BaseAppSettingsStorage,\n    ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {\n    override val theme = from(AppSettingsModel.theme)\n    override val defaultDarkTheme = from(AppSettingsModel.defaultDarkTheme)\n    override val defaultLightTheme = from(AppSettingsModel.defaultLightTheme)\n\n    override val selectedLanguage = from(languageLens)\n    override val font = from(fontLens)\n    override val uiScale = from(uiScaleLens)\n    val mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)\n    val useNativeMenuBar = from(AppSettingsModel.useNativeMenuBar)\n    override val showIconLabels = from(AppSettingsModel.showIconLabels)\n    override val useRelativeDateTime = from(AppSettingsModel.useRelativeDateTime)\n    val useSystemTray = from(AppSettingsModel.useSystemTray)\n    override val threadCount = from(AppSettingsModel.threadCount)\n    override val maxConcurrentDownloads = from(AppSettingsModel.maxConcurrentDownloads)\n    override val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)\n    override val useServerLastModifiedTime = from(AppSettingsModel.useServerLastModifiedTime)\n    override val appendExtensionToIncompleteDownloads = from(AppSettingsModel.appendExtensionToIncompleteDownloads)\n    override val useSparseFileAllocation = from(AppSettingsModel.useSparseFileAllocation)\n    override val useAverageSpeed = from(AppSettingsModel.useAverageSpeed)\n    override val maxDownloadRetryCount = from(AppSettingsModel.maxDownloadRetryCount)\n    override val showDownloadProgressDialog = from(AppSettingsModel.showDownloadProgressDialog)\n    override val showDownloadCompletionDialog = from(AppSettingsModel.showDownloadCompletionDialog)\n    override val speedLimit = from(AppSettingsModel.speedLimit)\n    override val autoStartOnBoot = from(AppSettingsModel.autoStartOnBoot)\n    override val notificationSound = from(AppSettingsModel.notificationSound)\n    override val defaultDownloadFolder = from(AppSettingsModel.defaultDownloadFolder)\n    override val browserIntegrationEnabled = from(AppSettingsModel.browserIntegrationEnabled)\n    override val browserIntegrationPort = from(AppSettingsModel.browserIntegrationPort)\n    override val trackDeletedFilesOnDisk = from(AppSettingsModel.trackDeletedFilesOnDisk)\n    override val deletePartialFileOnDownloadCancellation =\n        from(AppSettingsModel.deletePartialFileOnDownloadCancellation)\n    override val sizeUnit = from(AppSettingsModel.sizeUnit)\n    override val speedUnit = from(AppSettingsModel.speedUnit)\n    override val ignoreSSLCertificates = from(AppSettingsModel.ignoreSSLCertificates)\n    override val useCategoryByDefault = from(AppSettingsModel.useCategoryByDefault)\n    override val userAgent = from(AppSettingsModel.userAgent)\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/DesktopDefinedPaths.kt",
    "content": "package com.abdownloadmanager.desktop.storage\n\nimport com.abdownloadmanager.shared.util.DefinedPaths\nimport okio.Path\nimport java.io.File\n\nclass DesktopDefinedPaths(\n    dataDir: Path\n) : DefinedPaths(\n    dataDir\n) {\n    val pageStatesStorageFile: Path = configDir.resolve(\"pageStatesStorage.json\")\n    val renderApiFile: Path = optionsDir.resolve(\"renderApi.txt\")\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/DesktopExtraDownloadItemSettings.kt",
    "content": "package com.abdownloadmanager.desktop.storage\n\nimport com.abdownloadmanager.shared.storage.IExtraDownloadItemSettings\nimport ir.amirab.util.desktop.poweraction.ContainsPowerActionConfigOnFinish\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DesktopExtraDownloadItemSettings(\n    override val id: Long,\n    val powerActionTypeOnFinish: PowerActionConfig.Type? = null,\n    val powerActionUseForceOnFinish: Boolean = false,\n) : IExtraDownloadItemSettings, ContainsPowerActionConfigOnFinish {\n\n    override fun getPowerActionConfigOnFinish() = powerActionTypeOnFinish?.let {\n        PowerActionConfig(\n            powerActionTypeOnFinish,\n            powerActionUseForceOnFinish,\n        )\n    }\n\n    companion object : IExtraDownloadItemSettings.DataClassDefinitions<DesktopExtraDownloadItemSettings> {\n        override fun createDefault(id: Long) = DesktopExtraDownloadItemSettings(id = id)\n        override val serializer: KSerializer<DesktopExtraDownloadItemSettings> =\n            DesktopExtraDownloadItemSettings.serializer()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/DesktopExtraQueueSettings.kt",
    "content": "package com.abdownloadmanager.desktop.storage\n\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettings\nimport ir.amirab.util.desktop.poweraction.ContainsPowerActionConfigOnFinish\nimport ir.amirab.util.desktop.poweraction.PowerActionConfig\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DesktopExtraQueueSettings(\n    override val id: Long,\n    val powerActionTypeOnFinish: PowerActionConfig.Type? = null,\n    val powerActionUseForceOnFinish: Boolean = false,\n) : IExtraQueueSettings, ContainsPowerActionConfigOnFinish {\n\n    override fun getPowerActionConfigOnFinish() = powerActionTypeOnFinish?.let {\n        PowerActionConfig(\n            powerActionTypeOnFinish,\n            powerActionUseForceOnFinish,\n        )\n    }\n\n    companion object : IExtraQueueSettings.DataClassDefinitions<DesktopExtraQueueSettings> {\n        override fun createDefault(id: Long) = DesktopExtraQueueSettings(id)\n        override val serializer: KSerializer<DesktopExtraQueueSettings> = DesktopExtraQueueSettings.serializer()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/storage/PageStatesStorage.kt",
    "content": "package com.abdownloadmanager.desktop.storage\n\nimport com.abdownloadmanager.desktop.pages.home.HomePageStateToPersist\nimport androidx.datastore.core.DataStore\nimport arrow.optics.Lens\nimport arrow.optics.optics\nimport com.abdownloadmanager.desktop.pages.settings.SettingPageStateToPersist\nimport com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadPageStateStorage\nimport com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadPageStateToPersist\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByMapConfig\nimport ir.amirab.util.config.getDecoded\nimport ir.amirab.util.config.keyOfEncoded\nimport ir.amirab.util.config.putEncoded\nimport ir.amirab.util.config.MapConfig\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\n@optics\n@Serializable\ndata class CommonData(\n    val lastSavedLocations: List<String> = emptyList(),\n) {\n    companion object\n    class ConfigLens(prefix: String) : Lens<MapConfig, CommonData>, KoinComponent {\n        class Keys(prefix: String) {\n            val lastSavedLocations = keyOfEncoded<List<String>>(\"${prefix}lastSavedLocations\")\n        }\n\n        private val json: Json by inject()\n        private val keys = Keys(prefix)\n        override fun get(source: MapConfig): CommonData {\n            return with(json) {\n                CommonData(\n                    lastSavedLocations = source.getDecoded(keys.lastSavedLocations) ?: emptyList()\n                )\n            }\n        }\n\n        override fun set(source: MapConfig, focus: CommonData): MapConfig {\n            return with(json) {\n                source.putEncoded(keys.lastSavedLocations, focus.lastSavedLocations)\n                source\n            }\n        }\n    }\n}\n\n@optics\n@Serializable\ndata class PageStatesModel(\n    val home: HomePageStateToPersist = HomePageStateToPersist(),\n    val settings: SettingPageStateToPersist = SettingPageStateToPersist(),\n    val downloadPage: SingleDownloadPageStateToPersist = SingleDownloadPageStateToPersist(),\n    val global: CommonData = CommonData(),\n) {\n    companion object {\n        val default get() = PageStatesModel()\n    }\n\n    object ConfigLens : Lens<MapConfig, PageStatesModel>, KoinComponent {\n        private val json: Json by inject()\n\n        object Child {\n            val common = CommonData.ConfigLens(\"global.\")\n            val downloadPage = SingleDownloadPageStateToPersist.ConfigLens(\"downloadPage.\")\n            val home = HomePageStateToPersist.ConfigLens(\"home.\")\n            val settings = SettingPageStateToPersist.ConfigLens(\"settings.\")\n        }\n\n        override fun get(source: MapConfig): PageStatesModel {\n            return PageStatesModel(\n                home = Child.home.get(source),\n                settings = Child.settings.get(source),\n                downloadPage = Child.downloadPage.get(source),\n                global = Child.common.get(source)\n            )\n        }\n\n        override fun set(source: MapConfig, focus: PageStatesModel): MapConfig {\n            Child.home.set(source, focus.home)\n            Child.settings.set(source, focus.settings)\n            Child.downloadPage.set(source, focus.downloadPage)\n            Child.common.set(source, focus.global)\n            return source\n        }\n    }\n}\n\nclass PageStatesStorage(\n    settings: DataStore<MapConfig>,\n) : ConfigBaseSettingsByMapConfig<PageStatesModel>(settings, PageStatesModel.ConfigLens),\n    ILastSavedLocationsStorage,\n    SingleDownloadPageStateStorage {\n    override val lastUsedSaveLocations = from(PageStatesModel.global.lastSavedLocations)\n    override val singleDownloadPageState = from(PageStatesModel.downloadPage)\n    val homePageStorage = from(PageStatesModel.home)\n    val settingsPageStorage = from(PageStatesModel.settings)\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt",
    "content": "package com.abdownloadmanager.desktop.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.window.ApplicationScope\nimport androidx.compose.ui.window.application\nimport com.abdownloadmanager.desktop.AppArguments\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.AppEffects\nimport com.abdownloadmanager.desktop.actions.gotoSettingsAction\nimport com.abdownloadmanager.desktop.actions.requestExitAction\nimport com.abdownloadmanager.desktop.actions.showDownloadList\nimport com.abdownloadmanager.desktop.pages.about.ShowAboutDialog\nimport com.abdownloadmanager.desktop.pages.addDownload.ShowAddDownloadDialogs\nimport com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow\nimport com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs\nimport com.abdownloadmanager.desktop.pages.confirmexit.ConfirmExit\nimport com.abdownloadmanager.desktop.pages.credits.translators.ShowTranslators\nimport com.abdownloadmanager.desktop.pages.editdownload.EditDownloadWindow\nimport com.abdownloadmanager.desktop.pages.enterurl.EnterNewDownloadWindow\nimport com.abdownloadmanager.desktop.pages.extenallibs.ShowOpenSourceLibraries\nimport com.abdownloadmanager.desktop.pages.checksum.FileChecksumWindow\nimport com.abdownloadmanager.desktop.pages.home.HomeWindow\nimport com.abdownloadmanager.desktop.pages.newQueue.NewQueueDialog\nimport com.abdownloadmanager.desktop.pages.perhostsettings.PerHostSettingsWindow\nimport com.abdownloadmanager.desktop.pages.queue.QueuesWindow\nimport com.abdownloadmanager.desktop.pages.settings.FontManager\nimport com.abdownloadmanager.desktop.pages.settings.SettingWindow\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.desktop.pages.poweractionalert.PowerActionAlert\nimport com.abdownloadmanager.desktop.pages.singleDownloadPage.ShowDownloadDialogs\nimport com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog\nimport com.abdownloadmanager.desktop.ui.configurable.comon.CommonConfigurableRenderersForDesktop\nimport com.abdownloadmanager.desktop.ui.configurable.platform.PlatformConfigurableRenderersForDesktop\nimport com.abdownloadmanager.desktop.ui.widget.Tray\nimport com.abdownloadmanager.desktop.ui.widget.ShowMessageDialogs\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.desktop.utils.GlobalAppExceptionHandler\nimport com.abdownloadmanager.desktop.utils.ProvideGlobalExceptionHandler\nimport com.abdownloadmanager.desktop.utils.isInDebugMode\nimport com.abdownloadmanager.shared.ui.ProvideCommonSettings\nimport com.abdownloadmanager.shared.ui.ProvideSizeUnits\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRendererRegistry\nimport com.abdownloadmanager.shared.ui.theme.ABDownloaderTheme\nimport com.abdownloadmanager.shared.ui.widget.NotificationManager\nimport com.abdownloadmanager.shared.ui.widget.ProvideLanguageManager\nimport com.abdownloadmanager.shared.ui.widget.ProvideNotificationManager\nimport com.abdownloadmanager.shared.ui.widget.useNotification\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport com.abdownloadmanager.shared.util.ui.ProvideDebugInfo\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.desktop.PlatformDockToggler\nimport ir.amirab.util.desktop.mac.event.MacEventHandler\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isMac\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withTimeout\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.get\nimport org.koin.core.component.inject\n\nobject Ui : KoinComponent {\n    val scope: CoroutineScope by inject()\n    fun boot(\n        appArguments: AppArguments,\n        globalAppExceptionHandler: GlobalAppExceptionHandler,\n    ) {\n        val appComponent: AppComponent = get()\n        val themeManager: ThemeManager = get()\n        val fontManager: FontManager = get()\n        val languageManager: LanguageManager = get()\n        val notificationManager: NotificationManager = get()\n        themeManager.boot()\n        fontManager.boot()\n        languageManager.boot()\n        if (!appArguments.startSilent) {\n            appComponent.openHome()\n        }\n        if (Platform.isMac()) {\n            MacEventHandler.configure(\n                onClickIcon = appComponent::activateHomeIfNotOpen,\n                onAboutClick = {\n                    appComponent.showAboutPage.value = true\n                },\n                onSettingsClick = appComponent::openSettings,\n                onQuit = {\n                    scope.launch { appComponent.requestExitApp() }\n                }\n            )\n        }\n        application {\n            ProvideLocalProviders(\n                languageManager = languageManager,\n                appComponent = appComponent,\n                themeManager = themeManager,\n                fontManager = fontManager,\n                globalAppExceptionHandler = globalAppExceptionHandler,\n                notificationManager = notificationManager,\n            ) {\n                HandleEffectsForApp(appComponent)\n                SystemTray(appComponent)\n                val showHomeSlot =\n                    appComponent.showHomeSlot.collectAsState().value\n                showHomeSlot.child?.instance?.let {\n                    HomeWindow(it, appComponent::closeHome)\n                }\n                val showSettingSlot =\n                    appComponent.showSettingSlot.collectAsState().value\n                showSettingSlot.child?.instance?.let {\n                    SettingWindow(it, appComponent::closeSettings)\n                }\n                val showQueuesSlot =\n                    appComponent.showQueuesSlot.collectAsState().value\n                showQueuesSlot.child?.instance?.let {\n                    QueuesWindow(it)\n                }\n                val batchDownloadSlot =\n                    appComponent.batchDownloadSlot.collectAsState().value\n                batchDownloadSlot.child?.instance?.let {\n                    BatchDownloadWindow(it)\n                }\n                val editDownloadSlot =\n                    appComponent.editDownloadSlot.collectAsState().value\n                editDownloadSlot.child?.instance?.let {\n                    EditDownloadWindow(it)\n                }\n                EnterNewDownloadWindow(appComponent)\n                ShowAddDownloadDialogs(appComponent)\n                ShowDownloadDialogs(appComponent)\n                ShowCategoryDialogs(appComponent)\n                FileChecksumWindow(appComponent)\n                ShowUpdaterDialog(appComponent.updater)\n                ShowAboutDialog(appComponent)\n                NewQueueDialog(appComponent)\n                ShowMessageDialogs(appComponent)\n                ShowOpenSourceLibraries(appComponent)\n                ShowTranslators(appComponent)\n                ConfirmExit(appComponent)\n                PowerActionAlert(appComponent)\n                PerHostSettingsWindow(appComponent)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ProvideLocalProviders(\n    languageManager: LanguageManager,\n    themeManager: ThemeManager,\n    fontManager: FontManager,\n    appComponent: AppComponent,\n    notificationManager: NotificationManager,\n    globalAppExceptionHandler: GlobalAppExceptionHandler,\n    content: @Composable () -> Unit\n) {\n    val theme by themeManager.currentThemeColor.collectAsState()\n    val fontFamily by fontManager.currentFontFamily.collectAsState()\n    val configurableRendererRegistry = remember {\n        ConfigurableRendererRegistry {\n            listOf(\n                PlatformConfigurableRenderersForDesktop,\n                CommonConfigurableRenderersForDesktop,\n            ).forEach {\n                it.getAllRenderers().forEach { (key, renderer) ->\n                    this.register(key, renderer)\n                }\n            }\n        }\n    }\n    ProvideDebugInfo(AppInfo.isInDebugMode()) {\n        ProvideLanguageManager(languageManager) {\n            ProvideCommonSettings(\n                appSettings = appComponent.appSettings,\n                configurableRendererRegistry = configurableRendererRegistry,\n                iconProvider = appComponent.iconFromUriResolver\n            ) {\n                ProvideNotificationManager(notificationManager) {\n                    ABDownloaderTheme(\n                        myColors = theme,\n                        fontFamily = fontFamily,\n                        uiScale = appComponent.uiScale.collectAsState().value\n                    ) {\n                        ProvideGlobalExceptionHandler(globalAppExceptionHandler) {\n                            ProvideSizeUnits(appComponent.appRepository) {\n                                content()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HandleEffectsForApp(appComponent: AppComponent) {\n    val notificationManager = useNotification()\n    val scope = rememberCoroutineScope()\n    HandleEffects(appComponent) {\n        when (it) {\n            is AppEffects.SimpleNotificationNotification -> {\n                scope.launch {\n                    withTimeout(5000) {\n                        notificationManager.showNotification(it.notificationModel)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ApplicationScope.SystemTray(\n    component: AppComponent,\n) {\n    val useSystemTray by component.useSystemTray.collectAsState()\n    if (useSystemTray) {\n        LaunchedEffect(Unit) { PlatformDockToggler.hide() }\n        val menu = remember {\n            buildMenu {\n                +showDownloadList\n                +gotoSettingsAction\n                +requestExitAction\n            }\n        }\n        Tray(\n            icon = MyIcons.appIcon,\n            tooltip = AppInfo.displayName,\n            primaryAction = { showDownloadList.onClick() },\n            menu = menu,\n        )\n    } else {\n        LaunchedEffect(Unit) { PlatformDockToggler.show() }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/DesktopConfigurableRendererUtils.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.Help\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\n\n@Composable\nfun <T> TitleAndDescription(\n    cfg: Configurable<T>,\n    describe: Boolean = true,\n    modifier: Modifier = Modifier.padding(8.dp),\n) {\n    val enabled = isConfigEnabled()\n    Column(\n        modifier.ifThen(!enabled) {\n            alpha(0.5f)\n        }\n    ) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(\n                cfg.title.rememberString(),\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier.weight(1f, false)\n            )\n            if (cfg.description.rememberString().isNotBlank()) {\n                Spacer(Modifier.size(4.dp))\n                Help(\n                    Modifier.align(Alignment.Top),\n                    cfg\n                )\n            }\n        }\n        if (describe) {\n            val value = cfg.backedBy.collectAsState().value\n            val describedStringSource = remember(value) {\n                cfg.describe(value)\n            }\n            val describeContent = describedStringSource.rememberString()\n            if (describeContent.isNotBlank()) {\n                WithContentAlpha(0.75f) {\n                    Text(\n                        describeContent,\n                        fontSize = myTextSizes.base,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun ConfigTemplate(\n    modifier: Modifier,\n    title: @Composable ColumnScope.() -> Unit,\n    value: @Composable ColumnScope.() -> Unit,\n    nestedContent: @Composable ColumnScope.() -> Unit = {},\n) {\n    Column(\n        modifier\n    ) {\n        Row(\n            Modifier\n                .height(IntrinsicSize.Max),\n            horizontalArrangement = Arrangement.Center,\n        ) {\n            Column(\n                Modifier.weight(2f, true),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.Start,\n            ) {\n                title()\n            }\n            Column(\n                Modifier.fillMaxHeight().weight(1f, true),\n                verticalArrangement = Arrangement.Center,\n                horizontalAlignment = Alignment.End,\n            ) {\n                value()\n            }\n        }\n        Column(\n            Modifier.fillMaxWidth()\n        ) {\n            nestedContent()\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/CommonConfigurableRenderersForDesktop.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon\n\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.BooleanConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.DayOfWeekConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.EnumConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.FileChecksumConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.FloatConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.FolderConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.IntConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.LongConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.PerHostSettingsConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.SpeedLimitConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.StringConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.ThemeConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.TimeConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.ProxyConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.CommonConfigurableRenderers\n\nval CommonConfigurableRenderersForDesktop = CommonConfigurableRenderers(\n    booleanConfigurableRenderer = BooleanConfigurableRenderer,\n    dayOfWeekConfigurableRenderer = DayOfWeekConfigurableRenderer,\n    fileChecksumConfigurableRenderer = FileChecksumConfigurableRenderer,\n    floatConfigurableRenderer = FloatConfigurableRenderer,\n    folderConfigurableRenderer = FolderConfigurableRenderer,\n    intConfigurableRenderer = IntConfigurableRenderer,\n    longConfigurableRenderer = LongConfigurableRenderer,\n    perHostSettingsConfigurableRenderer = PerHostSettingsConfigurableRenderer,\n    enumConfigurableRenderer = EnumConfigurableRenderer,\n    speedConfigurableRenderer = SpeedLimitConfigurableRenderer,\n    stringConfigurableRenderer = StringConfigurableRenderer,\n    themeConfigurableRenderer = ThemeConfigurableRenderer,\n    timeConfigurableRenderer = TimeConfigurableRenderer,\n    proxyConfigurableRenderer = ProxyConfigurableRenderer,\n)\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/BooleanConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.Switch\n\nobject BooleanConfigurableRenderer : ConfigurableRenderer<BooleanConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: BooleanConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderBooleanConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderBooleanConfig(\n        cfg: BooleanConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n    ) {\n        val checked = cfg.stateFlow.collectAsState().value\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    BooleanConfigurable.RenderMode.Checkbox -> {\n                        CheckBox(\n                            value = checked,\n                            enabled = enabled,\n                            onValueChange = {\n                                setValue(it)\n                            }\n                        )\n                    }\n\n                    BooleanConfigurable.RenderMode.Switch -> {\n                        Switch(\n                            checked = checked,\n                            enabled = enabled,\n                            onCheckedChange = {\n                                setValue(it)\n                            }\n                        )\n                    }\n                }\n            })\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/DayOfWeekConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\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.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.DayOfWeekConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.ifThen\nimport kotlinx.datetime.DayOfWeek\n\nobject DayOfWeekConfigurableRenderer : ConfigurableRenderer<DayOfWeekConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: DayOfWeekConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderDayOfWeekConfigurable(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val allDays = DayOfWeek.entries.toSet()\n        val enabled = isConfigEnabled()\n        fun isSelected(dayOfWeek: DayOfWeek): Boolean {\n            return dayOfWeek in value\n        }\n\n        fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) {\n            if (!enabled) return\n            if (select) {\n                setValue(\n                    value.plus(dayOfWeek).sorted().toSet()\n                )\n            } else {\n                setValue(\n                    value.minus(dayOfWeek).sorted().toSet()\n                )\n            }\n        }\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Row(\n                        Modifier.ifThen(!enabled) {\n                            alpha(0.5f)\n                        }\n                    ) {\n                        FlowRow(Modifier.fillMaxWidth()) {\n                            allDays.forEach { dayOfWeek ->\n                                RenderDayOfWeek(\n                                    modifier = Modifier,\n                                    enabled = enabled,\n                                    dayOfWeek = dayOfWeek,\n                                    selected = isSelected(dayOfWeek),\n                                    onSelect = { s, isSelected ->\n                                        selectDay(dayOfWeek, isSelected)\n                                    }\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        )\n    }\n\n    @Composable\n    fun RenderDayOfWeek(\n        modifier: Modifier,\n        dayOfWeek: DayOfWeek,\n        selected: Boolean,\n        onSelect: (DayOfWeek, Boolean) -> Unit,\n        enabled: Boolean = true,\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = modifier\n                .padding(2.dp)\n                .clip(CircleShape)\n                .ifThen(selected) {\n                    background(myColors.onBackground / 10)\n                }\n                .clickable(enabled = enabled) {\n                    onSelect(dayOfWeek, !selected)\n                }\n                .padding(vertical = 4.dp)\n                .padding(horizontal = 8.dp)\n\n        ) {\n            MyIcon(\n                MyIcons.check,\n                null,\n                Modifier.size(10.dp)\n                    .alpha(if (selected) 1f else 0f),\n            )\n            Spacer(Modifier.width(2.dp))\n            Text(\n                text = dayOfWeek.asStringSource().rememberString(),\n                modifier = Modifier.alpha(\n                    if (selected) 1f\n                    else 0.5f\n                ),\n                softWrap = false,\n                fontSize = myTextSizes.base,\n            )\n        }\n    }\n\n    private fun DayOfWeek.asStringSource() = when (this) {\n        DayOfWeek.MONDAY -> Res.string.monday\n        DayOfWeek.TUESDAY -> Res.string.tuesday\n        DayOfWeek.WEDNESDAY -> Res.string.wednesday\n        DayOfWeek.THURSDAY -> Res.string.thursday\n        DayOfWeek.FRIDAY -> Res.string.friday\n        DayOfWeek.SATURDAY -> Res.string.saturday\n        DayOfWeek.SUNDAY -> Res.string.sunday\n    }.asStringSource()\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/EnumConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.EnumConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\n\nobject EnumConfigurableRenderer : ConfigurableRenderer<EnumConfigurable<Any>> {\n    @Composable\n    override fun RenderConfigurable(configurable: EnumConfigurable<Any>, configurableUiProps: ConfigurableUiProps) {\n        RenderEnumConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun <T> RenderEnumConfig(cfg: EnumConfigurable<T>, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val index = remember(cfg.possibleValues, value) {\n            cfg.possibleValues.indexOf(value)\n        }\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, false)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    EnumConfigurable.RenderMode.Spinner -> RenderSpinner(\n                        possibleValues = cfg.possibleValues,\n                        value = value,\n                        onSelect = {\n                            setValue(it)\n                        },\n                        valueToString = cfg.valueToString,\n                        modifier = Modifier.widthIn(min = 160.dp),\n                        enabled = enabled,\n                        render = {\n                            Text(cfg.describe(it).rememberString())\n                        })\n                }\n            }\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/FileChecksumConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.FileChecksumAlgorithm\nimport ir.amirab.util.compose.resources.myStringResource\n\nobject FileChecksumConfigurableRenderer : ConfigurableRenderer<FileChecksumConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FileChecksumConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFileChecksumConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFileChecksumConfig(cfg: FileChecksumConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val enabled = isConfigEnabled()\n        val hasFileChecksum = value != null\n        ConfigTemplate(\n            configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    TitleAndDescription(cfg, true)\n                }\n            },\n            nestedContent = {\n                Column(Modifier.align(Alignment.End)) {\n                    AnimatedVisibility(\n                        hasFileChecksum,\n                    ) {\n                        value?.let { value ->\n                            Row(\n                                Modifier\n                                    .padding(vertical = 8.dp),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                RenderSpinner(\n                                    possibleValues = FileChecksumAlgorithm\n                                        .all()\n                                        .map { it.algorithm },\n                                    value = value.algorithm,\n                                    modifier = Modifier.Companion,\n                                    enabled = enabled,\n                                    onSelect = {\n                                        setValue(value.copy(algorithm = it))\n                                    }\n                                ) {\n                                    Text(it)\n                                }\n                                Text(\":\", Modifier.padding(horizontal = 4.dp))\n                                MyTextField(\n                                    text = value.value,\n                                    onTextChange = {\n                                        setValue(value.copy(value = it))\n                                    },\n                                    shape = RectangleShape,\n                                    textPadding = PaddingValues(4.dp),\n                                    enabled = enabled,\n                                    modifier = Modifier.weight(1f),\n                                    placeholder = myStringResource(Res.string.file_checksum),\n                                )\n                            }\n                        }\n                    }\n                }\n            },\n            value = {\n                CheckBox(\n                    value = hasFileChecksum,\n                    enabled = enabled,\n                    onValueChange = {\n                        if (it) {\n                            setValue(\n                                FileChecksum(\n                                    FileChecksumAlgorithm.default().algorithm,\n                                    \"\",\n                                )\n                            )\n                        } else {\n                            setValue(null)\n                        }\n                    })\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/FloatConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.FloatConfigurable\nimport com.abdownloadmanager.shared.ui.widget.FloatTextField\n\nobject FloatConfigurableRenderer : ConfigurableRenderer<FloatConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FloatConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFloatConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFloatConfig(cfg: FloatConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    FloatConfigurable.RenderMode.TextField -> {\n                        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n\n                        val modifier = Modifier.Companion.width(100.dp)\n                        FloatTextField(\n                            value = value,\n                            onValueChange = { v ->\n                                setValue(v)\n                            },\n                            interactionSource = interactionSource,\n                            range = cfg.range,\n                            modifier = modifier,\n                            enabled = enabled,\n                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Companion.Decimal),\n                            placeholder = \"\",\n                        )\n                    }\n                }\n            }\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/FolderConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.FolderConfigurable\nimport com.abdownloadmanager.desktop.ui.util.rememberMyDirectoryPickerLauncher\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport java.io.File\n\nobject FolderConfigurableRenderer : ConfigurableRenderer<FolderConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FolderConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFolderConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFolderConfig(cfg: FolderConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val pickFolderLauncher = rememberMyDirectoryPickerLauncher(\n            title = cfg.title.rememberString(),\n            initialDirectory = remember(value) {\n                runCatching {\n                    File(value).canonicalPath\n                }.getOrNull()\n            },\n        ) { directory ->\n            directory?.let(setValue)\n        }\n\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                MyTextField(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = value,\n                    onTextChange = {\n                        setValue(it)\n                    },\n                    shape = myShapes.defaultRounded,\n                    textPadding = PaddingValues(4.dp),\n                    placeholder = cfg.title.rememberString(),\n                    end = {\n                        MyIcon(\n                            icon = MyIcons.folder,\n                            contentDescription = null,\n                            modifier = Modifier\n                                .pointerHoverIcon(PointerIcon.Default)\n                                .fillMaxHeight()\n                                .clickable { pickFolderLauncher.launch() }\n                                .wrapContentHeight()\n                                .padding(horizontal = 8.dp)\n                                .size(16.dp))\n                    }\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/IntConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\n\nobject IntConfigurableRenderer : ConfigurableRenderer<IntConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: IntConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderIntegerConfig(configurable, configurableUiProps)\n    }\n\n\n    private operator fun IntRange.get(index: Int): Int {\n        return (start + index).also {\n            if (it > last) {\n                throw IndexOutOfBoundsException(\"$it bigger that $last\")\n            }\n        }\n\n    }\n\n    @Composable\n    private fun RenderIntegerConfig(cfg: IntConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    IntConfigurable.RenderMode.TextField -> {\n                        val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n                        IntTextField(\n                            value = value,\n                            onValueChange = { v ->\n                                setValue(v)\n                            },\n//                    colors = TextFieldDefaults.outlinedTextFieldColors(\n//                        backgroundColor = Color.Transparent\n//                    ),\n                            interactionSource = interactionSource,\n                            range = cfg.range,\n                            modifier = Modifier.width(100.dp),\n                            enabled = enabled,\n                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                            placeholder = \"\",\n                        )\n                    }\n                }\n            })\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/LongConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.LongConfigurable\nimport com.abdownloadmanager.shared.ui.widget.LongTextField\n\nobject LongConfigurableRenderer : ConfigurableRenderer<LongConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: LongConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderLongConfig(configurable, configurableUiProps)\n    }\n\n\n    private operator fun LongRange.get(index: Int): Long {\n        return (start + index).also {\n            if (it > last) {\n                throw IndexOutOfBoundsException(\"$it bigger that $last\")\n            }\n        }\n    }\n\n    @Composable\n    private fun RenderLongConfig(cfg: LongConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                when (cfg.renderMode) {\n                    LongConfigurable.RenderMode.TextField -> {\n                        val interactionSource = remember { MutableInteractionSource() }\n                        LongTextField(\n                            value = value,\n                            onValueChange = { v ->\n                                setValue(v)\n                            },\n//                        colors = TextFieldDefaults.textFieldColors(\n//                            backgroundColor = Color.Transparent\n//                        ),\n                            modifier = Modifier.width(200.dp),\n                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                            interactionSource = interactionSource,\n                            range = cfg.range,\n                            enabled = enabled,\n                        )\n                    }\n                }\n            })\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/PerHostSettingsConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.NavigatableConfigurable\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport ir.amirab.util.compose.resources.myStringResource\n\nobject PerHostSettingsConfigurableRenderer : ConfigurableRenderer<NavigatableConfigurable> {\n    @Composable\n    override fun RenderConfigurable(\n        configurable: NavigatableConfigurable,\n        configurableUiProps: ConfigurableUiProps\n    ) {\n        RenderPerHostSettingsConfigurable(\n            cfg = configurable,\n            configurableUiProps = configurableUiProps,\n            onRequestOpenConfigWindow = configurable.onRequestNavigate,\n        )\n    }\n\n    @Composable\n    private fun RenderPerHostSettingsConfigurable(\n        cfg: NavigatableConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n        onRequestOpenConfigWindow: () -> Unit\n    ) {\n//    val value by cfg.stateFlow.collectAsState()\n//    val setValue = cfg::set\n//    val enabled = isConfigEnabled()\n\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                ActionButton(\n                    myStringResource(Res.string.change),\n                    onClick = onRequestOpenConfigWindow,\n                )\n            },\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/ProxyConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\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.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\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.onClick\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\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.graphics.Brush\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.ExpandableItem\nimport com.abdownloadmanager.shared.ui.widget.Help\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.ui.widget.Multiselect\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.ui.widget.RadioButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\nimport com.abdownloadmanager.shared.util.proxy.ProxyMode\nimport com.abdownloadmanager.shared.util.proxy.ProxyRules\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.downloader.connection.proxy.Proxy\nimport ir.amirab.downloader.connection.proxy.ProxyType\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.ifThen\n\nobject ProxyConfigurableRenderer : ConfigurableRenderer<ProxyConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: ProxyConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderProxyConfig(configurable, configurableUiProps)\n    }\n\n\n    @Composable\n    fun RenderProxyConfig(cfg: ProxyConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                RenderChangeProxyConfig(\n                    proxyWithRules = value,\n                    setProxyWithRules = { setValue(it) }\n                )\n            },\n        )\n    }\n\n    @Stable\n    private class ProxyEditState(\n        private val proxyData: ProxyData,\n        private val setProxyData: (ProxyData) -> Unit,\n    ) {\n        var proxyMode = mutableStateOf(proxyData.proxyMode)\n\n        //pac\n        var pacURL = mutableStateOf(proxyData.pac.uri)\n\n        //manual\n        var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type)\n\n        var proxyHost = mutableStateOf(proxyData.proxyWithRules.proxy.host)\n        var proxyPort = mutableStateOf(proxyData.proxyWithRules.proxy.port)\n\n        var useAuth = mutableStateOf(proxyData.proxyWithRules.proxy.username != null)\n        var proxyUsername = mutableStateOf(proxyData.proxyWithRules.proxy.username.orEmpty())\n        var proxyPassword = mutableStateOf(proxyData.proxyWithRules.proxy.password.orEmpty())\n\n        var excludeURLPatterns = mutableStateOf(proxyData.proxyWithRules.rules.excludeURLPatterns.joinToString(\" \"))\n\n        val canSave: Boolean by derivedStateOf {\n            when (proxyMode.value) {\n                ProxyMode.Direct -> true\n                ProxyMode.UseSystem -> true\n                ProxyMode.Manual -> {\n                    val hostValid = proxyHost.value.isNotBlank()\n                    hostValid\n                }\n\n                ProxyMode.Pac -> {\n                    HttpUrlUtils.isValidUrl(pacURL.value)\n                }\n            }\n\n        }\n\n        fun save() {\n            val useAuth = useAuth.value\n            if (!canSave) {\n                return\n            }\n            setProxyData(\n                proxyData.copy(\n                    proxyMode = proxyMode.value,\n                    pac = proxyData.pac.copy(pacURL.value),\n                    proxyWithRules = proxyData.proxyWithRules.copy(\n                        proxy = Proxy(\n                            type = proxyType.value,\n                            host = proxyHost.value.trim(),\n                            port = proxyPort.value,\n                            username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth },\n                            password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth },\n                        ),\n                        rules = ProxyRules(\n                            excludeURLPatterns = excludeURLPatterns.value\n                                .split(\" \")\n                                .map { it.trim() }\n                                .filterNot { it.isEmpty() },\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    @Composable\n    fun RenderChangeProxyConfig(\n        proxyWithRules: ProxyData,\n        setProxyWithRules: (ProxyData) -> Unit,\n    ) {\n        var showProxyConfig by remember {\n            mutableStateOf(false)\n        }\n        ActionButton(\n            myStringResource(Res.string.change_proxy),\n            onClick = {\n                showProxyConfig = true\n            },\n        )\n        if (showProxyConfig) {\n            val dismiss = {\n                showProxyConfig = false\n            }\n            val state = remember(setProxyWithRules) {\n                ProxyEditState(\n                    proxyData = proxyWithRules,\n                    setProxyData = {\n                        setProxyWithRules(it)\n                        dismiss()\n                    }\n                )\n            }\n            ProxyEditDialog(state, onDismiss = dismiss)\n        }\n    }\n\n\n    @Composable\n    private fun ProxyEditDialog(\n        state: ProxyEditState,\n        onDismiss: () -> Unit,\n    ) {\n        Dialog(\n            onDismissRequest = (onDismiss),\n            content = {\n                val (mode, setMode) = state.proxyMode\n                SettingsDialog(\n                    headerTitle = myStringResource(Res.string.proxy_change_title),\n                    onDismiss = onDismiss,\n                    content = {\n                        val shape = myShapes.defaultRounded\n                        Column(\n                            Modifier.Companion\n                                .verticalScroll(rememberScrollState())\n                        ) {\n                            Accordion(\n                                wrapItem = { item, content ->\n                                    val selected = item == mode\n                                    Box(\n                                        Modifier.Companion.ifThen(selected) {\n                                            Modifier.Companion\n                                                .clip(shape)\n                                                .border(1.dp, myColors.onBackground / 0.15f, shape)\n                                                .background(myColors.background / 25)\n                                        }\n                                    ) {\n                                        content()\n                                    }\n                                },\n                                possibleValues = ProxyMode.Companion.usableValues(),\n                                selectedItem = mode,\n                                renderHeader = {\n                                    val selected = it == mode\n                                    Row(\n                                        Modifier.Companion\n                                            .fillMaxWidth()\n                                            .clip(shape)\n                                            .clickable { setMode(it) }\n                                            .padding(8.dp)\n                                            .padding(\n                                                animateDpAsState(\n                                                    if (selected) 4.dp else 0.dp\n                                                ).value\n                                            )\n                                    ) {\n                                        RadioButton(\n                                            value = selected,\n                                            onValueChange = {},\n                                        )\n                                        Spacer(Modifier.Companion.width(8.dp))\n                                        Text(\n                                            text = it.asStringSource().rememberString(),\n                                            fontSize = if (selected) {\n                                                myTextSizes.lg\n                                            } else {\n                                                myTextSizes.base\n                                            },\n                                            fontWeight = if (selected) {\n                                                FontWeight.Companion.Bold\n                                            } else {\n                                                null\n                                            }\n                                        )\n                                    }\n                                },\n                                renderContent = {\n                                    val cm = Modifier.Companion\n                                        .fillMaxWidth()\n                                        .padding(\n                                            vertical = 12.dp,\n                                            horizontal = 16.dp\n                                        )\n                                    when (it) {\n                                        ProxyMode.Direct -> {\n\n                                        }\n\n                                        ProxyMode.UseSystem -> {\n                                            Column(cm) {\n                                                ActionButton(\n                                                    myStringResource(Res.string.proxy_open_system_proxy_settings),\n                                                    onClick = {\n                                                        DesktopUtils.Companion.openSystemProxySettings()\n                                                    },\n                                                )\n                                            }\n                                        }\n\n                                        ProxyMode.Manual -> {\n                                            Column(cm) {\n                                                RenderManualConfig(state)\n                                            }\n                                        }\n\n                                        ProxyMode.Pac -> {\n                                            Column(cm) {\n                                                RenderPACConfig(state)\n                                            }\n                                        }\n                                    }\n                                }\n                            )\n                            ProxyConfigSpacer()\n                        }\n                    },\n                    actions = {\n                        ActionButton(\n                            myStringResource(Res.string.change),\n                            enabled = state.canSave,\n                            onClick = {\n                                state.save()\n                            })\n                        Spacer(Modifier.Companion.width(8.dp))\n                        ActionButton(myStringResource(Res.string.cancel), onClick = {\n                            onDismiss()\n                        })\n                    }\n                )\n            }\n        )\n    }\n\n    @Composable\n    private fun RenderPACConfig(\n        state: ProxyEditState,\n    ) {\n        Column {\n            val (url, setPacUrl) = state.pacURL\n            DialogConfigItem(\n                modifier = Modifier.Companion,\n                title = {\n                    Text(myStringResource(Res.string.proxy_pac_url))\n                },\n                value = {\n                    Row(\n                        verticalAlignment = Alignment.Companion.CenterVertically,\n                    ) {\n                        MyTextField(\n                            text = url,\n                            onTextChange = setPacUrl,\n                            placeholder = \"http://path/to/file.pac\",\n                            modifier = Modifier.Companion.weight(1f),\n                        )\n                    }\n                }\n            )\n        }\n    }\n\n    @Composable\n    private fun RenderManualConfig(\n        state: ProxyEditState,\n    ) {\n        val (type, setType) = state.proxyType\n        val (host, setHost) = state.proxyHost\n        val (port, setPort) = state.proxyPort\n        val (useAuth, setUseAuth) = state.useAuth\n        val (username, setUsername) = state.proxyUsername\n        val (password, setPassword) = state.proxyPassword\n        val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Text(myStringResource(Res.string.proxy_type))\n            },\n            value = {\n                Multiselect(\n                    selections = ProxyType.entries.toList(),\n                    selectedItem = type,\n                    onSelectionChange = setType,\n                    modifier = Modifier.Companion,\n                    render = {\n                        Text(\n                            it.name,\n                            modifier = Modifier.Companion.padding(vertical = 4.dp, horizontal = 8.dp),\n                        )\n                    },\n                    selectedColor = LocalContentColor.current / 15,\n                    unselectedAlpha = 0.8f,\n                )\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Text(myStringResource(Res.string.address_and_port))\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.Companion.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = host,\n                        onTextChange = setHost,\n                        placeholder = \"127.0.0.1\",\n                        modifier = Modifier.Companion.weight(1f),\n                    )\n                    Text(\":\", Modifier.Companion.padding(horizontal = 8.dp))\n                    IntTextField(\n                        value = port,\n                        onValueChange = setPort,\n                        placeholder = myStringResource(Res.string.port),\n                        range = 1..65535,\n                        modifier = Modifier.Companion.width(96.dp),\n                        keyboardOptions = KeyboardOptions(),\n                        textPadding = PaddingValues(8.dp),\n                        shape = RoundedCornerShape(12.dp),\n                    )\n                }\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Row(\n                    modifier = Modifier.Companion.onClick {\n                        setUseAuth(!useAuth)\n                    }\n                ) {\n                    CheckBox(\n                        value = useAuth,\n                        onValueChange = setUseAuth,\n                        size = 16.dp\n                    )\n                    Spacer(Modifier.Companion.width(8.dp))\n                    Text(myStringResource(Res.string.use_authentication))\n                }\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.Companion.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = username,\n                        onTextChange = setUsername,\n                        placeholder = myStringResource(Res.string.username),\n                        modifier = Modifier.Companion.weight(1f),\n                        enabled = useAuth,\n                    )\n                    Spacer(Modifier.Companion.width(8.dp))\n                    MyTextField(\n                        text = password,\n                        onTextChange = setPassword,\n                        placeholder = myStringResource(Res.string.password),\n                        modifier = Modifier.Companion.weight(1f),\n                        enabled = useAuth,\n                    )\n                }\n            }\n        )\n        ProxyConfigSpacer()\n        DialogConfigItem(\n            modifier = Modifier.Companion,\n            title = {\n                Row {\n                    Text(myStringResource(Res.string.proxy_do_not_use_proxy_for))\n                    Spacer(Modifier.Companion.width(8.dp))\n                    Help(\n                        myStringResource(Res.string.proxy_do_not_use_proxy_for_description)\n                    )\n                }\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.Companion.CenterVertically,\n                ) {\n                    MyTextField(\n                        text = excludeURLPatterns,\n                        onTextChange = setExcludeURLPatterns,\n                        placeholder = \"example.com 192.168.1.*\",\n                        modifier = Modifier.Companion,\n                    )\n                }\n            }\n        )\n    }\n\n    @Composable\n    private fun SettingsDialog(\n        headerTitle: String,\n        onDismiss: () -> Unit,\n        content: @Composable () -> Unit,\n        actions: (@Composable RowScope.() -> Unit)? = null,\n    ) {\n        val shape = myShapes.defaultRounded\n        Column(\n            modifier = Modifier.Companion\n                .clip(shape)\n                .border(2.dp, myColors.onBackground / 10, shape)\n                .background(\n                    Brush.Companion.linearGradient(\n                        listOf(\n                            myColors.surface,\n                            myColors.background,\n                        )\n                    )\n                )\n                .padding(16.dp)\n                .width(450.dp),\n        ) {\n            Row(\n                verticalAlignment = Alignment.Companion.CenterVertically,\n                modifier = Modifier.Companion.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n            ) {\n                Text(\n                    headerTitle,\n                    fontSize = myTextSizes.lg,\n                    fontWeight = FontWeight.Companion.Bold,\n                )\n                MyIcon(\n                    MyIcons.windowClose,\n                    myStringResource(Res.string.close),\n                    Modifier.Companion\n                        .clip(CircleShape)\n                        .clickable { onDismiss() }\n                        .padding(12.dp)\n                        .size(12.dp),\n                )\n            }\n            Spacer(Modifier.Companion.height(8.dp))\n            Box(Modifier.Companion.weight(1f, false)) {\n                content()\n            }\n            actions?.let {\n                Spacer(Modifier.Companion.height(8.dp))\n                Row(\n                    Modifier.Companion.align(Alignment.Companion.End),\n                    verticalAlignment = Alignment.Companion.CenterVertically,\n                ) {\n                    actions()\n                }\n            }\n        }\n    }\n\n    @Composable\n    private fun ProxyConfigSpacer() {\n        Spacer(Modifier.Companion.height(8.dp))\n    }\n\n    @Composable\n    private fun DialogConfigItem(\n        modifier: Modifier,\n        title: @Composable ColumnScope.() -> Unit,\n        value: @Composable ColumnScope.() -> Unit,\n    ) {\n        Column(\n            modifier,\n        ) {\n            Column(\n                Modifier.Companion\n                    .height(IntrinsicSize.Max),\n            ) {\n                Column(\n                    verticalArrangement = Arrangement.Center,\n                    horizontalAlignment = Alignment.Companion.Start,\n                ) {\n                    title()\n                }\n                Spacer(Modifier.Companion.height(8.dp))\n                Column(\n                    verticalArrangement = Arrangement.Center,\n                    horizontalAlignment = Alignment.Companion.End,\n                ) {\n                    value()\n                }\n            }\n        }\n    }\n\n    private fun ProxyMode.asStringSource(): StringSource {\n        return when (this) {\n            ProxyMode.Direct -> Res.string.proxy_no\n            ProxyMode.UseSystem -> Res.string.proxy_system\n            ProxyMode.Manual -> Res.string.proxy_manual\n            ProxyMode.Pac -> Res.string.proxy_pac\n        }.asStringSource()\n    }\n\n    @Composable\n    private fun <T> Accordion(\n        possibleValues: List<T>,\n        selectedItem: T,\n        wrapItem: @Composable (T, @Composable () -> Unit) -> Unit = { _, content -> content() },\n        renderHeader: @Composable (T) -> Unit,\n        renderContent: @Composable (T) -> Unit,\n    ) {\n        Column {\n            possibleValues.forEach {\n                wrapItem(it) {\n                    ExpandableItem(\n                        isExpanded = selectedItem == it,\n                        header = {\n                            renderHeader(it)\n                        },\n                        body = {\n                            renderContent(it)\n                        },\n                    )\n                }\n            }\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/SpeedLimitConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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 com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.DoubleTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.LocalSpeedUnit\nimport ir.amirab.util.datasize.SizeConverter\nimport ir.amirab.util.datasize.SizeFactors\nimport ir.amirab.util.datasize.SizeUnit\nimport ir.amirab.util.datasize.SizeWithUnit\nimport ir.amirab.util.datasize.asConverterConfig\n\nobject SpeedLimitConfigurableRenderer : ConfigurableRenderer<SpeedLimitConfigurable> {\n    @Composable\n    override fun RenderConfigurable(\n        configurable: SpeedLimitConfigurable,\n        configurableUiProps: ConfigurableUiProps\n    ) {\n        RenderSpeedConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderSpeedConfig(cfg: SpeedLimitConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n\n        val speedUnit = LocalSpeedUnit.current\n        val allowedFactors = listOf(\n            SizeFactors.FactorValue.Kilo,\n            SizeFactors.FactorValue.Mega,\n        )\n        val units = allowedFactors.map {\n            SizeUnit(\n                factorValue = it,\n                baseSize = speedUnit.baseSize,\n                factors = speedUnit.factors\n            )\n        }\n        val enabled = isConfigEnabled()\n        val hasLimitSpeed = value > 0L\n\n        var currentUnit by remember(hasLimitSpeed) {\n            mutableStateOf(\n                SizeConverter.bytesToSize(\n                    value,\n                    speedUnit.copy(acceptedFactors = allowedFactors)\n                ).unit\n            )\n        }\n        var currentValue by remember(value) {\n            val v = SizeConverter.bytesToSize(\n                value, currentUnit.asConverterConfig()\n            ).formatedValue().toDouble()\n            mutableStateOf(v)\n        }\n        LaunchedEffect(currentValue, currentUnit) {\n            setValue(\n                SizeConverter.sizeToBytes(\n                    SizeWithUnit(currentValue, currentUnit),\n                )\n            )\n        }\n        ConfigTemplate(\n            configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    TitleAndDescription(cfg, true)\n                }\n            },\n            nestedContent = {\n                Column(Modifier.align(Alignment.End)) {\n                    AnimatedVisibility(hasLimitSpeed) {\n                        Row(\n                            Modifier\n                                .padding(vertical = 8.dp)\n                                .width(200.dp)\n                        ) {\n                            DoubleTextField(\n                                value = currentValue,\n                                onValueChange = {\n                                    currentValue = it\n                                },\n                                enabled = enabled && hasLimitSpeed,\n                                range = 0.0..1_000.0,\n                                unit = 1.0,\n                                modifier = Modifier.weight(1f),\n                            )\n                            Spacer(Modifier.width(2.dp))\n                            RenderSpinner(\n                                possibleValues = units,\n                                value = currentUnit,\n                                modifier = Modifier.Companion,\n                                enabled = enabled && hasLimitSpeed,\n                                onSelect = {\n                                    currentUnit = it\n                                }\n                            ) {\n                                val prettified = remember(it) {\n                                    \"$it/s\"\n                                }\n                                Text(prettified)\n                            }\n                        }\n                    }\n                }\n            },\n            value = {\n                CheckBox(\n                    value = hasLimitSpeed,\n                    enabled = enabled,\n                    onValueChange = {\n                        if (it) {\n                            setValue(\n                                SizeConverter.sizeToBytes(\n                                    SizeWithUnit(\n                                        256.0, currentUnit\n                                    )\n                                )\n                            )\n                        } else {\n                            setValue(0)\n                        }\n                    })\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/StringConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.widget.MyTextField\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\n\nobject StringConfigurableRenderer : ConfigurableRenderer<StringConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: StringConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderStringConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    fun RenderStringConfig(cfg: StringConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                MyTextField(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = value,\n                    onTextChange = {\n                        setValue(it)\n                    },\n                    shape = myShapes.defaultRounded,\n                    textPadding = PaddingValues(4.dp),\n                    placeholder = \"\",\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/ThemeConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.ThemeConfigurable\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\n\nobject ThemeConfigurableRenderer : ConfigurableRenderer<ThemeConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: ThemeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderThemeConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderThemeConfig(cfg: ThemeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                RenderSpinner(\n                    possibleValues = cfg.possibleValues, value = value, onSelect = {\n                        setValue(it)\n                    },\n                    modifier = Modifier.widthIn(min = 160.dp),\n                    enabled = enabled,\n                    valueToString = cfg.valueToString,\n                    render = {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.ifThen(!enabled) {\n                                alpha(0.5f)\n                            }\n                        ) {\n                            Spacer(\n                                Modifier\n                                    .clip(CircleShape)\n                                    .border(\n                                        1.dp,\n                                        Brush.verticalGradient(myColors.primaryGradientColors),\n                                        CircleShape\n                                    )\n                                    .padding(1.dp)\n                                    .background(\n                                        it.color,\n                                    )\n                                    .size(16.dp)\n                            )\n                            Spacer(Modifier.width(16.dp))\n                            Text(cfg.describe(it).rememberString(), fontSize = myTextSizes.lg)\n                        }\n                    })\n            }\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/comon/renderer/TimeConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.comon.renderer\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\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.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.configurable.item.TimeConfigurable\nimport com.abdownloadmanager.shared.ui.widget.IntTextField\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport kotlinx.datetime.LocalTime\n\nobject TimeConfigurableRenderer : ConfigurableRenderer<TimeConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: TimeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderTimeConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    fun RenderTimeConfig(cfg: TimeConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        var hour by remember(value) {\n            mutableStateOf(value.hour)\n        }\n        var minute by remember(value) {\n            mutableStateOf(value.minute)\n        }\n        LaunchedEffect(hour, minute) {\n            setValue(\n                LocalTime(\n                    hour = hour, minute = minute,\n                )\n            )\n        }\n\n        val textFieldModifier = Modifier.width(64.dp)\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    IntTextField(\n                        value = hour,\n                        onValueChange = {\n                            hour = it\n                        },\n                        range = 0..23,\n                        modifier = textFieldModifier,\n                        enabled = enabled,\n                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                        placeholder = \"hour\",\n                        prettify = { it.toString().padStart(2, '0') },\n                    )\n                    Text(\":\", Modifier.padding(horizontal = 4.dp))\n                    IntTextField(\n                        value = minute,\n                        onValueChange = {\n                            minute = it\n                        },\n                        range = 0..59,\n                        modifier = textFieldModifier,\n                        enabled = enabled,\n                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),\n                        placeholder = \"minute\",\n                        prettify = { it.toString().padStart(2, '0') },\n                    )\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/platform/DesktopConfigurableRenderers.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.platform\n\nimport com.abdownloadmanager.desktop.ui.configurable.platform.item.FontConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.desktop.ui.configurable.platform.renderer.FontConfigurableRenderer\nimport com.abdownloadmanager.desktop.ui.configurable.comon.renderer.ProxyConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.ContainsConfigurableRenderers\n\ndata class DesktopConfigurableRenderers(\n    val fontConfigurableRenderer: ConfigurableRenderer<FontConfigurable>,\n) : ContainsConfigurableRenderers {\n    override fun getAllRenderers(): Map<Configurable.Key, ConfigurableRenderer<*>> {\n        return mapOf(\n            FontConfigurable.Key to fontConfigurableRenderer,\n        )\n    }\n}\n\nval PlatformConfigurableRenderersForDesktop = DesktopConfigurableRenderers(\n    fontConfigurableRenderer = FontConfigurableRenderer,\n)\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/platform/item/FontConfigurable.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.platform.item\n\nimport com.abdownloadmanager.desktop.pages.settings.FontInfo\nimport com.abdownloadmanager.shared.ui.configurable.BaseEnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass FontConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<FontInfo>,\n    describe: (FontInfo) -> StringSource,\n    possibleValues: List<FontInfo>,\n    valueToString: (FontInfo) -> List<String> = {\n        listOf(it.name.getString())\n    },\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : BaseEnumConfigurable<FontInfo>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    possibleValues = possibleValues,\n    valueToString = valueToString,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/configurable/platform/renderer/FontConfigurableRenderer.kt",
    "content": "package com.abdownloadmanager.desktop.ui.configurable.platform.renderer\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.ui.configurable.platform.item.FontConfigurable\nimport com.abdownloadmanager.desktop.ui.configurable.ConfigTemplate\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRenderer\nimport com.abdownloadmanager.shared.ui.configurable.RenderSpinner\nimport com.abdownloadmanager.desktop.ui.configurable.TitleAndDescription\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableUiProps\nimport com.abdownloadmanager.shared.ui.configurable.isConfigEnabled\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\n\nobject FontConfigurableRenderer : ConfigurableRenderer<FontConfigurable> {\n    @Composable\n    override fun RenderConfigurable(configurable: FontConfigurable, configurableUiProps: ConfigurableUiProps) {\n        RenderFontConfig(configurable, configurableUiProps)\n    }\n\n    @Composable\n    private fun RenderFontConfig(cfg: FontConfigurable, configurableUiProps: ConfigurableUiProps) {\n        val value by cfg.stateFlow.collectAsState()\n        val setValue = cfg::set\n        val enabled = isConfigEnabled()\n        ConfigTemplate(\n            modifier = configurableUiProps.modifier.padding(configurableUiProps.itemPaddingValues),\n            title = {\n                TitleAndDescription(cfg, true)\n            },\n            value = {\n                RenderSpinner(\n                    possibleValues = cfg.possibleValues, value = value, onSelect = {\n                        setValue(it)\n                    },\n                    valueToString = cfg.valueToString,\n                    modifier = Modifier.widthIn(min = 160.dp),\n                    enabled = enabled,\n                    render = {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.ifThen(!enabled) {\n                                alpha(0.5f)\n                            }\n                        ) {\n                            Text(\n                                cfg.describe(it).rememberString(),\n                                fontFamily = it.fontFamily,\n                                fontSize = myTextSizes.lg,\n                            )\n                        }\n                    })\n            }\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/error/ErrorUi.kt",
    "content": "package com.abdownloadmanager.desktop.ui.error\n\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.ScreenSurface\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\n\n@Composable\nfun ErrorWindow(\n    throwable: Throwable,\n    close: () -> Unit,\n){\n    CustomWindow(\n        onCloseRequest = close,\n        resizable = true,\n        state = rememberWindowState(\n            size = DpSize(500.dp,400.dp),\n            position = WindowPosition.Aligned(Alignment.Center)\n        ),\n        alwaysOnTop = true,\n    ) {\n        ErrorUi(throwable, close)\n    }\n}\n\n@Composable\nprivate fun ErrorUi(\n    e: Throwable,\n    close: () -> Unit,\n) {\n    WindowTitle(\"Error\")\n    ScreenSurface(\n        modifier = Modifier.fillMaxSize(),\n        contentColor = myColors.onBackground,\n        background = myColors.background,\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(horizontal = 8.dp)\n        ) {\n            Header(\n                modifier = Modifier\n                    .fillMaxWidth(),\n                e\n            )\n            Spacer(Modifier.height(8.dp))\n            RenderException(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f),\n                e = e\n            )\n            Spacer(Modifier.height(8.dp))\n            Actions(\n                modifier = Modifier\n                    .fillMaxWidth(),\n                close = close,\n                copyInformation = {\n                    ClipboardUtil.copy(createInformation(e))\n                }\n            )\n            Spacer(Modifier.height(8.dp))\n        }\n    }\n}\n\nfun createInformation(\n    e: Throwable,\n): String {\n    val exceptionString = e.stackTraceToString().replace(\"\\t\", \"    \")\n    val version = AppInfo.version\n    val platform = AppInfo.platform.name\n    return \"\"\"\n### Application Runtime Error\n###### App Info\n```\nappVersion = $version\nplatform = $platform\n```\n###### Exception\n```\n$exceptionString\n```\n\"\"\".trimIndent()\n}\n\n@Composable\nprivate fun Header(modifier: Modifier = Modifier, e: Throwable) {\n    Text(\n        text = \"There is an error happen in application (\\\"${e.localizedMessage}\\\")\", modifier = modifier,\n        fontSize = myTextSizes.xl\n    )\n}\n\n@Composable\nprivate fun RenderException(modifier: Modifier, e: Throwable) {\n    val errorText = remember(e) {\n        e.stackTraceToString()\n            //replace tab with space for compose to render it correctly\n            .replace(\"\\t\", \"    \")\n    }\n    Box(\n        modifier = modifier\n            .background(myColors.surface)\n            .verticalScroll(rememberScrollState())\n            .padding(8.dp)\n    ) {\n        SelectionContainer {\n            Text(\n                text = errorText,\n                color = myColors.error,\n                fontSize = myTextSizes.xl,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun Actions(\n    modifier: Modifier = Modifier,\n    close: () -> Unit,\n    copyInformation: () -> Unit,\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.End,\n    ) {\n        ActionButton(\n            text = \"Copy Information\",\n            onClick = copyInformation\n        )\n        Spacer(Modifier.width(8.dp))\n        ActionButton(\n            text = \"Close\",\n            onClick = close\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/util/FilePickerUtils.kt",
    "content": "package com.abdownloadmanager.desktop.ui.util\n\nimport androidx.compose.runtime.Composable\nimport com.abdownloadmanager.shared.ui.util.LocalWindow\nimport io.github.vinceglb.filekit.compose.PickerResultLauncher\nimport io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher\nimport io.github.vinceglb.filekit.core.FileKitPlatformSettings\n\n@Composable\nfun rememberMyDirectoryPickerLauncher(\n    title: String? = null,\n    initialDirectory: String? = null,\n    attachToWindow: Boolean = true,\n    onResult: (String?) -> Unit,\n): PickerResultLauncher {\n    return rememberDirectoryPickerLauncher(\n        title = title,\n        initialDirectory = initialDirectory,\n        platformSettings = createPlatformSettings(\n            attachToWindow = attachToWindow\n        ),\n        onResult = {\n            onResult(it?.path)\n        },\n    )\n}\n\n@Composable\nfun createPlatformSettings(attachToWindow: Boolean): FileKitPlatformSettings {\n    return FileKitPlatformSettings(\n        parentWindow = LocalWindow.current.takeIf { attachToWindow }\n    )\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/ConfirmDialog.kt",
    "content": "package com.abdownloadmanager.desktop.ui.widget\n\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowPosition\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.ActionContainer\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.Dimension\n\n@Suppress(\"unused\")\nsealed class ConfirmDialogType {\n    data object Success : ConfirmDialogType()\n    data object Info : ConfirmDialogType()\n    data object Error : ConfirmDialogType()\n    data object Warning : ConfirmDialogType()\n}\n\n@Composable\nfun ConfirmDialog(\n    title: StringSource,\n    message: StringSource,\n    type: ConfirmDialogType,\n    onConfirm: () -> Unit,\n    onCancel: () -> Unit,\n) {\n    val uiScale = LocalUiScale.current\n    val h = 180.applyUiScale(uiScale)\n    val w = 400.applyUiScale(uiScale)\n    val state = rememberWindowState(\n        size = DpSize(w.dp, h.dp),\n        position = WindowPosition.Aligned(Alignment.Center)\n    )\n    CustomWindow(\n        state,\n        onRequestMinimize = null,\n        onRequestToggleMaximize = null,\n        onCloseRequest = onCancel,\n        alwaysOnTop = true,\n    ) {\n        LaunchedEffect(Unit) {\n            window.minimumSize = Dimension(w, h)\n        }\n        val typeName = type.toString()\n        WindowTitle(typeName)\n        Column {\n            Row(\n                Modifier\n                    .weight(1f)\n                    .padding(8.dp),\n            ) {\n                val color = when (type) {\n                    ConfirmDialogType.Error -> myColors.info\n                    ConfirmDialogType.Info -> myColors.warning\n                    ConfirmDialogType.Success -> myColors.success\n                    ConfirmDialogType.Warning -> myColors.warning\n                }\n                MyIcon(\n                    icon = MyIcons.info,\n                    tint = color,\n                    modifier = Modifier\n                        .padding(16.dp)\n                        .requiredSize(36.dp),\n                    contentDescription = null,\n                )\n                Column {\n                    Text(\n                        title.rememberString(),\n                        fontSize = myTextSizes.xl,\n                        fontWeight = FontWeight.Bold,\n                    )\n                    Spacer(Modifier.height(8.dp))\n                    Text(\n                        message.rememberString(),\n                        fontSize = myTextSizes.base,\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxWidth()\n                            .verticalScroll(rememberScrollState())\n                    )\n                    Spacer(Modifier.height(8.dp))\n                }\n            }\n            ActionContainer(\n                Modifier.fillMaxWidth()\n            ) {\n                Row(\n                    Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.End,\n                ) {\n                    val confirmFocusRequester = remember { FocusRequester() }\n                    LaunchedEffect(Unit) {\n                        confirmFocusRequester.requestFocus()\n                    }\n                    ActionButton(\n                        myStringResource(Res.string.ok),\n                        onClick = onConfirm,\n                        modifier = Modifier.focusRequester(confirmFocusRequester)\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    ActionButton(\n                        myStringResource(Res.string.cancel),\n                        onClick = onCancel\n                    )\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/MessageDialogModel.kt",
    "content": "package com.abdownloadmanager.desktop.ui.widget\n\nimport com.abdownloadmanager.desktop.AppComponent\nimport com.abdownloadmanager.desktop.window.custom.CustomWindow\nimport com.abdownloadmanager.desktop.window.custom.WindowTitle\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.rememberWindowState\nimport com.abdownloadmanager.shared.ui.widget.ActionButton\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport java.awt.Dimension\nimport java.util.UUID\n\ndata class MessageDialogModel(\n    val id: String = UUID.randomUUID().toString(),\n    val title: StringSource,\n    val description: StringSource,\n    val type: MessageDialogType = MessageDialogType.Info,\n)\n\n@Composable\nfun ShowMessageDialogs(\n    appComponent: AppComponent,\n) {\n    val list by appComponent.dialogMessages.collectAsState()\n\n    for (msg in list) {\n        MessageDialog(\n            msgContent = msg,\n            onConfirm = {\n                appComponent.onDismissDialogMessage(msg)\n            }\n        )\n    }\n}\n\n@Composable\nfun MessageDialog(\n    msgContent: MessageDialogModel,\n    onConfirm: () -> Unit,\n) {\n    val uiScale = LocalUiScale.current\n    val h = 200.applyUiScale(uiScale)\n    val w = 400.applyUiScale(uiScale)\n    val state = rememberWindowState(\n        size = DpSize(w.dp, h.dp)\n    )\n    CustomWindow(\n        state,\n        onRequestMinimize = null,\n        onRequestToggleMaximize = null,\n        onCloseRequest = onConfirm,\n        alwaysOnTop = true,\n    ) {\n        LaunchedEffect(Unit) {\n            window.minimumSize = Dimension(w, h)\n        }\n        val typeName = msgContent.type.toString()\n        WindowTitle(typeName)\n        Row(\n            Modifier.padding(8.dp),\n        ) {\n            val color = when (msgContent.type) {\n                MessageDialogType.Error -> myColors.info\n                MessageDialogType.Info -> myColors.warning\n                MessageDialogType.Success -> myColors.success\n                MessageDialogType.Warning -> myColors.warning\n            }\n            MyIcon(\n                icon = MyIcons.info,\n                tint = color,\n                modifier = Modifier\n                    .padding(16.dp)\n                    .requiredSize(36.dp),\n                contentDescription = null,\n            )\n            Column {\n                Text(\n                    msgContent.title.rememberString(),\n                    fontSize = myTextSizes.xl,\n                    fontWeight = FontWeight.Bold,\n                )\n                Spacer(Modifier.height(8.dp))\n                Text(\n                    msgContent.description.rememberString(),\n                    fontSize = myTextSizes.base,\n                    modifier = Modifier\n                        .weight(1f)\n                        .fillMaxWidth()\n                        .verticalScroll(rememberScrollState())\n                )\n                Spacer(Modifier.height(8.dp))\n                Row(\n                    Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.End,\n                ) {\n                    ActionButton(\n                        myStringResource(Res.string.ok),\n                        onClick = onConfirm\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/Tray.kt",
    "content": "package com.abdownloadmanager.desktop.ui.widget\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.window.ApplicationScope\nimport com.kdroid.composetray.menu.api.TrayMenuBuilder\nimport com.kdroid.composetray.tray.api.Tray\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.asDesktop\n\n@Composable\nfun ApplicationScope.Tray(\n    icon: IconSource,\n    tooltip: String,\n    primaryAction: () -> Unit,\n    menu: List<MenuItem>\n) {\n    // use this composable to properly update menu item properties (State<T>,StateFlow<T> properties)\n    val immutableMenu = menu.toImmutableMenuItem()\n    val menuContent: TrayMenuBuilder.() -> Unit = {\n        for (item in immutableMenu) {\n            renderTrayItem(item)\n        }\n    }\n    val shouldBeMonochrome = when (Platform.asDesktop()) {\n        Platform.Desktop.MacOS -> true\n        Platform.Desktop.Linux -> false\n        Platform.Desktop.Windows -> false\n    }\n    if (shouldBeMonochrome && icon is IconSource.VectorIconSource) {\n        // for tray icon the library automatically converts the ImageVector to monochrome\n        // we want this behavior only for macOS\n        Tray(\n            icon = icon.value,\n            tooltip = tooltip,\n            primaryAction = primaryAction,\n            menuContent = menuContent,\n        )\n    } else {\n        Tray(\n            icon = icon.rememberPainter(),\n            tooltip = tooltip,\n            primaryAction = primaryAction,\n            menuContent = menuContent\n        )\n    }\n}\n\nprivate fun TrayMenuBuilder.renderTrayItem(item: ImmutableMenuItem) {\n    when (item) {\n        is ImmutableMenuItem.SingleItem -> {\n            renderTraySingleItem(item)\n        }\n\n        is ImmutableMenuItem.SubMenu -> {\n            renderTraySubMenu(item)\n        }\n\n        is ImmutableMenuItem.Separator -> Divider()\n    }\n}\n\nprivate fun TrayMenuBuilder.renderTraySingleItem(item: ImmutableMenuItem.SingleItem) {\n    val title = item.title\n    val isEnabled = item.enabled\n    val onClick = item.onAction\n    when (val iconSource = item.icon) {\n        is IconSource.VectorIconSource -> Item(\n            label = title,\n            isEnabled = isEnabled,\n            onClick = onClick,\n            icon = iconSource.value,\n        )\n\n        is IconSource.PainterIconSource -> Item(\n            label = title,\n            isEnabled = isEnabled,\n            onClick = onClick,\n            icon = iconSource.value,\n        )\n\n        null -> Item(\n            label = title,\n            isEnabled = isEnabled,\n            onClick = onClick,\n        )\n    }\n}\n\nprivate fun TrayMenuBuilder.renderTraySubMenu(submenu: ImmutableMenuItem.SubMenu) {\n    val title = submenu.title\n    val isEnabled = submenu.enabled\n    val submenuContent: TrayMenuBuilder.() -> Unit = {\n        for (item in submenu.items) {\n            renderTrayItem(item)\n        }\n    }\n    when (val iconSource = submenu.icon) {\n        is IconSource.PainterIconSource -> {\n            SubMenu(\n                label = title,\n                isEnabled = isEnabled,\n                submenuContent = submenuContent,\n                icon = iconSource.value,\n            )\n        }\n\n        is IconSource.VectorIconSource -> {\n            SubMenu(\n                label = title,\n                isEnabled = isEnabled,\n                submenuContent = submenuContent,\n                icon = iconSource.value,\n            )\n        }\n\n        null -> {\n            SubMenu(\n                label = title,\n                isEnabled = isEnabled,\n                submenuContent = submenuContent,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun List<MenuItem>.toImmutableMenuItem(): List<ImmutableMenuItem> {\n    return map {\n        when (it) {\n            MenuItem.Separator -> ImmutableMenuItem.Separator\n            is MenuItem.SingleItem -> {\n                ImmutableMenuItem.SingleItem(\n                    title = it.title.collectAsState().value.rememberString(),\n                    icon = it.icon.collectAsState().value,\n                    enabled = it.isEnabled.value,\n                    onAction = it::onClick\n                )\n            }\n\n            is MenuItem.SubMenu -> ImmutableMenuItem.SubMenu(\n                title = it.title.collectAsState().value.rememberString(),\n                icon = it.icon.value,\n                enabled = it.isEnabled.value,\n                items = it.items.collectAsState().value.toImmutableMenuItem()\n            )\n        }\n    }\n}\n\n\n@Immutable\nprivate sealed class ImmutableMenuItem {\n    data object Separator : ImmutableMenuItem()\n    data class SingleItem(\n        val title: String,\n        val icon: IconSource?,\n        val enabled: Boolean,\n        val onAction: () -> Unit,\n    ) : ImmutableMenuItem()\n\n    data class SubMenu(\n        val title: String,\n        val icon: IconSource?,\n        val enabled: Boolean,\n        val items: List<ImmutableMenuItem>\n    ) : ImmutableMenuItem()\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/AppInfo.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport com.abdownloadmanager.desktop.AppArguments\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.desktop.storage.DesktopDefinedPaths\nimport com.abdownloadmanager.shared.util.AppVersion\nimport ir.amirab.util.platform.Platform\nimport okio.Path.Companion.toOkioPath\nimport java.io.File\n\nobject AppInfo {\n    val name = SharedConstants.appName\n    val displayName = SharedConstants.appDisplayName\n    val packageName = SharedConstants.packageName\n    val website = SharedConstants.projectWebsite\n    val sourceCode = SharedConstants.projectSourceCode\n    val translationsUrl = SharedConstants.projectTranslations\n\n\n    val version = AppVersion.get()\n    val platform = Platform.getCurrentPlatform()\n    val exeFile: String? = run {\n//        if (!AppProperties.isAppInstalled()){\n//            return@run null\n//        }\n        System.getProperty(\"jpackage.app-path\")\n    }\n\n    private fun File.findAppFolder() = generateSequence(this) { it.parentFile }\n        .firstOrNull { it.name.endsWith(\".app\") }\n\n    val installationFolder: String? = run {\n        exeFile?.let(::File)\n            ?.parentFile // executable path\n            ?.let {\n                when (Platform.getCurrentPlatform()) {\n                    Platform.Desktop.Linux -> it.parentFile // <installationFolder>/bin/ABDownloadManager\n                    Platform.Desktop.MacOS -> it.findAppFolder() // /Applications/ABDownloadManager.app\n                    Platform.Desktop.Windows -> it // <installationFolder>/ABDownloadManager.exe\n                    else -> null\n                }?.path\n            }\n    }\n\n    private fun getUserDataDir(): File {\n        val dataDirName = SharedConstants.dataDirName\n        return File(System.getProperty(\"user.home\"), dataDirName)\n    }\n\n    val dataDir by lazy {\n        PortableUtil.getPortableDataDir(installationFolder) ?: getUserDataDir()\n    }\n    val definedPaths = DesktopDefinedPaths(dataDir.toOkioPath())\n}\n\nfun AppInfo.isAppInstalled(): Boolean {\n    return AppInfo.exeFile != null\n}\n\nfun AppInfo.isInIDE(): Boolean {\n    return !isAppInstalled()\n}\n\nfun AppInfo.isInDebugMode(): Boolean {\n    return AppArguments.get().debug || AppProperties.isDebugMode() || isInIDE()\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/AppProperties.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport okio.FileSystem\nimport okio.Path.Companion.toOkioPath\nimport okio.Path.Companion.toPath\nimport okio.Source\nimport okio.buffer\nimport okio.use\nimport java.util.Properties\nimport kotlin.io.path.Path\n\nobject AppProperties {\n    private val defaultProps = Properties()\n    private val appProps = Properties(defaultProps)\n\n    private object Paths {\n        const val DEFAULT_APP_PROPS_PATH = \"configs/app_default.properties\"\n        const val INSTALLED_APP_PROPS_NAME = \"app.properties\"\n    }\n\n    private object Keys {\n        const val DEBUG: String = \"app.debug\"\n    }\n\n    private fun loadFromSource(props: Properties, source: Source) {\n        source.buffer()\n            .inputStream()\n            .use {\n                props.load(it)\n            }\n    }\n\n    private fun loadDefaultProps() {\n        FileSystem.RESOURCES\n            .source(Paths.DEFAULT_APP_PROPS_PATH.toPath())\n            .use {\n                loadFromSource(defaultProps, it)\n            }\n    }\n\n    private var foundAppProperties = false\n    private fun loadAppProps() {\n        val resourceDir:String?=System.getProperty(\"compose.application.resources.dir\")\n        if (resourceDir.isNullOrBlank()){\n            foundAppProperties = false\n            return\n        }\n        val file = Path(resourceDir,Paths.INSTALLED_APP_PROPS_NAME).toOkioPath()\n        if (!FileSystem.SYSTEM.exists(file)) {\n            // app is in development and don't have app.properties,\n            // so we use only default\n            foundAppProperties = false\n            return\n        } else {\n            foundAppProperties = true\n        }\n        FileSystem.SYSTEM\n            .source(file)\n            .use {\n                loadFromSource(appProps, it)\n            }\n    }\n\n    private fun ensureAndGet(key: String): Any {\n        return requireNotNull(\n            appProps.getProperty(key)\n        ) { \"key: '$key' not found in properties file\" }\n            .hydrateVariables()\n            .trim('\"')\n            .trim('\\'')\n    }\n\n    fun isDebugMode(): Boolean {\n        return ensureAndGet(Keys.DEBUG)\n            .toString()\n            .toBoolean()\n    }\n\n    //app.properties in installation directory\n    fun isAppPropertiesFound(): Boolean {\n        return foundAppProperties\n    }\n\n    val userDir: String get() {\n        return System.getProperty(\"user.home\")\n    }\n\n    fun boot(){\n        loadDefaultProps()\n        loadAppProps()\n    }\n\n    fun getAll() = appProps\n}\n\nprivate val variableRegex = \"\\\\$\\\\{(.*?)\\\\}\".toRegex()\nprivate fun String.hydrateVariables(): String {\n    return variableRegex.replace(this) {\n        val variableName = it.groupValues[1]\n        System.getProperty(variableName)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DebugboardUtils.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n//\n//import ir.amirab.debugboard.core.plugin.watcher.RemoveWatch\n//import kotlinx.coroutines.CoroutineScope\n//import kotlinx.coroutines.job\n//\n//fun RemoveWatch.inScope(scope: CoroutineScope) {\n//    val removeWatch = this\n//    scope.coroutineContext.job.invokeOnCompletion {\n//        removeWatch()\n//    }\n//}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DesktopEntryCreator.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isLinux\nimport java.io.File\n\nobject DesktopEntryCreator {\n    fun createLinuxDesktopEntry() {\n        if (!Platform.isLinux()) {\n            return\n        }\n        runCatching {\n            LinuxDesktopEntryCreator.createDesktopEntry(\n                name = AppInfo.displayName,\n                comment = \"Manage and organize your download files better than before\",\n                desktopEntryFilename = AppInfo.packageName,\n                execFile = requireNotNull(AppInfo.exeFile) {\n                    \"Exe file not known\"\n                },\n                startupWMClass = \"com-abdownloadmanager-desktop-AppKt\"\n            )\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n}\n\n\nprivate object LinuxDesktopEntryCreator {\n    fun createDesktopEntry(\n        name: String,\n        execFile: String,\n        comment: String,\n        desktopEntryFilename: String,\n        startupWMClass: String,\n    ) {\n        val iconFilePath = requireNotNull(getIconFilePath(execFile)) {\n            \"Icon path is null! for this exe file: $execFile\"\n        }\n        val desktopEntryContent = buildString {\n            appendLine(\"[Desktop Entry]\")\n            appendLine(\"Name=$name\")\n            appendLine(\"Comment=$comment\")\n            appendLine(\"GenericName=Downloader\")\n            appendLine(\"Categories=Utility;Network;\")\n            appendLine(\"Exec=\\\"$execFile\\\"\")\n            appendLine(\"Icon=${iconFilePath}\")\n            appendLine(\"Terminal=false\")\n            appendLine(\"Type=Application\")\n            appendLine(\"StartupWMClass=${startupWMClass}\")\n        }\n        val homePath = System.getProperty(\"user.home\")\n        val desktopEntryFile = File(homePath, \".local/share/applications/${desktopEntryFilename}.desktop\")\n        desktopEntryFile.writeText(desktopEntryContent)\n    }\n\n    private fun getIconFilePath(execFile: String): String? {\n        return runCatching {\n            val file = File(execFile)\n            val name = file.name\n            file\n                .parentFile.parentFile\n                .resolve(\"lib/$name.png\")\n                .takeIf { it.exists() }?.path\n        }.getOrNull()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DesktopShortcutManager.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport androidx.compose.ui.awt.awtEventOrNull\nimport androidx.compose.ui.input.key.KeyEvent\nimport com.abdownloadmanager.shared.util.PlatformKeyStroke\nimport com.abdownloadmanager.shared.util.ShortcutManager\nimport java.awt.Toolkit\nimport java.awt.event.InputEvent\nimport javax.swing.KeyStroke\n\nclass DesktopShortcutManager : ShortcutManager() {\n    override fun stringToKeyStroke(keyStrokeString: String): PlatformKeyStroke {\n        return KeyStroke\n            .getKeyStroke(keyStrokeString)\n            .asPlatformKeyStroke()\n    }\n\n    override fun getKeyStrokeFromEvent(s: KeyEvent): PlatformKeyStroke? {\n        val awtEvent = s.awtEventOrNull ?: return null\n        return runCatching {\n            KeyStroke.getKeyStrokeForEvent(awtEvent)\n        }.getOrNull()?.asPlatformKeyStroke()\n    }\n\n    override fun getKeyStrokeFromKeyCode(keyCode: Int): PlatformKeyStroke? {\n        return runCatching {\n            KeyStroke.getKeyStroke(keyCode, 0)\n        }.getOrNull()?.asPlatformKeyStroke()\n    }\n}\n\ndata class DesktopKeyStroke(\n    val awtKeyStroke: KeyStroke,\n) : PlatformKeyStroke {\n    override val keyCode: Int\n        get() = awtKeyStroke.keyCode\n\n    override fun getModifiers(): List<String> {\n        return KeyUtil.getModifiers(awtKeyStroke.modifiers)\n    }\n\n    override fun getKeyText(): String {\n        return KeyUtil.getKeyText(awtKeyStroke.keyCode)\n    }\n}\n\nfun KeyStroke.asPlatformKeyStroke() = DesktopKeyStroke(this)\n\nobject KeyUtil {\n    fun getKeyText(keyCode: Int): String {\n        return java.awt.event.KeyEvent.getKeyText(keyCode)\n    }\n\n    fun getModifiers(modifiers: Int): List<String> {\n        return buildList {\n            if (modifiers and InputEvent.META_DOWN_MASK != 0) {\n                add(Toolkit.getProperty(\"AWT.meta\", \"Meta\"))\n            }\n            if (modifiers and InputEvent.CTRL_DOWN_MASK != 0) {\n                add(Toolkit.getProperty(\"AWT.control\", \"Ctrl\"))\n            }\n            if (modifiers and InputEvent.ALT_DOWN_MASK != 0) {\n                add(Toolkit.getProperty(\"AWT.alt\", \"Alt\"))\n            }\n            if (modifiers and InputEvent.SHIFT_DOWN_MASK != 0) {\n                add(Toolkit.getProperty(\"AWT.shift\", \"Shift\"))\n            }\n            if (modifiers and InputEvent.ALT_GRAPH_DOWN_MASK != 0) {\n                add(Toolkit.getProperty(\"AWT.altGraph\", \"Alt Graph\"))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/GlobalAppExceptionHandler.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport com.abdownloadmanager.desktop.ui.error.ErrorWindow\nimport com.abdownloadmanager.shared.ui.theme.ABDownloaderTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.window.*\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport java.awt.Window\nimport java.awt.event.WindowEvent\nimport java.lang.Thread.UncaughtExceptionHandler\nimport java.util.concurrent.atomic.AtomicBoolean\nimport kotlin.concurrent.thread\nimport kotlin.system.exitProcess\n\ninterface GlobalAppExceptionHandler : UncaughtExceptionHandler,\n    WindowExceptionHandlerFactory {\n\n    @Composable\n    fun content()\n\n    /**\n     * tell handler to exit app after crash, this app is useless\n     */\n    fun onProcessIsUseless()\n}\n\nprivate class GlobalExceptionHandlerImpl : GlobalAppExceptionHandler {\n    private val isRenderedInAppScope = MutableStateFlow(false)\n\n    private var exitProcessOnClose = AtomicBoolean(false)\n    override fun onProcessIsUseless() {\n        exitProcessOnClose.set(true)\n    }\n\n    private fun shouldExitAppInsteadOfClose(): Boolean {\n        return exitProcessOnClose.get()\n    }\n\n    private fun showErrorInUi(throwable: Throwable) {\n        if (isRenderedInAppScope.value) {\n            showErrorInCurrentApplicationScope(throwable)\n        } else {\n            showErrorInNewApplicationScope(throwable)\n        }\n    }\n\n    val activeThrowableList = MutableStateFlow(emptyList<Throwable>())\n    private fun showErrorInCurrentApplicationScope(throwable: Throwable) {\n        activeThrowableList.update {\n            it + throwable\n        }\n    }\n\n    private fun showErrorInNewApplicationScope(throwable: Throwable) {\n        kotlin.runCatching {\n            application(exitProcessOnExit = false) {\n                val close = {\n                    if (shouldExitAppInsteadOfClose()) {\n                        exitProcess(0)\n                    } else {\n                        exitApplication()\n                    }\n                }\n                ABDownloaderTheme(\n                    ThemeManager.DefaultTheme,\n                ) {\n                    ErrorWindow(throwable, close)\n                }\n            }\n        }.onFailure {\n            println(\"We have error in Global Exception Handler\")\n            it.printStackTrace()\n        }\n    }\n\n\n    @Composable\n    override fun content() {\n        DisposableEffect(Unit) {\n            isRenderedInAppScope.update { true }\n            onDispose {\n                isRenderedInAppScope.update { false }\n            }\n        }\n        val list = activeThrowableList.collectAsState().value\n        for (throwable in list) {\n            ErrorWindow(\n                throwable = throwable,\n                close = {\n                    activeThrowableList.update {\n                        it - throwable\n                    }\n                    if (activeThrowableList.value.isEmpty()) {\n                        if (shouldExitAppInsteadOfClose()) {\n                            exitProcess(0)\n                        }\n                    }\n                }\n            )\n        }\n    }\n\n    private fun showErrorInConsole(thread: Thread, e: Throwable) {\n        val output = System.err\n        output.println(\"\"\"Exception in thread \"${thread.name}\" ${e::class.qualifiedName}\"\"\")\n        e.printStackTrace(output)\n    }\n\n    private fun showErrorInConsole(window: Window, throwable: Throwable) {\n        val output = System.err\n        output.println(\"\"\"Exception in windows $window ,${throwable::class.qualifiedName}\"\"\")\n        throwable.printStackTrace(output)\n    }\n\n    override fun uncaughtException(t: Thread, e: Throwable) {\n        showErrorInConsole(t, e)\n        showErrorInUi(e)\n    }\n\n    override fun exceptionHandler(window: Window): WindowExceptionHandler {\n        return WindowExceptionHandler {\n            thread {\n                showErrorInConsole(window, it)\n                showErrorInUi(it)\n                //await exit\n                window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))\n            }\n        }\n    }\n}\n\n@Composable\nfun ProvideGlobalExceptionHandler(\n    eh: GlobalAppExceptionHandler,\n    content: @Composable () -> Unit,\n) {\n    CompositionLocalProvider(LocalWindowExceptionHandlerFactory provides eh) {\n        content()\n        eh.content()\n    }\n}\n\nfun createAndSetGlobalExceptionHandler(): GlobalAppExceptionHandler {\n    val handler = GlobalExceptionHandlerImpl()\n    Thread.setDefaultUncaughtExceptionHandler(handler)\n    return handler\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/IntegrationUtil.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport java.io.File\nimport kotlin.concurrent.thread\n\nobject IntegrationPortBroadcaster {\n    const val INTEGRATION_DISABLED=-1\n    const val INTEGRATION_UNKNOWN=-2\n\n    private var requestedToCleanOnClose = false\n    /*fun cleanOnClose() {\n        if (requestedToCleanOnClose) return\n        Runtime.getRuntime().addShutdownHook(\n            thread(\n                start = false\n            ) {\n                setIntegrationPortInFile(null)\n            }\n        )\n    }*/\n//    private val portFIle get() = File(AppProperties.getConfigDirectory(), \"integration.port\")\n    private var port:Int=INTEGRATION_UNKNOWN\n    fun isInitialized(): Boolean {\n        return port!=INTEGRATION_UNKNOWN\n    }\n\n    fun setIntegrationPortInFile(portNumber: Int?) {\n        port = portNumber ?: INTEGRATION_DISABLED\n    }\n\n    fun getIntegrationPort(): Int {\n        return port\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/KeepAwakeManager.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.util.desktop.keepawake.KeepAwake\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.onEach\n\nclass KeepAwakeManager(\n    private val keepAwake: KeepAwake,\n    private val downloadSystem: DownloadSystem,\n    private val scope: CoroutineScope,\n) {\n    var job: Job? = null\n\n    fun boot() {\n        enable()\n    }\n\n    @Synchronized\n    fun enable() {\n        job?.cancel()\n        job = downloadSystem.downloadMonitor\n            .activeDownloadCount\n            .map { it > 0 }\n            .distinctUntilChanged()\n            .onEach { isDownloadsActive ->\n                if (isDownloadsActive) {\n                    keepAwake.keepAwake()\n                } else {\n                    keepAwake.allowSleep()\n                }\n            }.launchIn(scope)\n    }\n\n    @Synchronized\n    fun disable() {\n        job?.cancel()\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/OSFileIconProvider.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.painter.BitmapPainter\nimport androidx.compose.ui.graphics.toComposeImageBitmap\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.ui.IMyIcons\nimport ir.amirab.util.compose.IconSource\nimport java.awt.image.BufferedImage\nimport java.io.File\nimport javax.swing.filechooser.FileSystemView\n\nclass OSFileIconProvider(\n    private val icons: IMyIcons\n) : FileIconProvider {\n    private val registeredIcons = mutableMapOf<String, ImageBitmap>()\n    private val lock = Any()\n    private fun getIconOfFileExtension(\n        extension: String,\n    ): ImageBitmap? {\n        val imageBitmap = registeredIcons[extension]\n        if (imageBitmap != null) {\n            return imageBitmap\n        } else {\n            synchronized(lock) {\n                val bitmapFoundInSync = registeredIcons[extension]\n                if (bitmapFoundInSync != null) {\n                    return bitmapFoundInSync\n                }\n                val w = 24\n                val h = 24\n                return runCatching {\n                    val fileSystemView = FileSystemView.getFileSystemView()\n                    val file = File.createTempFile(\"file\", \"file.$extension\")\n                    val icon = fileSystemView.getSystemIcon(file, w, h)\n                    bufferedImageToImageBitmap(iconToImage(icon))\n                }.onSuccess {\n                    registeredIcons[extension] = it\n                }.getOrNull()\n            }\n        }\n    }\n\n    private fun iconToImage(icon: javax.swing.Icon): BufferedImage {\n        val width = icon.iconWidth\n        val height = icon.iconHeight\n        val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)\n        val graphics = image.createGraphics()\n        icon.paintIcon(null, graphics, 0, 0)\n        graphics.dispose()\n        return image\n    }\n\n    private fun bufferedImageToImageBitmap(bufferedImage: BufferedImage): ImageBitmap {\n        return bufferedImage.toComposeImageBitmap()\n    }\n\n    override fun getIcon(fileName: String): IconSource {\n        val extension = fileName.substringAfterLast('.', \"\")\n        val imageBitmap = getIconOfFileExtension(extension)\n            ?: return icons.file\n        return IconSource.PainterIconSource(\n            BitmapPainter(imageBitmap),\n            false\n        )\n    }\n\n    @Composable\n    override fun rememberIcon(fileName: String): IconSource {\n        return getIcon(fileName)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/PortableUtil.kt",
    "content": "package com.abdownloadmanager.desktop.utils\n\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport java.io.File\n\nobject PortableUtil {\n    fun getPortableDataDir(installationFolder: String?): File? {\n        if (installationFolder != null) {\n            getDefaultPortableFolder(installationFolder)\n                ?.let { return it }\n            getCustomPortableFolder(installationFolder)\n                ?.let { return it }\n        }\n        return null\n    }\n\n\n    private const val PORTABLE_FILE_NAME = \".portable\"\n\n    private fun getDefaultPortableFolder(\n        installationFolder: String\n    ): File? {\n        val dataDirName = SharedConstants.dataDirName\n        val portableDataDir = File(installationFolder, dataDirName)\n        return portableDataDir.takeIf {\n            it.exists() && it.canWrite()\n        }\n    }\n\n    private fun getCustomPortableFolder(\n        installationFolder: String,\n    ): File? {\n        val portableFile = File(installationFolder, PORTABLE_FILE_NAME)\n            .takeIf { it.exists() && it.canRead() }\n            ?: return null\n        try {\n            val customPortableDirText = portableFile\n                .readText()\n                .trim()\n                .takeIf { it.isNotEmpty() } ?: error(\"$PORTABLE_FILE_NAME file is empty\")\n            val absoluteFile = getAbsoluteFile(\n                baseFile = installationFolder,\n                maybeRelative = customPortableDirText\n            )\n            // make sure it can be used\n            return absoluteFile.canonicalFile\n        } catch (e: Exception) {\n            System.err.println(\"getting custom portable path failed\")\n            e.printStackTrace()\n            return null\n        }\n    }\n\n    private fun getAbsoluteFile(\n        baseFile: String,\n        maybeRelative: String\n    ): File {\n        val file = File(maybeRelative)\n        return if (file.isAbsolute) {\n            file\n        } else {\n            File(baseFile, maybeRelative)\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/native_messaging/NativeMessaging.kt",
    "content": "package com.abdownloadmanager.desktop.utils.native_messaging\n\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.desktop.utils.isAppInstalled\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\ndata class NativeMessagingManifests(\n    val firefoxNativeMessagingManifest: FirefoxNativeMessagingManifest,\n    val chromeNativeMessagingManifest: ChromeNativeMessagingManifest,\n)\n\n\n@Serializable\ndata class FirefoxNativeMessagingManifest(\n    @SerialName(\"name\")\n    val name: String,\n    @SerialName(\"description\")\n    val description: String,\n    @SerialName(\"path\")\n    val path: String,\n    @SerialName(\"type\")\n    val type: String,\n    @SerialName(\"allowed_extensions\")\n    val allowedExtensions: List<String>,\n)\n\n@Serializable\ndata class ChromeNativeMessagingManifest(\n    @SerialName(\"name\")\n    val name: String,\n    @SerialName(\"description\")\n    val description: String,\n    @SerialName(\"path\")\n    val path: String,\n    @SerialName(\"type\")\n    val type: String,\n    @SerialName(\"allowed_origins\")\n    val allowedOrigins: List<String>,\n)\n\n\nclass NativeMessaging(\n    private val nativeMessagingManifestApplier: NativeMessagingManifestApplier,\n) {\n    fun boot(){\n        installManifests()\n    }\n    fun installManifests() {\n        val firefox = createFirefoxManifest()\n        val chrome = createChromeManifest()\n        if (chrome!=null && firefox!=null){\n            nativeMessagingManifestApplier.updateManifests(\n                NativeMessagingManifests(\n                    firefoxNativeMessagingManifest = firefox,\n                    chromeNativeMessagingManifest = chrome,\n                )\n            )\n        }\n    }\n\n    private fun createFirefoxManifest(): FirefoxNativeMessagingManifest? {\n        if (!AppInfo.isAppInstalled()) return null\n        val execFile = AppInfo.exeFile!!\n        return FirefoxNativeMessagingManifest(\n            name = AppInfo.displayName,\n            description = AppInfo.displayName,\n            path = execFile,\n            type = \"stdio\",\n            allowedExtensions = listOf(\n                \"\"\n            )\n        )\n    }\n    private fun createChromeManifest(): ChromeNativeMessagingManifest? {\n        if (!AppInfo.isAppInstalled()) return null\n        val execFile = AppInfo.exeFile!!\n        return ChromeNativeMessagingManifest(\n            name = AppInfo.displayName,\n            description = AppInfo.displayName,\n            path = execFile,\n            type = \"stdio\",\n            allowedOrigins = listOf(\n                \"\"\n            )\n        )\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/native_messaging/NativeMessagingManifestApplier.kt",
    "content": "package com.abdownloadmanager.desktop.utils.native_messaging\n\nimport com.abdownloadmanager.desktop.utils.AppInfo\nimport com.abdownloadmanager.desktop.utils.AppProperties\nimport com.abdownloadmanager.desktop.utils.isAppInstalled\nimport ir.amirab.util.createParentDirectories\nimport ir.amirab.util.deleteIfExists\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.desktop.WindowsRegistry\nimport ir.amirab.util.writeText\nimport kotlinx.serialization.json.Json\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.io.path.*\n\nabstract class NativeMessagingManifestApplier : KoinComponent {\n    protected val json by inject<Json>()\n    protected inline fun <reified T : Any> serialize(data: T): String {\n        return json.encodeToString(data)\n    }\n\n    protected inline fun <reified T : Any> deserialize(string: String): T {\n        return json.decodeFromString(string)\n    }\n\n    abstract fun updateManifests(manifests: NativeMessagingManifests)\n    abstract fun removeManifests()\n\n    companion object {\n        fun getForCurrentPlatform(): NativeMessagingManifestApplier {\n            if (!AppInfo.isAppInstalled()){\n                return NoOpNativeMessagingApplier()\n            }\n            return when(AppInfo.platform){\n                Platform.Desktop.Linux -> LinuxNativeMessagingManifestApplier()\n                Platform.Desktop.MacOS -> MacosNativeMessagingManifestApplier()\n                Platform.Desktop.Windows -> WindowsNativeMessagingManifestApplier()\n                Platform.Android -> error(\"there is no native messaging for android so this code should never used in android\")\n            }\n        }\n    }\n}\n\nclass WindowsNativeMessagingManifestApplier : NativeMessagingManifestApplier() {\n    private val baseNativeMessagingDir get() = AppInfo.definedPaths.configDir / \"native-messaging\"\n    private val firefoxManifestFile get() = baseNativeMessagingDir / \"firefox-native-messaging-manifest.json\"\n    private val chromeManifestFile get() = baseNativeMessagingDir / \"chrome-native-messaging-manifest.json\"\n    private val firefoxRegistryPath get() = \"HKCU\\\\SOFTWARE\\\\Mozilla\\\\NativeMessagingHosts\\\\${AppInfo.packageName}\"\n    private val chromeRegistryPath get() = \"HKCU\\\\SOFTWARE\\\\Google\\\\Chrome\\\\NativeMessagingHosts\\\\${AppInfo.packageName}\"\n\n    override fun updateManifests(\n        manifests: NativeMessagingManifests\n    ) {\n        listOf(\n            firefoxManifestFile,\n            chromeManifestFile,\n        ).forEach { it.createParentDirectories() }\n        firefoxManifestFile.writeText(serialize(manifests.firefoxNativeMessagingManifest))\n        WindowsRegistry.setValueInRegistry(\n            path = firefoxRegistryPath,\n            key = null,\n            value = firefoxManifestFile.toString()\n        )\n        chromeManifestFile.writeText(serialize(manifests.chromeNativeMessagingManifest))\n        WindowsRegistry.setValueInRegistry(\n            path = chromeRegistryPath,\n            key = null,\n            value = chromeManifestFile.toString()\n        )\n    }\n\n    override fun removeManifests() {\n        firefoxManifestFile.deleteIfExists()\n        WindowsRegistry.removePathInRegistry(\n            path = firefoxRegistryPath,\n        )\n        chromeManifestFile.deleteIfExists()\n        WindowsRegistry.removePathInRegistry(\n            path = chromeRegistryPath,\n        )\n    }\n\n}\n\nclass MacosNativeMessagingManifestApplier : NativeMessagingManifestApplier() {\n    private val firefoxNativeMessagingPath\n        get() = Path(AppProperties.userDir, \"Library/Application Support/Mozilla/NativeMessagingHosts\",\n            \"${AppInfo.packageName}.json\"\n        )\n    private val chromeNativeMessagingPath\n        get() = Path(AppProperties.userDir, \"Library/Application Support/Google/Chrome/NativeMessagingHosts\",\n            \"${AppInfo.packageName}.json\"\n        )\n    private val chromiumNativeMessagingPath\n        get() = Path(AppProperties.userDir, \"Library/Application Support/Chromium/NativeMessagingHosts\",\n            \"${AppInfo.packageName}.json\"\n        )\n\n\n\n    override fun updateManifests(manifests: NativeMessagingManifests) {\n        listOf(\n            firefoxNativeMessagingPath,\n            chromeNativeMessagingPath,\n            chromiumNativeMessagingPath\n        ).forEach { it.createParentDirectories() }\n\n        firefoxNativeMessagingPath.writeText(serialize(manifests.firefoxNativeMessagingManifest))\n        val chromeManifestString=serialize(manifests.chromeNativeMessagingManifest)\n        chromeNativeMessagingPath.writeText(chromeManifestString)\n        chromiumNativeMessagingPath.writeText(chromeManifestString)\n    }\n\n    override fun removeManifests() {\n        firefoxNativeMessagingPath.deleteIfExists()\n        chromeNativeMessagingPath.deleteIfExists()\n        chromiumNativeMessagingPath.deleteIfExists()\n    }\n}\n\nclass LinuxNativeMessagingManifestApplier : NativeMessagingManifestApplier() {\n    private val firefoxNativeMessagingPath\n        get() = Path(AppProperties.userDir, \".mozilla/native-messaging-hosts\", \"${AppInfo.packageName}.json\")\n    private val chromeNativeMessagingPath\n        get() = Path(AppProperties.userDir, \".config/google-chrome/NativeMessagingHosts\", \"${AppInfo.packageName}.json\")\n    private val chromiumNativeMessagingPath\n        get() = Path(AppProperties.userDir, \".config/chromium/NativeMessagingHosts\", \"${AppInfo.packageName}.json\")\n\n    override fun updateManifests(manifests: NativeMessagingManifests) {\n        listOf(\n            firefoxNativeMessagingPath,\n            chromeNativeMessagingPath,\n            chromiumNativeMessagingPath\n        ).forEach { it.createParentDirectories() }\n\n        firefoxNativeMessagingPath.writeText(serialize(manifests.firefoxNativeMessagingManifest))\n        val chromeManifestString = serialize(manifests.chromeNativeMessagingManifest)\n        chromeNativeMessagingPath.writeText(chromeManifestString)\n        chromiumNativeMessagingPath.writeText(chromeManifestString)\n    }\n\n    override fun removeManifests() {\n        firefoxNativeMessagingPath.deleteIfExists()\n        chromeNativeMessagingPath.deleteIfExists()\n        chromiumNativeMessagingPath.deleteIfExists()\n    }\n}\n\nclass NoOpNativeMessagingApplier : NativeMessagingManifestApplier() {\n    override fun updateManifests(manifests: NativeMessagingManifests) {\n        //no-op\n    }\n\n    override fun removeManifests() {\n        //no-op\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/AutoConfigurableProxyProviderForDesktop.kt",
    "content": "package com.abdownloadmanager.desktop.utils.proxy\n\nimport com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector\nimport com.github.markusbernhardt.proxy.selector.misc.ProxyListFallbackSelector\nimport com.github.markusbernhardt.proxy.selector.pac.PacProxySelector\nimport com.github.markusbernhardt.proxy.selector.pac.UrlPacScriptSource\nimport ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider\nimport java.net.ProxySelector\n\nclass AutoConfigurableProxyProviderForDesktop(\n    private val proxyCachingConfig: ProxyCachingConfig\n) : AutoConfigurableProxyProvider {\n    @Volatile\n    private var packProxySelector: ProxySelector? = null\n\n    @Volatile\n    private var lastUsedUri: String? = null\n    override fun getAutoConfigurableProxy(uri: String): ProxySelector? {\n        if (lastUsedUri == uri) {\n            val o = packProxySelector\n            return o ?: createAndInitializePacProxySelector(uri)\n        } else {\n            return createAndInitializePacProxySelector(uri)\n        }\n    }\n\n    private fun createAndInitializePacProxySelector(uri: String): ProxySelector {\n        synchronized(this) {\n            val s = installBufferingAndFallbackBehaviour(PacProxySelector(UrlPacScriptSource(uri)))\n            lastUsedUri = uri\n            packProxySelector = s\n            return s\n        }\n    }\n\n    private fun installBufferingAndFallbackBehaviour(selector: ProxySelector): ProxySelector {\n        var selector = selector\n        if (selector is PacProxySelector) {\n            if (proxyCachingConfig.pacCacheSize > 0) {\n                selector = BufferedProxySelector(\n                    proxyCachingConfig.pacCacheSize,\n                    proxyCachingConfig.pacCacheTTL,\n                    selector,\n                    proxyCachingConfig.pacCacheScope\n                )\n            }\n            selector = ProxyListFallbackSelector(selector)\n        }\n        return selector\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/DesktopSystemProxySelectorProvider.kt",
    "content": "package com.abdownloadmanager.desktop.utils.proxy\n\nimport com.github.markusbernhardt.proxy.ProxySearch\nimport ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider\nimport java.net.ProxySelector\n\nclass DesktopSystemProxySelectorProvider(\n    private val proxyCachingConfig: ProxyCachingConfig\n) : SystemProxySelectorProvider {\n    private val proxySearch by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {\n        createProxySearch()\n    }\n\n    private fun createProxySearch(): ProxySearch {\n        return ProxySearch.getDefaultProxySearch().apply {\n            setPacCacheSettings(\n                proxyCachingConfig.pacCacheSize,\n                proxyCachingConfig.pacCacheTTL,\n                proxyCachingConfig.pacCacheScope\n            )\n        }\n    }\n\n    override fun getSystemProxySelector(): ProxySelector? {\n        return proxySearch.proxySelector\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/ProxyCachingConfig.kt",
    "content": "package com.abdownloadmanager.desktop.utils.proxy\n\nimport com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector\n\nclass ProxyCachingConfig(\n    val pacCacheSize: Int,\n    val pacCacheTTL: Long,\n    val pacCacheScope: BufferedProxySelector.CacheScope\n) {\n    companion object {\n        fun default() = ProxyCachingConfig(\n            pacCacheSize = 10,\n            pacCacheTTL = 60 * 60 * 1000,\n            pacCacheScope = BufferedProxySelector.CacheScope.CACHE_SCOPE_URL\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/renderapi/RenderApi.kt",
    "content": "package com.abdownloadmanager.desktop.utils.renderapi\n\nimport com.abdownloadmanager.shared.util.BaseStorage\nimport ir.amirab.util.createParentDirectories\nimport ir.amirab.util.deleteIfExists\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.readText\nimport ir.amirab.util.writeText\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport okio.Path\n\n/**\n * this class provides these features\n * - override the default render api on some platforms\n * - save/restore user selected render api\n * ### Note\n * [boot] must be called before any compose UI logic.\n * @param file the file path the to save the renderApi backend\n */\nclass CustomRenderApi(\n    private val file: Path\n) : BaseStorage<RenderApi?>() {\n    fun boot() {\n        startPersistData()\n        val customRenderApiRequested =\n            System.getenv(\"SKIKO_RENDER_API\") != null ||\n                    System.getProperty(\"skiko.renderApi\") != null\n        if (!customRenderApiRequested) {\n            val renderApi = getSavedValueIfSupported()\n                ?: getOurDefaultRenderApi()\n            if (renderApi != null) {\n                System.setProperty(\"skiko.renderApi\", renderApi.name)\n            }\n        }\n    }\n\n\n    private fun getSavedValueIfSupported(): RenderApi? {\n        return get()?.takeIf {\n            isRenderApiSupportedInThisPlatform(it)\n        }\n    }\n\n    private fun get(): RenderApi? {\n        return this.data.value\n    }\n\n    private fun getOurDefaultRenderApi(): RenderApi? {\n        return when (Platform.getCurrentPlatform()) {\n            Platform.Desktop.Linux -> RenderApi.SOFTWARE\n            Platform.Desktop.Windows -> {\n                val arch = Arch.getCurrentArch()\n                when (arch) {\n                    Arch.X64 -> RenderApi.OPENGL\n                    Arch.Arm64 -> RenderApi.ANGLE\n                    else -> null\n                }\n            }\n\n            else -> null\n        }\n    }\n\n    private fun isRenderApiSupportedInThisPlatform(renderApi: RenderApi): Boolean {\n        return getSupportedRenderApiForThisPlatform().contains(renderApi)\n    }\n\n    fun getSupportedRenderApiForThisPlatform(): List<RenderApi> {\n        return supportedRenderApisPerPlatform.getOrDefault(Platform.getCurrentPlatform(), emptyList())\n    }\n\n    private val supportedRenderApisPerPlatform = mapOf(\n        Platform.Desktop.Windows to listOf(\n            RenderApi.DIRECT3D,\n            RenderApi.OPENGL,\n            RenderApi.ANGLE,\n            RenderApi.SOFTWARE,\n        ),\n        Platform.Desktop.Linux to listOf(\n            RenderApi.OPENGL,\n            RenderApi.SOFTWARE,\n        ),\n        Platform.Desktop.MacOS to listOf(\n            RenderApi.METAL,\n            RenderApi.OPENGL,\n            RenderApi.SOFTWARE,\n        ),\n    )\n\n    override val inMemoryState: MutableStateFlow<RenderApi?> = MutableStateFlow(\n        runCatching {\n            RenderApi.valueOf(file.readText())\n        }.getOrNull()\n    )\n\n    override suspend fun saveData(data: RenderApi?) {\n        runCatching {\n            if (data != null) {\n                file.createParentDirectories()\n                file.writeText(data.name)\n            } else {\n                file.deleteIfExists()\n            }\n        }\n    }\n}\n\nenum class RenderApi(val prettyName: String) {\n    SOFTWARE(\"Software\"),\n    DIRECT3D(\"DirectX\"),\n    METAL(\"Metal\"),\n    OPENGL(\"Open GL\"),\n    ANGLE(\"ANGLE\"),\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/Comunication.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.serializer\nimport org.http4k.client.OkHttp\nimport org.http4k.core.*\nimport org.http4k.lens.BiDiBodyLensSpec\nimport org.http4k.lens.string\nimport org.http4k.routing.RoutingHttpHandler\nimport org.http4k.routing.bind\nimport kotlin.reflect.KType\nimport kotlin.reflect.typeOf\n\ninline fun<reified T:Any> Command(name: String):Command<T>{\n    return Command(name, typeOf<T>())\n}\nclass Command<T : Any>(val name: String, val type: KType)\nsealed class CommandResult<T : Any> {\n    override fun toString(): String {\n        return this::class.simpleName!!\n    }\n    fun orElse(block:(Error<T>)->T):T{\n        return when(this){\n            is Success->value\n            else ->block(this as Error<T>)\n        }\n    }\n\n    fun <R>fold(\n        onError:(Error<T>)->R,\n        onSuccess:(Success<T>)->R,\n    ):R{\n        return when(this){\n            is Error -> {\n                onError(this)\n            }\n            is Success -> {\n                onSuccess(this)\n            }\n        }\n    }\n\n\n\n    data class Success<T : Any>(val value: T) : CommandResult<T>()\n    open class Error<T : Any>(val value: String) : CommandResult<T>(){\n        override fun toString(): String {\n            val name= super.toString()\n            return \"\"\"\n                $name , $value\n            \"\"\".trimIndent()\n        }\n    }\n    class ServerNotExists<T : Any> : Error<T>(\"server not exists\")\n    class ClientError<T : Any>(value: String) : Error<T>(\"client error $value\")\n    class ServerError<T : Any>(code: Int, message: String, body: String) : Error<T>(\"\"\"\nserver error\n    statusCode: $code , message: $message\n    body: $body\n    \"\"\".trimIndent()\n    )\n}\ninfix fun <T : Any> Command<T>.bindSafe(\n    handle: (Request) -> T,\n): RoutingHttpHandler {\n    return name bind {\n        Response(Status.OK)\n            .with(json(handle(it),type))\n    }\n}\n\nprivate val client by lazy {\n    OkHttp()\n}\n\n\ninternal fun <T : Any> typeSafeRequest(\n    port:Int,\n    command: Command<T>\n): CommandResult<T> {\n    val autoBody = Body.auto<T>(command.type).toLens()\n    val request = Request(\n        method = Method.GET,\n        uri = Uri.of(\"http://localhost:$port/${command.name}\"),\n    )\n    val response = try {\n        client(request)\n    } catch (e: Exception) {\n        return CommandResult.ClientError(e.localizedMessage)\n    }\n    return if (response.status.successful) {\n        try {\n            CommandResult.Success(autoBody(response))\n        } catch (e: Exception) {\n            CommandResult.Error(e.localizedMessage)\n        }\n    } else {\n        CommandResult.ServerError(\n            response.status.code,\n            response.status.description,\n            response.bodyString(),\n        )\n    }\n}\n\nprivate val json by lazy {\n    Json\n}\nfun <T> toJson(data: T, serializer: KSerializer<T>): String {\n    return json.encodeToString(serializer, data)\n}\n\nfun <T> fromJson(string: String, serializer: KSerializer<T>): T {\n    return json.decodeFromString(serializer, string)\n}\n\ninline fun <reified T> toJson(data: T): String {\n    return toJson(data, serializer())\n}\n\ninline fun <reified T> fromJson(string: String): T {\n    return fromJson(string, serializer())\n}\n\ninline fun <reified T> Body.Companion.auto(): BiDiBodyLensSpec<T> {\n    return Body.string(ContentType.APPLICATION_JSON)\n        .map(\n            nextIn = {\n                fromJson<T>(it)\n            },\n            nextOut = {\n                toJson<T>(it)\n            },\n        )\n}\n\n//unchecked auto!!\nfun <T> Body.Companion.auto(kType: KType): BiDiBodyLensSpec<T> {\n    val serializer = serializer(kType) as KSerializer<T>\n    return Body.string(ContentType.APPLICATION_JSON)\n        .map(\n            nextIn = {\n                fromJson(it, serializer)\n            },\n            nextOut = {\n                toJson(it, serializer)\n            },\n        )\n}\n\n\ninline fun <reified T : Any> json(data: T): (Response) -> Response {\n    return Body.auto<T>().toLens() of data\n}\nfun <T : Any> json(data: T,type: KType): (Response) -> Response {\n    return Body.auto<T>(type).toLens() of data\n}\n\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/MutableSingleInstanceServerHandler.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport org.http4k.core.HttpHandler\nimport org.http4k.core.Response\nimport org.http4k.core.Status\nimport org.http4k.routing.bind\nimport org.http4k.routing.orElse\nimport org.http4k.routing.routes\n\nclass MutableSingleInstanceServerHandler : SingleInstanceServerHandler {\n    private val handlers = mutableListOf<Pair<Command<Any>, () -> Any>>()\n\n    private var mainHandler = createRoutes()\n\n    private fun createRoutes(): HttpHandler {\n        val handlersArray = handlers.map { (cmd, handler) ->\n            cmd bindSafe {\n                handler()\n            }\n        }.toTypedArray()\n        return routes(\n            *handlersArray,\n            // add this since empty routes will crash\n            orElse bind {\n                Response(Status.NOT_FOUND)\n            }\n        )\n    }\n\n    override val handler: HttpHandler\n        get() = mainHandler\n\n    fun updateRoutes() {\n        mainHandler = createRoutes()\n    }\n\n    fun <T : Any> add(command: Command<T>, handle: () -> T) {\n        synchronized(this) {\n            val element = (command to handle) as Pair<Command<Any>, () -> Any>\n            handlers.add(element)\n            updateRoutes()\n        }\n    }\n\n    fun <T : Any> remove(command: Command<T>) {\n        synchronized(this) {\n            handlers.removeIf {\n                it.first == command\n            }\n            updateRoutes()\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/SingleAppInstanceLocker.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport okio.Path\nimport java.io.RandomAccessFile\nimport kotlin.concurrent.thread\nimport kotlin.io.path.createParentDirectories\n\nclass SingleAppInstanceLocker(\n    private val lockPath: Path\n) {\n    private fun getLockPath(): Path {\n        return lockPath\n    }\n\n    /**\n     * @throws AnotherInstanceIsRunning\n     */\n    fun tryLockInstance() {\n        val file = getLockPath()\n            .also {\n                it.toNioPath().createParentDirectories()\n            }\n            .toFile()\n\n        val raf = RandomAccessFile(file, \"rw\")\n        val lock = kotlin\n            .runCatching { raf.channel.tryLock() }\n            .getOrElse { null }\n        if (lock != null) {\n            //we get a lock\n            Runtime\n                .getRuntime()\n                .addShutdownHook(thread(start = false) {\n                    lock.release()\n                    raf.close()\n                    file.delete()\n                })\n            return\n        } else {\n            throw AnotherInstanceIsRunning()\n        }\n\n    }\n}\n\nclass AnotherInstanceIsRunning(\n) : RuntimeException(\"Another Instance Is Running\")\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/SingleInstanceServer.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport ir.amirab.util.http4k.NanoHttp\nimport okio.*\nimport org.http4k.core.HttpHandler\nimport org.http4k.core.Request\nimport org.http4k.core.then\nimport org.http4k.filter.ServerFilters\nimport org.http4k.server.asServer\nimport kotlin.concurrent.thread\nimport kotlin.io.path.exists\nimport kotlin.io.path.readText\n\nclass SingleInstanceServer(\n    private val portPath: Path\n) {\n    private var serverInfo: ServerInfo? = null\n    fun start(handle: HttpHandler) {\n        val server = createServer(handle)\n        server.start()\n        portPath.toFile().writeText(server.getPort().toString())\n        Runtime\n            .getRuntime()\n            .addShutdownHook(thread(start = false) {\n                stop()\n            })\n\n    }\n\n    fun stop() {\n        portPath.toFile().delete()\n        serverInfo?.let {\n            it.stop()\n        }\n    }\n\n    private fun createServer(handle: HttpHandler): ServerInfo {\n        val middlewares=ServerFilters.CatchAll.invoke{\n            it.printStackTrace()\n            ServerFilters.CatchAll.originalBehaviour(it)\n        }\n        val appRoutes = {req:Request->\n//            println(\"new request $req\")\n            handle(req)\n        }\n        val server = middlewares\n            .then(appRoutes)\n            .asServer(NanoHttp(\"localhost\",0))\n        return ServerInfo(\n            getPort = { server.port() },\n            start = { server.start() },\n            stop = { server.stop() },\n        )\n    }\n\n    fun <T:Any>sendMessage(message: Command<T>): CommandResult<T> {\n        val port = portPath\n            .toNioPath()\n            .takeIf { it.exists() }\n            ?.runCatching { readText() }\n            ?.getOrNull()\n            ?.toIntOrNull()\n                ?: return CommandResult.ServerNotExists()\n        return typeSafeRequest(port, message)\n    }\n}\n\n\nprivate data class ServerInfo(\n    val getPort: ()->Int,\n    val start: () -> Unit,\n    val stop: () -> Unit,\n)"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/SingleInstanceServerHandler.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport org.http4k.core.HttpHandler\nimport org.http4k.core.Request\nimport org.http4k.core.Response\n\ninterface SingleInstanceServerHandler : HttpHandler {\n    val handler: HttpHandler\n    override fun invoke(request: Request): Response {\n        return handler(request)\n    }\n}\n"
  },
  {
    "path": "desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/singleInstance/SingleInstanceUtil.kt",
    "content": "package com.abdownloadmanager.desktop.utils.singleInstance\n\nimport okio.Path\n\nclass SingleInstanceUtil(baseFolder: Path) {\n    private val locker by lazy {\n        SingleAppInstanceLocker(baseFolder / \"app.lock\")\n    }\n    private val server by lazy {\n        SingleInstanceServer(baseFolder / \"app.port\")\n    }\n\n    fun <T:Any>sendToInstance(msg: Command<T>): CommandResult<T> {\n        return server.sendMessage(msg).also {\n//            println(\"server respond with ${it}\")\n        }\n    }\n\n    @Throws(AnotherInstanceIsRunning::class)\n    fun lockInstance(\n        createMessageHandler: () -> SingleInstanceServerHandler,\n    ) {\n        locker.tryLockInstance()\n\n        // we are alone so we create the server\n        server.start(createMessageHandler())\n    }\n}"
  },
  {
    "path": "desktop/app/src/main/resources/configs/app_default.properties",
    "content": "app.debug=\"false\"\n"
  },
  {
    "path": "desktop/app-utils/build.gradle.kts",
    "content": "plugins {\n    id(MyPlugins.kotlin)\n    id(MyPlugins.composeDesktop)\n}\ndependencies {\n    api(project(\":desktop:shared\"))\n    api(project(\":shared:app\"))\n    implementation(libs.jbrApi)\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/BrowserUtil.kt",
    "content": "package com.abdownloadmanager.desktop.window\n\nimport com.abdownloadmanager.shared.util.BrowserType\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.asDesktop\nimport ir.amirab.util.toUpUntil\nimport java.io.File\n\n\nabstract class Browser {\n    abstract fun getPossibleExecutablePaths(): List<File>\n\n    fun isInstalled(): Boolean {\n        return getExecutablePath() != null\n    }\n\n    open fun openLink(url: String): Boolean {\n        val executablePath = getExecutablePath()\n        if (executablePath == null) {\n            return false\n        }\n        val cmd = when (Platform.asDesktop()) {\n            Platform.Desktop.Linux -> arrayOf(executablePath.path, url)\n            Platform.Desktop.Windows -> arrayOf(executablePath.path, url)\n            Platform.Desktop.MacOS -> {\n                val appFolder = executablePath.toUpUntil {\n                    // in macOS app folders are like /Applications/App Name.app/\n                    it.name.endsWith(\".app\")\n                }?.path\n                if (appFolder == null) {\n                    return false\n                }\n                arrayOf(\"open\", \"-a\", appFolder, url)\n            }\n        }\n        Runtime.getRuntime().exec(cmd)\n        return true\n    }\n\n    fun getExecutablePath(): File? {\n        return getPossibleExecutablePaths().firstOrNull { it.exists() }\n    }\n\n    companion object {\n        fun getBrowserByType(type: BrowserType): Browser? {\n            return when (type) {\n                BrowserType.Chrome -> ChromeBrowser\n                BrowserType.Firefox -> FirefoxBrowser\n                BrowserType.Edge -> EdgeBrowser\n                BrowserType.Opera -> OperaBrowser\n            }\n        }\n    }\n}\n\nobject FirefoxBrowser : Browser() {\n    override fun getPossibleExecutablePaths(): List<File> {\n        return when (Platform.asDesktop()) {\n            Platform.Desktop.Windows -> {\n                val firefoxExe = \"Mozilla Firefox\\\\firefox.exe\"\n                buildList {\n                    listOf(\n                        \"ProgramW6432\",\n                        \"ProgramFiles\",\n                        \"ProgramFiles(x86)\",\n                        \"LOCALAPPDATA\",\n                    ).forEach {\n                        System.getenv(it)?.let { path ->\n                            add(File(path, firefoxExe))\n                        }\n                    }\n                }\n            }\n\n            Platform.Desktop.MacOS -> {\n                listOf(\n                    File(\"/Applications/Firefox.app\"),\n                )\n            }\n\n            Platform.Desktop.Linux -> {\n                listOf(\n                    File(\"/usr/bin/firefox\"),\n                    File(\"/usr/bin/firefox-bin\"),\n                )\n            }\n        }\n    }\n}\n\nobject ChromeBrowser : Browser() {\n    override fun getPossibleExecutablePaths(): List<File> {\n        return when (Platform.asDesktop()) {\n            Platform.Desktop.Windows -> {\n                val chromeExe = \"Google\\\\Chrome\\\\Application\\\\chrome.exe\"\n                buildList {\n                    listOf(\n                        \"ProgramW6432\",\n                        \"PROGRAMFILES\",\n                        \"PROGRAMFILES(X86)\",\n                        \"LOCALAPPDATA\",\n                    ).forEach {\n                        System.getenv(it)?.let { path ->\n                            add(File(path, chromeExe))\n                        }\n                    }\n                }\n            }\n\n            Platform.Desktop.MacOS -> {\n                listOf(\n                    File(\"/Applications/Google Chrome.app\")\n                )\n            }\n\n            Platform.Desktop.Linux -> {\n                listOf(\n                    File(\"/usr/bin/google-chrome\"),\n                    File(\"/usr/bin/chromium-browser\"),\n                    File(\"/usr/bin/chromium\")\n                )\n            }\n        }\n    }\n}\n\nobject EdgeBrowser : Browser() {\n    override fun getPossibleExecutablePaths(): List<File> {\n        return when (Platform.asDesktop()) {\n            Platform.Desktop.Linux -> {\n                listOf(\n                    File(\"/usr/bin/microsoft-edge\"),\n                )\n            }\n\n            Platform.Desktop.MacOS -> {\n                listOf(\n                    File(\"/Applications/Microsoft Edge.app\"),\n                )\n            }\n\n            Platform.Desktop.Windows -> {\n                val child = \"Microsoft\\\\Edge\\\\Application\\\\msedge.exe\"\n                buildList {\n                    listOf(\n                        \"ProgramW6432\",\n                        \"PROGRAMFILES\",\n                        \"PROGRAMFILES(X86)\",\n                        \"LOCALAPPDATA\",\n                    ).forEach {\n                        System.getenv(it)?.let { path ->\n                            add(File(path, child))\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nobject OperaBrowser : Browser() {\n    override fun getPossibleExecutablePaths(): List<File> {\n        return when (Platform.asDesktop()) {\n            Platform.Desktop.Windows -> {\n                val relativeExe = \"Opera\\\\launcher.exe\"\n                buildList {\n                    System.getenv(\"LOCALAPPDATA\")?.let { path ->\n                        add(File(path, \"Programs\\\\$relativeExe\"))\n                    }\n                    listOf(\n                        \"ProgramW6432\",\n                        \"PROGRAMFILES\",\n                        \"PROGRAMFILES(X86)\",\n                    ).forEach {\n                        System.getenv(it)?.let { path ->\n                            add(File(path, relativeExe))\n                        }\n                    }\n                }\n            }\n\n            Platform.Desktop.MacOS -> {\n                listOf(\n                    File(\"/Applications/Opera.app\"),\n                )\n            }\n\n            Platform.Desktop.Linux -> {\n                listOf(\n                    File(\"/usr/bin/opera\"),\n                )\n            }\n\n        }\n\n    }\n\n}\n\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/CustomWindow.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom\n\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport ir.amirab.util.compose.IconSource\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.waitForUpOrCancellation\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.window.WindowDraggableArea\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.input.pointer.*\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalViewConfiguration\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.FrameWindowScope\nimport androidx.compose.ui.window.Window\nimport androidx.compose.ui.window.WindowPlacement\nimport androidx.compose.ui.window.WindowState\nimport com.abdownloadmanager.desktop.window.custom.titlebar.TitleBar\nimport com.abdownloadmanager.shared.util.PopUpContainer\nimport com.abdownloadmanager.shared.util.ResponsiveBox\nimport com.abdownloadmanager.shared.util.ui.WithTitleBarDirection\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.LocalUiScale\nimport com.abdownloadmanager.shared.util.ui.theme.UiScaledContent\nimport com.jetbrains.JBR\nimport com.jetbrains.WindowDecorations\nimport com.jetbrains.WindowMove\nimport ir.amirab.util.desktop.LocalFrameWindowScope\nimport com.abdownloadmanager.shared.ui.util.LocalWindow\nimport ir.amirab.util.desktop.screen.applyUiScale\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.withContext\nimport java.awt.event.ComponentAdapter\nimport java.awt.event.ComponentEvent\nimport java.awt.event.MouseEvent\n\n\n// a window frame which totally rendered with compose\n@Composable\nprivate fun FrameWindowScope.CustomWindowFrame(\n    onRequestMinimize: (() -> Unit)?,\n    onRequestClose: () -> Unit,\n    onRequestToggleMaximize: (() -> Unit)?,\n    title: String,\n    titlePosition: TitlePosition,\n    windowIcon: Painter? = null,\n    background: Color,\n    onBackground: Color,\n    isLight: Boolean,\n    start: (@Composable () -> Unit)?,\n    end: (@Composable () -> Unit)?,\n    content: @Composable () -> Unit,\n) {\n//    val borderColor = MaterialTheme.colors.surface\n    WithContentColor(onBackground) {\n        Column(\n            Modifier\n                .fillMaxSize()\n                .ifThen(!JBR.isWindowDecorationsSupported()) {\n                    ifThen(isWindowFloating()) {\n                        val borderColor = if (isLight) Color.LightGray else Color.DarkGray\n                        border(1.dp, borderColor, RectangleShape)\n                            .padding(1.dp)\n                    }\n                }\n                .background(background)\n        ) {\n            WithTitleBarDirection {\n                SnapDraggableToolbar(\n                    title = title,\n                    windowIcon = windowIcon,\n                    titlePosition = titlePosition,\n                    start = start,\n                    end = end,\n                    onRequestMinimize = onRequestMinimize,\n                    onRequestClose = onRequestClose,\n                    onRequestToggleMaximize = onRequestToggleMaximize\n                )\n            }\n\n            content()\n        }\n    }\n}\n\n@Composable\nfun isWindowFocused(): Boolean {\n    return LocalWindowInfo.current.isWindowFocused\n}\n\n@Composable\nfun isWindowMaximized(): Boolean {\n    return LocalWindowState.current.placement == WindowPlacement.Maximized\n}\n\n@Composable\nfun isWindowFloating(): Boolean {\n    return LocalWindowState.current.placement == WindowPlacement.Floating\n}\n\n@Composable\nfun FrameWindowScope.SnapDraggableToolbar(\n    title: String,\n    windowIcon: Painter? = null,\n    titlePosition: TitlePosition,\n    start: (@Composable () -> Unit)?,\n    end: (@Composable () -> Unit)?,\n    onRequestMinimize: (() -> Unit)?,\n    onRequestToggleMaximize: (() -> Unit)?,\n    onRequestClose: () -> Unit,\n) {\n    val titleBar = TitleBar.getPlatformTitleBar()\n    if (JBR.isWindowDecorationsSupported()) {\n        val density = LocalDensity.current\n        val uiScale = LocalUiScale.current\n        fun computeHeaderHeight(height: Dp): Float {\n            return height.value.applyUiScale(uiScale)\n        }\n\n        var headerHeight by remember {\n            mutableStateOf(computeHeaderHeight(titleBar.titleBarHeight))\n        }\n        val customTitleBar = remember {\n            JBR.getWindowDecorations().createCustomTitleBar()\n        }\n        LaunchedEffect(headerHeight) {\n            customTitleBar.height = headerHeight\n            customTitleBar.putProperty(\"controls.visible\", false)\n            val previousPlacement = window.placement\n            JBR.getWindowDecorations().setCustomTitleBar(window, customTitleBar)\n            // JBR resets window placement to Floating so we should restore our placement\n            // is there a better way?\n            if (window.placement != previousPlacement) {\n                withContext(Dispatchers.Main) {\n                    window.placement = previousPlacement\n                }\n            }\n        }\n        Box(\n            Modifier\n                .onSizeChanged {\n                    headerHeight = computeHeaderHeight(\n                        density.run { it.height.toDp() }\n                    )\n                }\n        ) {\n            Spacer(\n                Modifier\n                    .matchParentSize()\n                    .customTitleBarMouseEventHandler(customTitleBar)\n            )\n            FrameContent(\n                titleBar = titleBar,\n                modifier = Modifier,\n                title = title,\n                windowIcon = windowIcon,\n                titlePosition = titlePosition,\n                start = start,\n                end = end,\n                onRequestMinimize = onRequestMinimize,\n                onRequestToggleMaximize = onRequestToggleMaximize,\n                onRequestClose = onRequestClose\n            )\n        }\n    } else {\n        SystemDraggableSection(\n            onRequestToggleMaximize = onRequestToggleMaximize,\n        ) { modifier ->\n            FrameContent(\n                titleBar = titleBar,\n                modifier = modifier,\n                title = title,\n                windowIcon = windowIcon,\n                titlePosition = titlePosition,\n                start = start,\n                end = end,\n                onRequestMinimize = onRequestMinimize,\n                onRequestToggleMaximize = onRequestToggleMaximize,\n                onRequestClose = onRequestClose\n            )\n        }\n    }\n}\n\n@Composable\ninternal fun FrameWindowScope.SystemDraggableSection(\n    onRequestToggleMaximize: (() -> Unit)?,\n    content: @Composable (Modifier) -> Unit\n) {\n    val windowMove: WindowMove? = JBR.getWindowMove()\n    val viewConfig = LocalViewConfiguration.current\n    var lastPress by remember { mutableStateOf(0L) }\n    if (windowMove != null) {\n        content(\n            Modifier\n                .onPointerEvent(PointerEventType.Press, PointerEventPass.Main) {\n                    if (\n                        this.currentEvent.button == PointerButton.Primary &&\n                        this.currentEvent.changes.any { changed -> !changed.isConsumed }\n                    ) {\n                        windowMove.startMovingTogetherWithMouse(window, MouseEvent.BUTTON1)\n                        if (\n                            System.currentTimeMillis() - lastPress in\n                            viewConfig.doubleTapMinTimeMillis..viewConfig.doubleTapTimeoutMillis\n                        ) {\n                            onRequestToggleMaximize?.invoke()\n                        }\n                        lastPress = System.currentTimeMillis()\n                    }\n                },\n        )\n    } else {\n        WindowDraggableArea {\n            content(Modifier)\n        }\n    }\n}\n\ninternal fun Modifier.customTitleBarMouseEventHandler(\n    titleBar: WindowDecorations.CustomTitleBar\n): Modifier =\n    pointerInput(Unit) {\n        val currentContext = currentCoroutineContext()\n        awaitPointerEventScope {\n            var inUserControl = false\n            while (currentContext.isActive) {\n                val event = awaitPointerEvent(PointerEventPass.Main)\n                event.changes.forEach {\n                    if (!it.isConsumed && !inUserControl) {\n                        titleBar.forceHitTest(false)\n                    } else {\n                        if (event.type == PointerEventType.Press) {\n                            inUserControl = true\n                        }\n                        if (event.type == PointerEventType.Release) {\n                            inUserControl = false\n                        }\n                        titleBar.forceHitTest(true)\n                    }\n                }\n            }\n        }\n    }\n\n@Composable\nprivate fun FrameWindowScope.getCurrentWindowSize(): DpSize {\n    var windowSize by remember {\n        mutableStateOf(DpSize(window.width.dp, window.height.dp))\n    }\n    //observe window size\n    DisposableEffect(window) {\n        val listener = object : ComponentAdapter() {\n            override fun componentResized(p0: ComponentEvent?) {\n                windowSize = DpSize(window.width.dp, window.height.dp)\n            }\n        }\n        window.addComponentListener(listener)\n        onDispose {\n            window.removeComponentListener(listener)\n        }\n    }\n    return windowSize\n}\n\n\n@Composable\nprivate fun FrameContent(\n    titleBar: TitleBar,\n    modifier: Modifier,\n    title: String,\n    windowIcon: Painter? = null,\n    titlePosition: TitlePosition,\n    start: (@Composable () -> Unit)?,\n    end: (@Composable () -> Unit)?,\n    onRequestMinimize: (() -> Unit)?,\n    onRequestToggleMaximize: (() -> Unit)?,\n    onRequestClose: () -> Unit,\n) {\n    titleBar.RenderTitleBar(\n        titleBar = titleBar,\n        modifier = modifier\n            .fillMaxWidth(),\n        title = title,\n        windowIcon = windowIcon,\n        titlePosition = titlePosition,\n        start = start,\n        end = end,\n        onRequestMinimize = onRequestMinimize,\n        onRequestToggleMaximize = onRequestToggleMaximize,\n        onRequestClose = onRequestClose,\n    )\n}\n\n\nprivate val defaultAppIcon: IconSource\n    @Composable\n    get() {\n        return MyIcons.appIcon\n    }\n\nprivate fun Color.toWindowColorType() = java.awt.Color(\n    red, green, blue\n)\n\n@Composable\nfun CustomWindow(\n    state: WindowState,\n    onCloseRequest: () -> Unit,\n    resizable: Boolean = true,\n    onRequestMinimize: (() -> Unit)? = {\n        state.isMinimized = true\n    },\n    onRequestToggleMaximize: (() -> Unit)? = {\n        if (state.placement == WindowPlacement.Maximized) {\n            state.placement = WindowPlacement.Floating\n        } else {\n            state.placement = WindowPlacement.Maximized\n        }\n    },\n    windowController: WindowController = remember {\n        WindowController()\n    },\n    onKeyEvent: (KeyEvent) -> Boolean = { false },\n    alwaysOnTop: Boolean = false,\n    preventMinimize: Boolean = onRequestMinimize == null,\n    content: @Composable FrameWindowScope.() -> Unit,\n) {\n    val start = windowController.start\n    val end = windowController.end\n    val title = windowController.title.orEmpty()\n    val titlePosition = windowController.titlePosition\n    val icon = windowController.icon ?: defaultAppIcon.rememberPainter()\n\n\n    val undecorated: Boolean\n    val isAeroSnapSupported = JBR.isWindowDecorationsSupported()\n    if (isAeroSnapSupported) {\n        //we use aero snap\n        undecorated = false\n    } else {\n        //we decorate window and add our custom layout\n        undecorated = true\n    }\n    Window(\n        state = state,\n        transparent = false,\n        undecorated = undecorated,\n        icon = icon,\n        title = title,\n        resizable = resizable,\n        onCloseRequest = onCloseRequest,\n        onKeyEvent = onKeyEvent,\n        alwaysOnTop = alwaysOnTop,\n    ) {\n        val isLight = myColors.isLight\n        val background = myColors.background\n        LaunchedEffect(background) {\n            withContext(Dispatchers.Main) {\n                //I set window background fix window edge flickering on window resize\n                window.background = background.takeOrElse {\n                    if (isLight) Color.White\n                    else Color.Black\n                }.toWindowColorType()\n            }\n        }\n        UiScaledContent {\n            CompositionLocalProvider(\n                LocalWindowController provides windowController,\n                LocalWindowState provides state,\n                LocalWindow provides window,\n                LocalFrameWindowScope provides this\n            ) {\n                if (preventMinimize) {\n                    PreventMinimize()\n                }\n                // a window frame which totally rendered with compose\n                CustomWindowFrame(\n                    onRequestMinimize = onRequestMinimize,\n                    onRequestClose = onCloseRequest,\n                    onRequestToggleMaximize = onRequestToggleMaximize,\n                    title = title,\n                    titlePosition = titlePosition,\n                    windowIcon = icon,\n                    background = background,\n                    onBackground = myColors.onBackground,\n                    isLight = isLight,\n                    start = start,\n                    end = end,\n                ) {\n                    ResponsiveBox {\n                        Box(Modifier.clearFocusOnTap()) {\n                            PopUpContainer {\n                                content()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PreventMinimize() {\n    val state = LocalWindowState.current\n    LaunchedEffect(state.isMinimized) {\n        if (state.isMinimized) {\n            state.isMinimized = false\n        }\n    }\n}\n\nprivate fun Modifier.clearFocusOnTap(): Modifier = composed {\n    val focusManager = LocalFocusManager.current\n    Modifier.pointerInput(Unit) {\n        awaitEachGesture {\n            awaitFirstDown(pass = PointerEventPass.Main)\n            val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Main)\n            if (upEvent != null) {\n                focusManager.clearFocus()\n            }\n        }\n    }\n}\n\nclass WindowController(\n    title: String? = null,\n    icon: Painter? = null,\n) {\n    var title by mutableStateOf(title)\n    var titlePosition by mutableStateOf(TitlePosition.default())\n    var icon by mutableStateOf(icon)\n    var start: (@Composable () -> Unit)? by mutableStateOf(null)\n    var end: (@Composable () -> Unit)? by mutableStateOf(null)\n}\n\n@Immutable\ndata class TitlePosition(\n    val centered: Boolean = false,\n    val afterStart: Boolean = false,\n    val padding: PaddingValues = PaddingValues(0.dp),\n) {\n    companion object {\n        fun default() = TitlePosition()\n    }\n}\n\n@Composable\nfun rememberWindowController(\n    title: String? = null,\n    icon: Painter? = null,\n): WindowController {\n    val controller = remember {\n        WindowController(\n            title, icon\n        )\n    }\n    LaunchedEffect(title) {\n        controller.title = title\n    }\n    LaunchedEffect(icon) {\n        controller.icon = icon\n    }\n    return controller\n}\n\nprivate val LocalWindowController =\n    compositionLocalOf<WindowController> { error(\"window controller not provided\") }\nprivate val LocalWindowState =\n    compositionLocalOf<WindowState> { error(\"window controller not provided\") }\n\n@Composable\nfun WindowStart(content: @Composable () -> Unit) {\n    val c = LocalWindowController.current\n    c.start = content\n    DisposableEffect(Unit) {\n        onDispose {\n            c.start = null\n        }\n    }\n}\n\n@Composable\nfun WindowEnd(content: @Composable () -> Unit) {\n    val c = LocalWindowController.current\n    c.end = content\n    DisposableEffect(Unit) {\n        onDispose {\n            c.end = null\n        }\n    }\n}\n\n@Composable\nfun WindowTitle(title: String) {\n    val c = LocalWindowController.current\n    LaunchedEffect(title) {\n        c.title = title\n    }\n    DisposableEffect(Unit) {\n        onDispose {\n            c.title = null\n        }\n    }\n}\n\n@Composable\nfun WindowTitlePosition(titlePosition: TitlePosition) {\n    val c = LocalWindowController.current\n    LaunchedEffect(titlePosition) {\n        c.titlePosition = titlePosition\n    }\n    DisposableEffect(Unit) {\n        onDispose {\n            c.titlePosition = TitlePosition.default()\n        }\n    }\n}\n\n@Composable\nfun WindowIcon(icon: IconSource) {\n    WindowIcon(icon.rememberPainter())\n}\n\n@Composable\nfun WindowIcon(icon: Painter) {\n    val current = LocalWindowController.current\n    DisposableEffect(icon) {\n        current.let {\n            it.icon = icon\n        }\n        onDispose {\n            current.let {\n                it.icon = null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/DropDownTooltip.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.Tooltip\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\n@Composable\nprivate fun SystemButtonTooltip(\n    stringSource: StringSource,\n    content: @Composable () -> Unit,\n) {\n    Tooltip(\n        tooltip = stringSource,\n        anchor = Alignment.BottomCenter,\n        alignment = Alignment.BottomCenter,\n        content = content,\n    )\n}\n\n@Composable\ninternal fun WindowCloseButtonTooltip(\n    content: @Composable () -> Unit\n) {\n    SystemButtonTooltip(\n        stringSource = Res.string.window_close.asStringSource(),\n    ) {\n        content()\n    }\n}\n\n@Composable\ninternal fun WindowToggleMaximizeTooltip(\n    content: @Composable () -> Unit\n) {\n    SystemButtonTooltip(\n        stringSource = if (isWindowMaximized()) {\n            Res.string.window_restore\n        } else {\n            Res.string.window_maximize\n        }.asStringSource(),\n    ) {\n        content()\n    }\n}\n\n@Composable\ninternal fun WindowMinimizeTooltip(\n    content: @Composable () -> Unit\n) {\n    SystemButtonTooltip(\n        stringSource = Res.string.window_minimize.asStringSource(),\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/OptionsDialog.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom\n\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.window.*\nimport com.abdownloadmanager.shared.util.ui.theme.UiScaledContent\nimport java.awt.event.WindowEvent\nimport java.awt.event.WindowFocusListener\n\n\n@Composable\nfun BaseOptionDialog(\n    onCloseRequest: () -> Unit,\n    state: DialogState = rememberDialogState(),\n    resizeable: Boolean = true,\n    content: @Composable WindowScope.() -> Unit,\n) {\n    DialogWindow(\n        visible = true,\n        state = state,\n        decoration = WindowDecoration.Undecorated(),\n        transparent = true,\n        resizable = resizeable,\n        //we need this to allow click outside\n        modalityType = DialogModalityType.Modeless,\n        onCloseRequest = onCloseRequest,\n    ) {\n        val focusListener = remember {\n            object : WindowFocusListener {\n                override fun windowGainedFocus(e: WindowEvent?) {\n                    //do nothing\n                }\n\n                override fun windowLostFocus(e: WindowEvent) {\n                    onCloseRequest()\n                }\n            }\n        }\n        DisposableEffect(window) {\n            window.addWindowFocusListener(focusListener)\n            window.isAlwaysOnTop = true\n            onDispose {\n                window.removeWindowFocusListener(focusListener)\n            }\n        }\n//        window.subtractInset()\n        UiScaledContent {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/SystemButtonPositionProvider.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar\n\ninterface SystemButtonPositionProvider {\n    fun getPositions(): SystemButtonsPosition?\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/TitleBar.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.linux.LinuxTitleBar\nimport com.abdownloadmanager.desktop.window.custom.titlebar.mac.MacTitleBar\nimport com.abdownloadmanager.desktop.window.custom.titlebar.windows.WindowsTitleBar\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.asDesktop\n\ninterface TitleBar {\n    val titleBarHeight: Dp\n    val systemButtonsPosition: SystemButtonsPosition\n    @Composable\n    fun RenderSystemButtons(\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onToggleMaximize: (() -> Unit)?,\n    )\n\n    @Composable\n    fun RenderTitleBarContent(\n        title: String,\n        titlePosition: TitlePosition,\n        modifier: Modifier,\n        windowIcon: Painter?,\n        start: (@Composable () -> Unit)?,\n        end: (@Composable () -> Unit)?,\n    )\n\n    @Composable\n    fun RenderTitleBar(\n        modifier: Modifier,\n        titleBar: TitleBar,\n        title: String,\n        windowIcon: Painter?,\n        titlePosition: TitlePosition,\n        start: (@Composable () -> Unit)?,\n        end: (@Composable () -> Unit)?,\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onRequestToggleMaximize: (() -> Unit)?,\n    )\n\n    companion object {\n        val DefaultTitleBarHeigh = 32.dp\n        fun getPlatformTitleBar(): TitleBar {\n            return when (Platform.asDesktop()) {\n                Platform.Desktop.Windows -> WindowsTitleBar\n                Platform.Desktop.MacOS -> MacTitleBar\n                Platform.Desktop.Linux -> LinuxTitleBar\n            }\n        }\n    }\n}\n\nenum class SystemButtonType {\n    Close,\n    Minimize,\n    Maximize,\n}\n\ndata class SystemButtonsPosition(\n    val buttons: List<SystemButtonType>,\n    val isLeft: Boolean,\n)\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/TitleBarShared.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\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.layout.wrapContentWidth\nimport androidx.compose.foundation.onClick\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.isWindowFocused\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\n\n@Composable\ninternal fun CommonTitleBarContent(\n    modifier: Modifier,\n    title: String,\n    windowIcon: Painter?,\n    titlePosition: TitlePosition,\n    start: @Composable (() -> Unit)?,\n    end: @Composable (() -> Unit)?\n) {\n    Row(\n        modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Row(\n            Modifier\n                .onClick {\n//                         capture pointer\n                }\n                .fillMaxHeight(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Spacer(Modifier.width(16.dp))\n            windowIcon?.let {\n                WithContentAlpha(1f) {\n                    Image(it, null, Modifier.size(16.dp))\n                }\n                Spacer(Modifier.width(8.dp))\n            }\n        }\n        if (!titlePosition.afterStart) {\n            Title(\n                modifier = Modifier\n                    .ifThen(titlePosition.centered) {\n                        weight(1f)\n                            .ifThen(start == null) {\n                                wrapContentWidth()\n                            }\n                    }\n                    .padding(titlePosition.padding),\n                title = title\n            )\n        }\n        start?.let {\n            Row(\n                Modifier\n            ) {\n                start()\n                Spacer(Modifier.width(8.dp))\n            }\n        }\n        if (titlePosition.afterStart) {\n            Title(\n                modifier = Modifier\n                    .weight(1f)\n                    .ifThen(titlePosition.centered) {\n                        wrapContentWidth()\n                    }\n                    .padding(titlePosition.padding),\n                title = title\n            )\n        }\n        if (!titlePosition.centered && !titlePosition.afterStart) {\n            Spacer(Modifier.weight(1f))\n        }\n        end?.let {\n            Row(\n                Modifier\n            ) {\n                end()\n                Spacer(Modifier.width(8.dp))\n            }\n        }\n    }\n}\n\n@Composable\nfun Title(\n    modifier: Modifier, title: String,\n) {\n    val isWindowFocused = isWindowFocused()\n    WithContentColor(myColors.onBackground) {\n        WithContentAlpha(\n            animateFloatAsState(\n                if (isWindowFocused) 1f else 0.5f\n            ).value\n        ) {\n            Text(\n                title,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                fontSize = myTextSizes.base,\n                modifier = Modifier\n                    .then(modifier)\n            )\n        }\n    }\n}\n\n@Composable\ninternal fun CommonRenderTitleBar(\n    modifier: Modifier,\n    titleBar: TitleBar,\n    title: String,\n    windowIcon: Painter? = null,\n    titlePosition: TitlePosition,\n    start: (@Composable () -> Unit)?,\n    end: (@Composable () -> Unit)?,\n    onRequestClose: () -> Unit,\n    onRequestMinimize: (() -> Unit)?,\n    onRequestToggleMaximize: (() -> Unit)?,\n) {\n    Row(\n        modifier.height(titleBar.titleBarHeight),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val systemButtonsAtFirst = titleBar.systemButtonsPosition.isLeft\n\n        if (systemButtonsAtFirst) {\n            titleBar.RenderSystemButtons(\n                onRequestClose = onRequestClose,\n                onRequestMinimize = onRequestMinimize,\n                onToggleMaximize = onRequestToggleMaximize,\n            )\n        }\n        titleBar.RenderTitleBarContent(\n            title = title,\n            titlePosition = titlePosition,\n            modifier = Modifier.weight(1f),\n            windowIcon = windowIcon,\n            start = start,\n            end = end\n        )\n        if (!systemButtonsAtFirst) {\n            titleBar.RenderSystemButtons(\n                onRequestClose = onRequestClose,\n                onRequestMinimize = onRequestMinimize,\n                onToggleMaximize = onRequestToggleMaximize,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/linux/LinuxSystemButtonsProvider.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.linux\n\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonPositionProvider\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonsPosition\nimport java.io.File\n\nobject LinuxSystemButtonsProvider : SystemButtonPositionProvider {\n    override fun getPositions(): SystemButtonsPosition? {\n        return runCatching {\n            getSystemButtonLayout()\n        }.getOrNull()\n    }\n}\n\n\nprivate fun getSystemButtonLayout(): SystemButtonsPosition? {\n    val desktop = System.getenv(\"XDG_CURRENT_DESKTOP\")?.lowercase()\n        ?: System.getenv(\"DESKTOP_SESSION\")?.lowercase()\n        ?: \"unknown\"\n\n    return when {\n        \"gnome\" in desktop -> parseColonLayout(\n            runCommand(\n                \"gsettings\",\n                \"get\",\n                \"org.gnome.desktop.wm.preferences\",\n                \"button-layout\"\n            )\n        )\n\n        \"mate\" in desktop -> parseColonLayout(\n            runCommand(\n                \"gsettings\",\n                \"get\",\n                \"org.mate.Marco.general\",\n                \"button-layout\"\n            )\n        )\n\n        \"xfce\" in desktop -> parsePipeLayout(\n            runCommand(\n                \"xfconf-query\",\n                \"-c\",\n                \"xfwm4\",\n                \"-p\",\n                \"/general/button_layout\"\n            )\n        )\n\n        \"kde\" in desktop -> parseKDELayout(File(System.getProperty(\"user.home\"), \".config/kwinrc\"))\n        else -> null\n    }?.takeIf { it.buttons.isNotEmpty() }\n}\n\n// For GNOME / MATE → colon-separated layout\nprivate fun parseColonLayout(layoutRaw: String?): SystemButtonsPosition? {\n    val layout = layoutRaw?.removeSurrounding(\"'\", \"'\")?.trim() ?: return null\n    val (left, right) = layout.split(\":\", limit = 2).map { it.trim() + \",\" }.let {\n        parseButtons(it.getOrNull(0).orEmpty()) to parseButtons(it.getOrNull(1).orEmpty())\n    }\n\n    return when {\n        left.isNotEmpty() -> SystemButtonsPosition(left, isLeft = true)\n        right.isNotEmpty() -> SystemButtonsPosition(right, isLeft = false)\n        else -> null\n    }\n}\n\n// For XFCE → pipe-separated layout\nprivate fun parsePipeLayout(layout: String?): SystemButtonsPosition? {\n    val parts = layout?.split(\"|\")?.map { it.trim() } ?: return null\n    return if (parts.isNotEmpty() && parts[0].isNotEmpty()) {\n        SystemButtonsPosition(parseButtons(parts[0]), isLeft = true)\n    } else if (parts.size > 1 && parts[1].isNotEmpty()) {\n        SystemButtonsPosition(parseButtons(parts[1]), isLeft = false)\n    } else {\n        null\n    }\n}\n\n// For KDE → parse kwinrc file\nprivate fun parseKDELayout(file: File): SystemButtonsPosition? {\n    if (!file.exists()) return null\n    val lines = file.readLines()\n    val left = lines.find { it.startsWith(\"ButtonsOnLeft=\") }?.substringAfter(\"=\")?.trim()\n    val right = lines.find { it.startsWith(\"ButtonsOnRight=\") }?.substringAfter(\"=\")?.trim()\n\n    return when {\n        !left.isNullOrEmpty() -> SystemButtonsPosition(parseButtons(left), isLeft = true)\n        !right.isNullOrEmpty() -> SystemButtonsPosition(parseButtons(right), isLeft = false)\n        else -> null\n    }\n}\n\nprivate fun parseButtons(raw: String): List<SystemButtonType> {\n    return raw.split(\",\", \"|\", \" \")\n        .mapNotNull {\n            when (it.lowercase()) {\n                \"close\" -> SystemButtonType.Close\n                \"maximize\" -> SystemButtonType.Maximize\n                \"minimize\" -> SystemButtonType.Minimize\n                else -> null\n            }\n        }\n}\n\nprivate fun runCommand(vararg args: String): String? {\n    return try {\n        val process = ProcessBuilder(*args).redirectErrorStream(true).start()\n        process.inputStream.reader().use { it.readText().trim() }\n    } catch (_: Exception) {\n        null\n    }\n}\n\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/linux/LinuxTitleBar.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.linux\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.Dp\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.isWindowFocused\nimport com.abdownloadmanager.desktop.window.custom.titlebar.CommonRenderTitleBar\nimport com.abdownloadmanager.desktop.window.custom.titlebar.CommonTitleBarContent\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonsPosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.TitleBar\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\n\nobject LinuxTitleBar : TitleBar {\n    override val titleBarHeight: Dp = TitleBar.Companion.DefaultTitleBarHeigh\n    override val systemButtonsPosition: SystemButtonsPosition by lazy {\n        LinuxSystemButtonsProvider.getPositions()\n            ?: SystemButtonsPosition(\n                buttons = listOf(\n                    SystemButtonType.Minimize,\n                    SystemButtonType.Maximize,\n                    SystemButtonType.Close,\n                ),\n                isLeft = false,\n            )\n    }\n\n    @Composable\n    override fun RenderSystemButtons(\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onToggleMaximize: (() -> Unit)?\n    ) {\n        LinuxSystemButtons(\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onToggleMaximize = onToggleMaximize,\n            buttons = systemButtonsPosition.buttons,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBarContent(\n        title: String,\n        titlePosition: TitlePosition,\n        modifier: Modifier,\n        windowIcon: Painter?,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?\n    ) {\n        CommonTitleBarContent(\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            start = start,\n            end = end,\n            modifier = modifier,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBar(\n        modifier: Modifier,\n        titleBar: TitleBar,\n        title: String,\n        windowIcon: Painter?,\n        titlePosition: TitlePosition,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?,\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onRequestToggleMaximize: (() -> Unit)?\n    ) {\n        val windowFocused = isWindowFocused()\n        CommonRenderTitleBar(\n            modifier = modifier\n                .background(\n                    animateColorAsState(\n                        if (windowFocused) Color.Companion.Transparent\n                        else myColors.onBackground / 0.05f\n                    ).value\n                ),\n            titleBar = titleBar,\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            start = start,\n            end = end,\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onRequestToggleMaximize = onRequestToggleMaximize,\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/linux/SystemButtons.Linux.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.linux\n\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.onClick\nimport androidx.compose.foundation.shape.CircleShape\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.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.WindowCloseButtonTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowMinimizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowToggleMaximizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.isWindowFocused\nimport com.abdownloadmanager.desktop.window.custom.isWindowMaximized\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType.*\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\n\n@Composable\nprivate fun SystemButton(\n    onClick: () -> Unit,\n    icon: IconSource,\n    modifier: Modifier = Modifier,\n) {\n    val onBackground = if (myColors.isLight) Color.Black else Color.White\n    val background = onBackground / 0.1f\n\n    val hoveredBackgroundColor: Color = onBackground / 0.2f\n    val onHoveredBackgroundColor: Color = onBackground\n\n    val isFocused = isWindowFocused()\n    val interactionSource = remember { MutableInteractionSource() }\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    MyIcon(\n        icon = icon,\n        contentDescription = null,\n        tint = animateColorAsState(\n            when {\n                isHovered -> onHoveredBackgroundColor\n                else -> onBackground\n            }.copy(\n                alpha = if (isFocused || isHovered) {\n                    1f\n                } else {\n                    0.5f\n                }\n            )\n        ).value,\n        modifier = modifier\n            .hoverable(interactionSource)\n            .onClick { onClick() }\n            .fillMaxHeight()\n            .wrapContentHeight()\n            .padding(horizontal = 4.dp)\n            .background(\n                animateColorAsState(\n                    when {\n                        isHovered -> hoveredBackgroundColor\n                        else -> background\n                    }\n                ).value,\n                CircleShape\n            )\n            .padding(6.dp)\n            .requiredSize(6.dp)\n    )\n}\n\n\n@Composable\ninternal fun LinuxSystemButtons(\n    onRequestClose: () -> Unit,\n    onRequestMinimize: (() -> Unit)?,\n    onToggleMaximize: (() -> Unit)?,\n    buttons: List<SystemButtonType>,\n) {\n    Row(\n        // Toolbar is aligned center vertically, so I fill that and place it on top\n        modifier = Modifier\n            .padding(horizontal = 6.dp)\n            .fillMaxHeight().wrapContentHeight(Alignment.Top),\n        verticalAlignment = Alignment.Top\n    ) {\n        buttons.forEach {\n            when (it) {\n                Close -> {\n                    WindowCloseButtonTooltip {\n                        SystemButton(\n                            onRequestClose,\n                            icon = MyIcons.windowClose,\n                            modifier = Modifier,\n                        )\n                    }\n                }\n\n                Minimize -> {\n                    onRequestMinimize?.let {\n                        WindowMinimizeTooltip {\n                            SystemButton(\n                                icon = MyIcons.windowMinimize,\n                                onClick = onRequestMinimize,\n                                modifier = Modifier\n                            )\n                        }\n                    }\n                }\n\n                Maximize -> {\n                    onToggleMaximize?.let {\n                        WindowToggleMaximizeTooltip {\n                            SystemButton(\n                                icon = if (isWindowMaximized()) {\n                                    MyIcons.windowFloating\n                                } else {\n                                    MyIcons.windowMaximize\n                                },\n                                onClick = onToggleMaximize,\n                                modifier = Modifier\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/mac/MacTitleBar.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.mac\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.CommonRenderTitleBar\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonsPosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.Title\nimport com.abdownloadmanager.desktop.window.custom.titlebar.TitleBar\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport ir.amirab.util.compose.layout.RelativeAlignment\nimport ir.amirab.util.ifThen\nimport kotlin.math.roundToInt\n\nobject MacTitleBar : TitleBar {\n    override val titleBarHeight: Dp = TitleBar.Companion.DefaultTitleBarHeigh\n    override val systemButtonsPosition: SystemButtonsPosition = SystemButtonsPosition(\n        buttons = listOf(\n            SystemButtonType.Close,\n            SystemButtonType.Minimize,\n            SystemButtonType.Maximize,\n        ),\n        isLeft = true,\n    )\n    @Composable\n    override fun RenderSystemButtons(\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onToggleMaximize: (() -> Unit)?\n    ) {\n        MacOSSystemButtons(\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onToggleMaximize = onToggleMaximize,\n            buttons = systemButtonsPosition.buttons,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBarContent(\n        title: String,\n        titlePosition: TitlePosition,\n        modifier: Modifier,\n        windowIcon: Painter?,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?\n    ) {\n        MacTitleBarContent(\n            modifier = modifier,\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            systemButtonsWidth = 60.dp,\n            start = start,\n            end = end,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBar(\n        modifier: Modifier,\n        titleBar: TitleBar,\n        title: String,\n        windowIcon: Painter?,\n        titlePosition: TitlePosition,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?,\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onRequestToggleMaximize: (() -> Unit)?\n    ) {\n        CommonRenderTitleBar(\n            modifier = modifier,\n            titleBar = titleBar,\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            start = start,\n            end = end,\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onRequestToggleMaximize = onRequestToggleMaximize,\n        )\n    }\n}\n\n@Composable\nprivate fun MacTitleBarContent(\n    modifier: Modifier,\n    title: String,\n    windowIcon: Painter?,\n    titlePosition: TitlePosition,\n    systemButtonsWidth: Dp,\n    start: @Composable (() -> Unit)?,\n    end: @Composable (() -> Unit)?\n) {\n    val density = LocalDensity.current\n    Row(\n        modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val titleShouldBeCentered = titlePosition.centered || start == null\n        val afterStart = titlePosition.afterStart\n        if (!afterStart) {\n            Row(\n                Modifier\n                    .ifThen(titleShouldBeCentered) {\n                        weight(1f)\n                            .wrapContentWidth(\n                                RelativeAlignment.Horizontal(\n                                    mainAlignment = Alignment.CenterHorizontally,\n                                    relative = -(density.run {\n                                        systemButtonsWidth.toPx()\n                                    }.roundToInt())\n                                )\n                            )\n                    }\n                    .padding(titlePosition.padding),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Spacer(Modifier.width(8.dp))\n                windowIcon?.let {\n                    WithContentAlpha(1f) {\n                        Image(it, null, Modifier.size(16.dp))\n                    }\n                    Spacer(Modifier.width(8.dp))\n                }\n                Title(\n                    modifier = Modifier,\n                    title = title\n                )\n            }\n        }\n        start?.let {\n            Row(\n                Modifier\n            ) {\n                start()\n                Spacer(Modifier.width(8.dp))\n            }\n        }\n        if (afterStart) {\n            Row(\n                Modifier\n                    .weight(1f)\n                    .ifThen(titleShouldBeCentered) {\n                        wrapContentWidth()\n                    }\n                    .padding(titlePosition.padding),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Spacer(Modifier.width(8.dp))\n                windowIcon?.let {\n                    WithContentAlpha(1f) {\n                        Image(it, null, Modifier.size(16.dp))\n                    }\n                    Spacer(Modifier.width(8.dp))\n                }\n                Title(\n                    modifier = Modifier,\n                    title = title\n                )\n            }\n        }\n        if (!titleShouldBeCentered && !titlePosition.afterStart) {\n            Spacer(Modifier.weight(1f))\n        }\n        end?.let {\n            Row(\n                Modifier\n            ) {\n                end()\n                Spacer(Modifier.width(4.dp))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/mac/SystemButtons.MacOS.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.mac\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.onClick\nimport androidx.compose.foundation.shape.CircleShape\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.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.WindowCloseButtonTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowMinimizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowToggleMaximizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.isWindowFocused\nimport com.abdownloadmanager.desktop.window.custom.isWindowMaximized\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.shared.util.darker\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\n\n@Composable\ninternal fun MacOSSystemButtons(\n    onRequestClose: () -> Unit,\n    onRequestMinimize: (() -> Unit)?,\n    onToggleMaximize: (() -> Unit)?,\n    buttons: List<SystemButtonType>,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n    val isUserInThisArea by interactionSource.collectIsHoveredAsState()\n    Row(\n        // Toolbar is aligned center vertically, so I fill that and place it on top\n        modifier = Modifier\n            .padding(horizontal = 6.dp)\n            .hoverable(interactionSource)\n            .fillMaxHeight().wrapContentHeight(Alignment.Top),\n        verticalAlignment = Alignment.Top\n    ) {\n        buttons.forEach {\n            when (it) {\n                SystemButtonType.Close -> {\n                    CloseButton(onRequestClose, isUserInThisArea)\n                }\n\n                SystemButtonType.Minimize -> {\n                    MinimizeButton(onRequestMinimize, isUserInThisArea)\n                }\n\n                SystemButtonType.Maximize -> {\n                    ToggleMaximizeButton(onToggleMaximize, isUserInThisArea)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MinimizeButton(onRequestMinimize: (() -> Unit)?, isUserInThisArea: Boolean) {\n    onRequestMinimize?.let {\n        WindowMinimizeTooltip {\n            SystemButton(\n                onClick = onRequestMinimize,\n                modifier = Modifier,\n                hoveredBackgroundColor = Color(0xFFFFBD2E),\n                icon = MyIcons.windowMinimize,\n                isUserInThisArea = isUserInThisArea,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ToggleMaximizeButton(onToggleMaximize: (() -> Unit)?, isUserInThisArea: Boolean) {\n    onToggleMaximize?.let {\n        WindowToggleMaximizeTooltip {\n            SystemButton(\n                onClick = onToggleMaximize,\n                modifier = Modifier,\n                hoveredBackgroundColor = Color(0xFF28C840),\n                icon = if (isWindowMaximized()) {\n                    MyIcons.windowFloating\n                } else {\n                    MyIcons.windowMaximize\n                },\n                isUserInThisArea = isUserInThisArea,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CloseButton(onRequestClose: () -> Unit, isUserInThisArea: Boolean) {\n    WindowCloseButtonTooltip {\n        SystemButton(\n            onRequestClose,\n            modifier = Modifier,\n            hoveredBackgroundColor = Color(0xFFFF5F57),\n            icon = MyIcons.windowClose,\n            isUserInThisArea = isUserInThisArea,\n        )\n    }\n}\n\n@Composable\nprivate fun SystemButton(\n    onClick: () -> Unit,\n    hoveredBackgroundColor: Color,\n    unfocusedBackgroundColor: Color = myColors.onBackground / 0.2f,\n    icon: IconSource,\n    isUserInThisArea: Boolean,\n    modifier: Modifier = Modifier,\n) {\n    val isWindowFocused = isWindowFocused()\n    val interactionSource = remember { MutableInteractionSource() }\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    Box(\n        modifier = modifier\n            .hoverable(interactionSource)\n            .onClick { onClick() }\n            .fillMaxHeight()\n            .wrapContentHeight()\n            .padding(horizontal = 6.dp)\n            .background(\n                animateColorAsState(\n                    when {\n                        !isWindowFocused -> unfocusedBackgroundColor\n                        isHovered -> hoveredBackgroundColor.darker()\n                        else -> hoveredBackgroundColor\n                    }\n                ).value,\n                CircleShape\n            )\n            .requiredSize(12.dp)\n    ) {\n        if (\n            isUserInThisArea && isWindowFocused\n        ) {\n            MyIcon(\n                icon = icon,\n                tint = Color.Black,\n                modifier = Modifier\n                    .align(Alignment.Center)\n                    .size(5.dp),\n                contentDescription = null,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/windows/SystemButtons.Windows.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.windows\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\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.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.desktop.window.custom.WindowCloseButtonTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowMinimizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.WindowToggleMaximizeTooltip\nimport com.abdownloadmanager.desktop.window.custom.isWindowFocused\nimport com.abdownloadmanager.desktop.window.custom.isWindowMaximized\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\n\n@Composable\nprivate fun SystemButton(\n    onClick: () -> Unit,\n    background: Color = Color.Transparent,\n    onBackground: Color = LocalContentColor.current,\n    hoveredBackgroundColor: Color = background,\n    onHoveredBackgroundColor: Color = LocalContentColor.current,\n    icon: IconSource,\n    modifier: Modifier = Modifier,\n) {\n    val isFocused = isWindowFocused()\n    val interactionSource = remember { MutableInteractionSource() }\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    MyIcon(\n        icon = icon,\n        contentDescription = null,\n        tint = animateColorAsState(\n            when {\n                isHovered -> onHoveredBackgroundColor\n                else -> onBackground\n            }.copy(\n                alpha = if (isFocused || isHovered) {\n                    1f\n                } else {\n                    0.5f\n                }\n            )\n        ).value,\n        modifier = modifier\n            .clickable { onClick() }\n            .background(\n                animateColorAsState(\n                    when {\n                        isHovered -> hoveredBackgroundColor\n                        else -> background\n                    }\n                ).value\n            )\n            .hoverable(interactionSource)\n            .windowButton()\n    )\n}\n\n\n@Composable\nprivate fun CloseButton(\n    onRequestClose: () -> Unit,\n    modifier: Modifier,\n) {\n    SystemButton(\n        onRequestClose,\n        background = Color.Transparent,\n        onBackground = myColors.onBackground,\n        hoveredBackgroundColor = Color(0xFFc42b1c),\n        onHoveredBackgroundColor = myColors.onError,\n        icon = MyIcons.windowClose,\n        modifier = modifier,\n    )\n}\n\nprivate fun Modifier.windowButton(): Modifier {\n    return fillMaxHeight()\n        .wrapContentHeight()\n        .padding(\n            horizontal = 20.dp,\n        )\n        .requiredSize(8.dp)\n}\n\n@Composable\ninternal fun WindowsSystemButtons(\n    onRequestClose: () -> Unit,\n    onRequestMinimize: (() -> Unit)?,\n    onToggleMaximize: (() -> Unit)?,\n    buttons: List<SystemButtonType>,\n) {\n    Row(\n        // Toolbar is aligned center vertically, so I fill that and place it on top\n        modifier = Modifier.fillMaxHeight().wrapContentHeight(Alignment.Top),\n        verticalAlignment = Alignment.Top\n    ) {\n        buttons.forEach {\n            when (it) {\n                SystemButtonType.Close -> {\n                    WindowCloseButtonTooltip {\n                        CloseButton(\n                            onRequestClose = onRequestClose,\n                            modifier = Modifier\n                        )\n                    }\n                }\n\n                SystemButtonType.Minimize -> {\n                    onRequestMinimize?.let {\n                        WindowMinimizeTooltip {\n                            SystemButton(\n                                icon = MyIcons.windowMinimize,\n                                onClick = onRequestMinimize,\n                                modifier = Modifier\n                            )\n                        }\n                    }\n                }\n\n                SystemButtonType.Maximize -> {\n                    onToggleMaximize?.let {\n                        WindowToggleMaximizeTooltip {\n                            SystemButton(\n                                icon = if (isWindowMaximized()) {\n                                    MyIcons.windowFloating\n                                } else {\n                                    MyIcons.windowMaximize\n                                },\n                                onClick = onToggleMaximize,\n                                modifier = Modifier\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/titlebar/windows/WindowsTitleBar.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom.titlebar.windows\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.Dp\nimport com.abdownloadmanager.desktop.window.custom.TitlePosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.CommonRenderTitleBar\nimport com.abdownloadmanager.desktop.window.custom.titlebar.CommonTitleBarContent\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonType\nimport com.abdownloadmanager.desktop.window.custom.titlebar.SystemButtonsPosition\nimport com.abdownloadmanager.desktop.window.custom.titlebar.TitleBar\n\nobject WindowsTitleBar : TitleBar {\n    override val titleBarHeight: Dp = TitleBar.Companion.DefaultTitleBarHeigh\n    override val systemButtonsPosition: SystemButtonsPosition = SystemButtonsPosition(\n        buttons = listOf(\n            SystemButtonType.Minimize,\n            SystemButtonType.Maximize,\n            SystemButtonType.Close,\n        ),\n        isLeft = false,\n    )\n\n    @Composable\n    override fun RenderSystemButtons(\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onToggleMaximize: (() -> Unit)?\n    ) {\n        WindowsSystemButtons(\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onToggleMaximize = onToggleMaximize,\n            buttons = systemButtonsPosition.buttons,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBarContent(\n        title: String,\n        titlePosition: TitlePosition,\n        modifier: Modifier,\n        windowIcon: Painter?,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?\n    ) {\n        CommonTitleBarContent(\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            start = start,\n            end = end,\n            modifier = modifier,\n        )\n    }\n\n    @Composable\n    override fun RenderTitleBar(\n        modifier: Modifier,\n        titleBar: TitleBar,\n        title: String,\n        windowIcon: Painter?,\n        titlePosition: TitlePosition,\n        start: @Composable (() -> Unit)?,\n        end: @Composable (() -> Unit)?,\n        onRequestClose: () -> Unit,\n        onRequestMinimize: (() -> Unit)?,\n        onRequestToggleMaximize: (() -> Unit)?\n    ) {\n        CommonRenderTitleBar(\n            modifier = modifier,\n            titleBar = titleBar,\n            title = title,\n            windowIcon = windowIcon,\n            titlePosition = titlePosition,\n            start = start,\n            end = end,\n            onRequestClose = onRequestClose,\n            onRequestMinimize = onRequestMinimize,\n            onRequestToggleMaximize = onRequestToggleMaximize,\n        )\n    }\n\n}\n"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/custom/utils.kt",
    "content": "package com.abdownloadmanager.desktop.window.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport java.awt.Toolkit\nimport java.awt.Window\nimport kotlin.math.max\n\n@Composable\nfun Window.subtractInset() {\n    LaunchedEffect(Unit) {\n        val inset = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration)\n        val size = Toolkit.getDefaultToolkit().screenSize.apply {\n            // Only one side will have an inset at any point in time, all others remain at 0\n            // We need to find the one side that has it and apply it to the appropriate dimension\n            width -= max(inset.left, inset.right)\n            height -= max(inset.top, inset.bottom)\n        }\n        val rangeX = 0..size.width\n        val rangeY = 0..size.height\n        // Works when taskbar is on top or left of screen\n        if (x !in rangeX || y !in rangeY) {\n            setLocation(\n                x.coerceIn(rangeX),\n                y.coerceIn(rangeY)\n            )\n        }\n\n        // Works for when taskbar is on right or bottom of screen\n        if (x + width !in rangeX || y + height !in rangeY) {\n            setLocation(\n                (x + width).coerceIn(rangeX) - width,\n                (y + height).coerceIn(rangeY) - height\n            )\n        }\n    }\n}"
  },
  {
    "path": "desktop/app-utils/src/main/kotlin/com/abdownloadmanager/desktop/window/moveSafe.kt",
    "content": "package com.abdownloadmanager.desktop.window\n\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntRect\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.window.PopupPositionProviderAtPosition\nimport ir.amirab.util.desktop.GlobalLayoutDirection\nimport java.awt.Component\nimport java.awt.Insets\nimport java.awt.Toolkit\nimport java.awt.Window\n\nfun Window.moveSafe(\n    position: DpOffset,\n    alignment: Alignment = Alignment.BottomEnd,\n) {\n    val window = this\n    val p = PopupPositionProviderAtPosition(\n        positionPx = Offset.Zero,\n        isRelativeToAnchor = true,\n        offsetPx = Offset.Zero,\n        alignment = alignment,\n        windowMarginPx = 0,\n    )\n\n    val screenSize = getScreenSize()\n    val insets = getScreenInsets(window)\n    val offset = p.calculatePosition(\n        popupContentSize = IntSize(\n            window.width, window.height\n        ),\n        layoutDirection = GlobalLayoutDirection,\n        windowSize = screenSize - insets,\n        anchorBounds = IntRect(\n            position.x.value.toInt(),\n            position.y.value.toInt(),\n            position.x.value.toInt(),\n            position.y.value.toInt(),\n        ),\n    ) + insets\n    window.setLocation(\n        offset.x,\n        offset.y,\n    )\n}\n\nfun getScreenInsets(component: Component): Insets {\n    return runCatching {\n        Toolkit.getDefaultToolkit().getScreenInsets(component.graphicsConfiguration)\n    }.getOrElse {\n        Insets(0, 0, 0, 0)\n    }\n}\n\nprivate operator fun IntSize.minus(insets: Insets): IntSize {\n    return IntSize(\n        width - (insets.left + insets.right),\n        height - (insets.top + insets.bottom)\n    )\n}\n\nprivate operator fun IntOffset.plus(insets: Insets): IntOffset {\n    return copy(\n        x = x + insets.left,\n        y = y + insets.top,\n    )\n}\n\n//it is dp size!\nprivate fun getScreenSize(): IntSize {\n    Toolkit.getDefaultToolkit().screenSize.run {\n        return IntSize(\n            width, height\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/mac_utils/build.gradle.kts",
    "content": "plugins{\n    id(MyPlugins.kotlin)\n}"
  },
  {
    "path": "desktop/mac_utils/src/main/kotlin/ir/amirab/util/desktop/mac/event/MacEventHandler.kt",
    "content": "package ir.amirab.util.desktop.mac.event\n\nimport java.awt.Desktop\nimport java.awt.desktop.AppReopenedEvent\nimport java.awt.desktop.AppReopenedListener\nimport java.awt.desktop.QuitEvent\nimport java.awt.desktop.QuitHandler\nimport java.awt.desktop.QuitResponse\n\nobject MacEventHandler {\n    fun configure(\n        onClickIcon: () -> Unit,\n        onAboutClick: () -> Unit,\n        onSettingsClick: () -> Unit,\n        onQuit: () -> Unit,\n    ) {\n        if (Desktop.isDesktopSupported() && Desktop.getDesktop()\n                .isSupported(Desktop.Action.APP_EVENT_REOPENED)\n        ) {\n            Desktop.getDesktop().apply {\n                addAppEventListener(object : AppReopenedListener {\n                    override fun appReopened(e: AppReopenedEvent?) {\n                        onClickIcon.invoke()\n                    }\n                })\n                if (isSupported(Desktop.Action.APP_ABOUT)) {\n                    setAboutHandler { onAboutClick.invoke() }\n                }\n                if (isSupported(Desktop.Action.APP_PREFERENCES)) {\n                    setPreferencesHandler { onSettingsClick.invoke() }\n                }\n                if (isSupported(Desktop.Action.APP_QUIT_HANDLER)) {\n                    setQuitHandler { _, response ->\n                        response.cancelQuit()\n                        onQuit.invoke()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "desktop/shared/build.gradle.kts",
    "content": "plugins {\n    id(MyPlugins.kotlin)\n    id(MyPlugins.composeDesktop)\n}\n\ndependencies {\n    // Jna\n    implementation(libs.jna.core)\n    implementation(libs.jna.platform)\n\n    implementation(project(\":shared:app\"))\n    implementation(project(\":shared:utils\"))\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/DesktopUtils.kt",
    "content": "package ir.amirab.util.desktop\n\nimport ir.amirab.util.desktop.keepawake.KeepAwake\nimport ir.amirab.util.desktop.poweraction.PowerAction\nimport ir.amirab.util.desktop.utils.linux.LinuxUtils\nimport ir.amirab.util.desktop.utils.mac.MacOSUtils\nimport ir.amirab.util.desktop.utils.windows.WindowsUtils\nimport ir.amirab.util.platform.Platform\n\n\ninterface DesktopUtils {\n    fun openSystemProxySettings()\n    fun powerAction(): PowerAction\n    fun keepAwakeService(): KeepAwake\n\n    companion object : DesktopUtils by getDesktopUtilOfCurrentOS()\n}\n\nprivate fun getDesktopUtilOfCurrentOS(): DesktopUtils {\n    val platform = Platform.getCurrentPlatform() as Platform.Desktop\n    return when (platform) {\n        Platform.Desktop.Windows -> WindowsUtils()\n        Platform.Desktop.MacOS -> MacOSUtils()\n        Platform.Desktop.Linux -> LinuxUtils()\n    }\n}\n\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/GlobalKeyboardModifiers.kt",
    "content": "package ir.amirab.util.desktop\n\nimport androidx.compose.ui.input.pointer.isCtrlPressed\nimport androidx.compose.ui.input.pointer.isMetaPressed\nimport androidx.compose.ui.input.pointer.isShiftPressed\nimport androidx.compose.ui.platform.WindowInfo\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isMac\n\nfun isCtrlPressed(windowInfo: WindowInfo): Boolean {\n    val keyboardModifiers = windowInfo.keyboardModifiers\n    return if (Platform.isMac()) {\n        keyboardModifiers.isMetaPressed\n    } else {\n        keyboardModifiers.isCtrlPressed\n    }\n}\n\nfun isShiftPressed(windowInfo: WindowInfo): Boolean {\n    return windowInfo.keyboardModifiers.isShiftPressed\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/OsUtils.kt",
    "content": "package ir.amirab.util.desktop\n\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.window.FrameWindowScope\nimport java.awt.ComponentOrientation\nimport java.awt.GraphicsConfiguration\nimport java.awt.GraphicsEnvironment\nimport java.util.*\n\nval LocalFrameWindowScope = compositionLocalOf<FrameWindowScope> {\n    error(\"LocalFrameWindowScope not provided yet\")\n}\n\nval GlobalDensity\n    get() = GraphicsEnvironment.getLocalGraphicsEnvironment()\n        .defaultScreenDevice\n        .defaultConfiguration\n        .density\nval GraphicsConfiguration.density: Density\n    get() = Density(\n        defaultTransform.scaleX.toFloat(),\n        fontScale = 1f\n    )\n\nval GlobalLayoutDirection get() = Locale.getDefault().layoutDirection\nval Locale.layoutDirection: LayoutDirection\n    get() = ComponentOrientation.getOrientation(this).layoutDirection\n\nval ComponentOrientation.layoutDirection: LayoutDirection\n    get() = when {\n        isLeftToRight -> LayoutDirection.Ltr\n        isHorizontal -> LayoutDirection.Rtl\n        else -> LayoutDirection.Ltr\n    }\n\n\nval trayIconSize = when (DesktopPlatform.Current) {\n    // https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 22x22)\n    DesktopPlatform.Linux -> Size(22f, 22f)\n    // https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 16x16)\n    DesktopPlatform.Windows -> Size(16f, 16f)\n    // https://medium.com/@acwrightdesign/creating-a-macos-menu-bar-application-using-swiftui-54572a5d5f87\n    DesktopPlatform.MacOS -> Size(22f, 22f)\n    DesktopPlatform.Unknown -> Size(32f, 32f)\n}\n\nenum class DesktopPlatform {\n    Linux,\n    Windows,\n    MacOS,\n    Unknown;\n\n    companion object {\n        /**\n         * Identify OS on which the application is currently running.\n         */\n        val Current: DesktopPlatform by lazy {\n            val name = System.getProperty(\"os.name\")\n            when {\n                name?.startsWith(\"Linux\") == true -> Linux\n                name?.startsWith(\"Win\") == true -> Windows\n                name == \"Mac OS X\" -> MacOS\n                else -> Unknown\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/PlatformAppActivator.kt",
    "content": "package ir.amirab.util.desktop\n\nimport ir.amirab.util.desktop.activator.mac.MacAppActivator\nimport ir.amirab.util.platform.Platform\n\ninterface PlatformAppActivator {\n    fun active()\n\n    companion object : PlatformAppActivator by getPlatformAppActivatorForCurrentOs()\n}\n\nclass EmptyAppActivator : PlatformAppActivator {\n    override fun active() {\n        // no-op\n    }\n}\n\nprivate fun getPlatformAppActivatorForCurrentOs() = when (Platform.getCurrentPlatform()) {\n    Platform.Desktop.MacOS -> MacAppActivator()\n    else -> EmptyAppActivator()\n}"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/PlatformDockToggler.kt",
    "content": "package ir.amirab.util.desktop\n\nimport ir.amirab.util.desktop.dock.mac.MacDockToggler\nimport ir.amirab.util.platform.Platform\n\ninterface PlatformDockToggler {\n    fun show()\n    fun hide()\n\n    companion object : PlatformDockToggler by getForCurrentOs()\n}\n\n\nclass EmptyDockDockToggler : PlatformDockToggler {\n    override fun show() {\n        // no-op\n    }\n\n    override fun hide() {\n        // no-op\n    }\n}\n\nprivate fun getForCurrentOs() = when (Platform.getCurrentPlatform()) {\n    Platform.Desktop.MacOS -> MacDockToggler()\n    else -> EmptyDockDockToggler()\n}"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/WindowsRegistry.kt",
    "content": "package ir.amirab.util.desktop\n\nobject WindowsRegistry {\n\n    /**\n     * Set value in registry\n     *\n     * @param path full path including HKey and path\n     * @param key key name or `null` for default\n     */\n    fun setValueInRegistry(\n        path: String,\n        key: String?,\n        value: String,\n    ) {\n        val keySection = if (key == null) {\n            arrayOf(\"/ve\")\n        } else {\n            arrayOf(\"/v\", quoted(key))\n        }\n        Runtime.getRuntime().exec(\n            arrayOf(\n                \"reg\", \"add\", quoted(path), *keySection,\n                \"/t\", \"REG_SZ\",\n                \"/d\", value,\n                \"/f\",\n            )\n        )\n    }\n\n\n    /**\n     * gets a value in Registry or null\n     *\n     * @param path full path including HKey and path\n     * @param key key name or `null` for default\n     *\n     * @return the value or `null` on fail or not found\n     */\n    fun getValueInRegistry(\n        path: String,\n        key: String?,//null for default\n    ): String? {\n        val keySection = if (key == null) {\n            arrayOf(\"/ve\")\n        } else {\n            arrayOf(\"/v\", quoted(key))\n        }\n        return try {\n            val p = Runtime.getRuntime().exec(\n                arrayOf(\n                    \"reg\", \"query\", quoted(path), *keySection,\n                )\n            )\n            p.inputStream.reader().use {\n                val text = it.readText()\n                val result = queryResultPattern.find(text)\n                result?.groupValues?.getOrNull(1)\n            }\n        } catch (e: Throwable) {\n            return null\n        }\n    }\n\n    /**\n     * remove entire path in registry.\n     *\n     * **BE CAREFUL** about this\n     *\n     * @param path full path including HKey and path\n     */\n    fun removePathInRegistry(path: String) {\n        Runtime.getRuntime().exec(\n            arrayOf(\n                \"reg\", \"delete\", quoted(path),\n                \"/f\",\n            )\n        )\n    }\n\n    /**\n     * remove value in registry\n     *\n     * @param path full path including HKey and path\n     * @param key key name or `null` for default\n     */\n    fun removeValueInRegistry(path: String, key: String?) {\n        val keySection = if (key == null) {\n            arrayOf(\"/ve\")\n        } else {\n            arrayOf(\"/v\", quoted(key))\n        }\n        Runtime.getRuntime().exec(\n            arrayOf(\n                \"reg\", \"delete\", quoted(path),\n                *keySection, \"/f\",\n            )\n        )\n    }\n\n    // utils\n\n    /**\n     * wrap the [value] with quote name -> \"name\"\n     */\n    private fun quoted(value: String) = \"\\\"$value\\\"\"\n\n\n    /**\n     * the correct result for\n     * ```\n     * reg query path [...params]\n     * ```\n     * @see [getValueInRegistry]\n     *\n     * would be\n     *```\n     * HKCU\\path\\to\\destination\n     *     type    name    value\n     *```\n     * if you use this so many times you may change it to `lazy` instead of `get`\n     */\n    private val queryResultPattern get() = \"\"\"\\n(?:\\s+)\\w+(?:\\s)+\\w+(?:\\s)+(.+)\"\"\".toRegex()\n\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/activator/mac/MacAppActivator.kt",
    "content": "package ir.amirab.util.desktop.activator.mac\n\nimport ir.amirab.util.desktop.PlatformAppActivator\nimport ir.amirab.util.desktop.utils.mac.FoundationLibrary\n\nclass MacAppActivator : PlatformAppActivator {\n    override fun active() {\n        val requiredFoundation = FoundationLibrary.INSTANCE ?: return\n        runCatching {\n            val nsAppClass = requiredFoundation.objc_getClass(\"NSApplication\")\n            val sharedAppSel = requiredFoundation.sel_registerName(\"sharedApplication\")\n            val activateSel = requiredFoundation.sel_registerName(\"activateIgnoringOtherApps:\")\n\n            val nsApp = requiredFoundation.objc_msgSend(nsAppClass, sharedAppSel)\n            requiredFoundation.objc_msgSend(nsApp, activateSel, true)\n        }\n    }\n}"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/dock/mac/MacDockToggler.kt",
    "content": "package ir.amirab.util.desktop.dock.mac\n\nimport ir.amirab.util.desktop.PlatformDockToggler\nimport ir.amirab.util.desktop.utils.mac.FoundationLibrary\n\nclass MacDockToggler : PlatformDockToggler {\n\n    private val foundation = FoundationLibrary.INSTANCE\n\n    private val isAvailable = foundation != null\n\n    private val nsAppClass by lazy { foundation!!.objc_getClass(\"NSApplication\") }\n    private val sharedAppSel by lazy { foundation!!.sel_registerName(\"sharedApplication\") }\n    private val setPolicySel by lazy { foundation!!.sel_registerName(\"setActivationPolicy:\") }\n\n    private val nsRunningAppClass by lazy { foundation!!.objc_getClass(\"NSRunningApplication\") }\n    private val currentAppSel by lazy { foundation!!.sel_registerName(\"currentApplication\") }\n    private val activateSel by lazy { foundation!!.sel_registerName(\"activateWithOptions:\") }\n\n    private val NSApplicationActivationPolicyRegular = 0\n    private val NSApplicationActivationPolicyAccessory = 1\n    private val NSApplicationActivateIgnoringOtherApps = 1\n\n    override fun show() {\n        if (isAvailable) {\n            setPolicy(NSApplicationActivationPolicyRegular)\n        }\n    }\n\n    override fun hide() {\n        if (isAvailable) {\n            hideAndKeepFocus()\n        }\n    }\n\n    private fun hideAndKeepFocus() {\n        val nsApp = foundation!!.objc_msgSend(nsAppClass, sharedAppSel)\n        val nsRunningApp = foundation.objc_msgSend(nsRunningAppClass, currentAppSel)\n\n        foundation.objc_msgSend(nsApp, setPolicySel, NSApplicationActivationPolicyAccessory)\n        foundation.objc_msgSend(nsRunningApp, activateSel, NSApplicationActivateIgnoringOtherApps)\n    }\n\n    private fun setPolicy(policy: Int) {\n        val nsApp = foundation!!.objc_msgSend(nsAppClass, sharedAppSel)\n        foundation.objc_msgSend(nsApp, setPolicySel, policy)\n    }\n}"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/keepawake/KeepAwake.kt",
    "content": "package ir.amirab.util.desktop.keepawake\n\ninterface KeepAwake {\n    /**\n     * Prevents the system from going to sleep.\n     */\n    fun keepAwake()\n\n    /**\n     * Allows the system to go to sleep again.\n     */\n    fun allowSleep()\n\n    class NoOpKeepAwake : KeepAwake {\n        override fun keepAwake() {\n            // No operation, does nothing\n        }\n\n        override fun allowSleep() {\n            // No operation, does nothing\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/keepawake/MacKeepAwake.kt",
    "content": "package ir.amirab.util.desktop.keepawake\n\nclass MacKeepAwake : KeepAwake {\n    var process: Process? = null\n\n    @Synchronized\n    override fun keepAwake() {\n        process?.destroy()\n        process = runCatching {\n            ProcessBuilder(\"caffeinate\", \"-s\")\n                .redirectErrorStream(true)\n                .start()\n        }.getOrElse { null }\n    }\n\n    override fun allowSleep() {\n        runCatching {\n            process?.destroy()\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/keepawake/WindowsKeepAwake.kt",
    "content": "package ir.amirab.util.desktop.keepawake\n\nimport com.sun.jna.platform.win32.Kernel32\nimport kotlin.concurrent.thread\n\nclass WindowsKeepAwake : KeepAwake {\n    /**\n     * 1.keepAwake -> 2.cancellation require to be called in single thread\n     */\n    @Volatile\n    private var thread: Thread? = null\n\n    @Synchronized\n    override fun keepAwake() {\n        if (thread != null) {\n            // already active\n            return\n        }\n        thread = thread(\n            name = \"WindowsKeepAwake\",\n            isDaemon = true,\n        ) {\n            try {\n                // keep the system awake!\n                Kernel32.INSTANCE.SetThreadExecutionState(\n                    Kernel32.ES_CONTINUOUS or Kernel32.ES_SYSTEM_REQUIRED\n                )\n                Thread.sleep(Long.MAX_VALUE)\n            } catch (_: InterruptedException) {\n                // we expect this!\n            } catch (e: Exception) {\n                // it shouldn't happen, but we don't throw any exception here!\n                e.printStackTrace()\n            } finally {\n                // thread interrupted! now we can allow system to go sleep!\n                runCatching {\n                    Kernel32.INSTANCE.SetThreadExecutionState(\n                        Kernel32.ES_CONTINUOUS\n                    )\n                }\n                thread = null\n            }\n        }\n    }\n\n    @Synchronized\n    override fun allowSleep() {\n        thread?.let {\n            it.interrupt()\n            it.join()\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/poweraction/PowerAction.kt",
    "content": "package ir.amirab.util.desktop.poweraction\n\ninterface PowerAction {\n    fun initiate(config: PowerActionConfig): Boolean\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/poweraction/PowerActionConfig.kt",
    "content": "package ir.amirab.util.desktop.poweraction\n\ndata class PowerActionConfig(\n    val type: Type,\n    val force: Boolean,\n) {\n    enum class Type {\n        Shutdown,\n        Hibernate,\n        Sleep,\n    }\n}\n\ninterface ContainsPowerActionConfigOnFinish {\n    fun getPowerActionConfigOnFinish(): PowerActionConfig?\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/poweraction/PowerActionLinux.kt",
    "content": "package ir.amirab.util.desktop.poweraction\n\nimport ir.amirab.util.execAndWait\n\nclass PowerActionLinux : PowerAction {\n    override fun initiate(config: PowerActionConfig): Boolean {\n        return when (config.type) {\n            PowerActionConfig.Type.Shutdown -> shutdown(config.force)\n            PowerActionConfig.Type.Hibernate -> TODO()\n            PowerActionConfig.Type.Sleep -> TODO()\n        }\n    }\n\n    private fun shutdown(force: Boolean): Boolean {\n        val commands = listOf(\n            arrayOf(\n                \"dbus-send\", \"--system\", \"--print-reply\",\n                \"--dest=org.freedesktop.login1\",\n                \"/org/freedesktop/login1\",\n                \"org.freedesktop.login1.Manager.PowerOff\",\n                \"boolean:true\",\n            ),\n            arrayOf(\n                \"systemctl\", \"poweroff\"\n            ),\n        )\n        return commands.any { command ->\n            runCatching {\n                execAndWait(command)\n            }.getOrElse { false }\n        }\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/poweraction/PowerActionMac.kt",
    "content": "package ir.amirab.util.desktop.poweraction\n\nimport ir.amirab.util.execAndWait\n\nclass PowerActionMac : PowerAction {\n    override fun initiate(config: PowerActionConfig): Boolean {\n        return when (config.type) {\n            PowerActionConfig.Type.Shutdown -> shutdown(config.force)\n            PowerActionConfig.Type.Hibernate -> TODO()\n            PowerActionConfig.Type.Sleep -> TODO()\n        }\n    }\n\n    private fun shutdown(force: Boolean): Boolean {\n        return execAndWait(\n            arrayOf(\n                \"osascript\", \"-e\", \"tell application \\\"System Events\\\" to shut down\"\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/poweraction/PowerActionWindows.kt",
    "content": "package ir.amirab.util.desktop.poweraction\n\nimport ir.amirab.util.execAndWait\n\nclass PowerActionWindows : PowerAction {\n    override fun initiate(config: PowerActionConfig): Boolean {\n        return when (config.type) {\n            PowerActionConfig.Type.Shutdown -> shutdown(config.force)\n            PowerActionConfig.Type.Hibernate -> TODO()\n            PowerActionConfig.Type.Sleep -> TODO()\n        }\n    }\n\n    private fun shutdown(force: Boolean): Boolean {\n        val command = arrayOf(\"shutdown\", \"/s\", \"/t\", \"0\")\n        return execAndWait(command)\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/screen/DesktopScreen.kt",
    "content": "package ir.amirab.util.desktop.screen\n\nimport androidx.compose.ui.unit.*\nimport com.abdownloadmanager.shared.util.ui.theme.DEFAULT_UI_SCALE\nimport java.awt.GraphicsEnvironment\n\nfun getGlobalScale(): Float {\n    val graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment()\n    val defaultScreenDevice = graphicsEnvironment.defaultScreenDevice\n    val defaultTransform = defaultScreenDevice.defaultConfiguration.defaultTransform\n    return defaultTransform.scaleX.toFloat() // Assuming uniform scaling\n}\n\nfun Int.applyUiScale(\n    userUiScale: Float,\n): Int {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    return (this * userUiScale).toInt()\n}\n\nfun Float.applyUiScale(\n    userUiScale: Float,\n): Float {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    return (this * userUiScale)\n}\n\nfun Int.unApplyUiScale(\n    userUiScale: Float,\n): Int {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    return (this / userUiScale).toInt()\n}\n\nfun Float.unApplyUiScale(\n    userUiScale: Float,\n): Float {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    return (this / userUiScale)\n}\n\nfun DpSize.applyUiScale(\n    userUiScale: Float,\n): DpSize {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    if (this == DpSize.Unspecified) return this\n    return DpSize(\n        width = width.let {\n            if (isSpecified) it.value.toInt().applyUiScale(userUiScale).dp\n            else it\n        },\n        height = height.let {\n            if (isSpecified) it.value.toInt().applyUiScale(userUiScale).dp\n            else it\n        },\n    )\n}\n\nfun DpSize.unApplyUiScale(\n    userUiScale: Float,\n): DpSize {\n    if (userUiScale == DEFAULT_UI_SCALE) return this\n    if (this == DpSize.Unspecified) return this\n    return DpSize(\n        width = width.let {\n            if (isSpecified) it.value.toInt().unApplyUiScale(userUiScale).dp\n            else it\n        },\n        height = height.let {\n            if (isSpecified) it.value.toInt().applyUiScale(userUiScale).dp\n            else it\n        },\n    )\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/utils/linux/LinuxUtils.kt",
    "content": "package ir.amirab.util.desktop.utils.linux\n\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.desktop.keepawake.KeepAwake\nimport ir.amirab.util.desktop.poweraction.PowerAction\nimport ir.amirab.util.desktop.poweraction.PowerActionLinux\nimport ir.amirab.util.execAndWait\n\nclass LinuxUtils : DesktopUtils {\n    private val keepAwake = KeepAwake.NoOpKeepAwake()\n    private val powerActionForLinux = PowerActionLinux()\n    override fun openSystemProxySettings() {\n        val desktopEnv = System.getenv(\"XDG_CURRENT_DESKTOP\")\n        when {\n            desktopEnv?.contains(\"GNOME\") ?: false -> {\n                execAndWait(\n                    arrayOf(\n                        \"gnome-control-center network\"\n                    )\n                )\n            }\n\n            desktopEnv?.contains(\"KDE\") ?: false -> {\n                execAndWait(\n                    arrayOf(\n                        \"systemsettings5 proxy\"\n                    )\n                )\n            }\n\n            else -> {\n                println(\"Can't open System Proxy Settings: Unsupported desktop environment: $desktopEnv\")\n            }\n        }\n    }\n\n    override fun powerAction(): PowerAction {\n        return powerActionForLinux\n    }\n\n    override fun keepAwakeService(): KeepAwake {\n        return keepAwake\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/utils/mac/FoundationLibrary.kt",
    "content": "package ir.amirab.util.desktop.utils.mac\n\nimport com.sun.jna.Library\nimport com.sun.jna.Native\nimport com.sun.jna.Pointer\n\ninternal interface FoundationLibrary : Library {\n    fun objc_getClass(name: String): Pointer\n    fun sel_registerName(name: String): Pointer\n\n    fun objc_msgSend(receiver: Pointer, selector: Pointer): Pointer\n    fun objc_msgSend(receiver: Pointer, selector: Pointer, b: Boolean): Pointer\n    fun objc_msgSend(receiver: Pointer, selector: Pointer, i: Int): Pointer\n    fun objc_msgSend(receiver: Pointer, selector: Pointer, l: Long): Pointer\n    fun objc_msgSend(receiver: Pointer, selector: Pointer, p: Pointer): Pointer\n    fun objc_msgSend(receiver: Pointer, selector: Pointer, o: Any): Pointer\n\n\n    companion object {\n        val INSTANCE by lazy {\n            runCatching { Native.load(\"objc\", FoundationLibrary::class.java) }.getOrNull()\n        }\n    }\n}"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/utils/mac/MacOSUtils.kt",
    "content": "package ir.amirab.util.desktop.utils.mac\n\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.desktop.keepawake.KeepAwake\nimport ir.amirab.util.desktop.keepawake.MacKeepAwake\nimport ir.amirab.util.desktop.poweraction.PowerAction\nimport ir.amirab.util.desktop.poweraction.PowerActionMac\nimport ir.amirab.util.desktop.poweraction.PowerActionWindows\nimport ir.amirab.util.execAndWait\n\nclass MacOSUtils : DesktopUtils {\n    private val keepAwakeService = MacKeepAwake()\n    private val powerActionForMac = PowerActionMac()\n    override fun openSystemProxySettings() {\n        val commands = listOf(\n            arrayOf(\"open\", \"x-apple.systempreferences:com.apple.Network-Settings.extension\"),\n            arrayOf(\"open\", \"/System/Library/PreferencePanes/Network.prefPane\"),\n            arrayOf(\"open\", \"/System/Preferences/Network\")\n        )\n\n        for (command in commands) {\n            if (execAndWait(command)) return\n        }\n    }\n\n    override fun powerAction(): PowerAction {\n        return powerActionForMac\n    }\n\n    override fun keepAwakeService(): KeepAwake {\n        return keepAwakeService\n    }\n}\n"
  },
  {
    "path": "desktop/shared/src/main/kotlin/ir/amirab/util/desktop/utils/windows/WindowsUtils.kt",
    "content": "package ir.amirab.util.desktop.utils.windows\n\nimport ir.amirab.util.desktop.DesktopUtils\nimport ir.amirab.util.desktop.keepawake.KeepAwake\nimport ir.amirab.util.desktop.keepawake.WindowsKeepAwake\nimport ir.amirab.util.desktop.poweraction.PowerAction\nimport ir.amirab.util.desktop.poweraction.PowerActionWindows\nimport ir.amirab.util.execAndWait\n\nclass WindowsUtils : DesktopUtils {\n    private val keepAwakeService = WindowsKeepAwake()\n    private val powerActionWindows = PowerActionWindows()\n    override fun openSystemProxySettings() {\n        val result = execAndWait(\n            arrayOf(\n                \"cmd\", \"/c\", \"start\",\n                \"ms-settings:network-proxy\",\n            )\n        )\n        if (!result) {\n            execAndWait(\n                arrayOf(\n                    \"rundll32.exe shell32.dll,Control_RunDLL inetcpl.cpl,,4\"\n                )\n            )\n        }\n    }\n\n    override fun powerAction(): PowerAction {\n        return powerActionWindows\n    }\n\n    override fun keepAwakeService(): KeepAwake {\n        return keepAwakeService\n    }\n}\n"
  },
  {
    "path": "downloader/core/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Kotlin.serialization)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n                implementation(libs.kotlin.serialization.json)\n                implementation(libs.kotlin.datetime)\n                implementation(libs.kotlin.coroutines.core)\n                api(libs.okio.okio)\n                api(libs.okhttp.okhttp)\n                api(libs.okhttp.coroutines)\n                implementation(project(\":shared:utils\"))\n                api(\"io.lindstrom:m3u8-parser:0.29\")\n            }\n        }\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"ir.amirab.downloader.core\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/androidMain/kotlin/ir/amirab/downloader/utils/SparseFile.android.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.nio.file.OpenOption\nimport java.nio.file.StandardOpenOption\n\nactual object SparseFile : ISparseFile {\n    override fun createSparseFile(file: File): Boolean {\n        if (!file.exists()) {\n            val options = arrayOf<OpenOption>(\n                StandardOpenOption.WRITE,\n                StandardOpenOption.CREATE_NEW,\n                StandardOpenOption.SPARSE\n            )\n            return runCatching {\n                Files.newByteChannel(\n                    file.toPath(),\n                    *options,\n                ).use {}\n                true\n            }.getOrElse { false }\n        }\n        return false\n    }\n\n    override fun canWeCreateSparseFile(file: File): Boolean {\n        // android doesn't tell us!\n        return true\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/DownloadManager.kt",
    "content": "package ir.amirab.downloader\n\nimport arrow.core.Some\nimport ir.amirab.downloader.db.IDownloadListDb\nimport ir.amirab.downloader.db.IDownloadPartListDb\nimport ir.amirab.downloader.downloaditem.*\nimport ir.amirab.downloader.downloaditem.contexts.DuplicateRemoval\nimport ir.amirab.downloader.downloaditem.contexts.RemovedBy\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.utils.DuplicateFilterByPath\nimport ir.amirab.downloader.utils.EmptyFileCreator\nimport ir.amirab.downloader.utils.FileNameUtil\nimport ir.amirab.downloader.utils.OnDuplicateStrategy.*\nimport ir.amirab.util.FileNameValidator\nimport ir.amirab.util.PathValidator\nimport ir.amirab.util.suspendGuardedEntry\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport okio.Throttler\nimport java.io.File\n\nclass DownloadManager(\n    val dlListDb: IDownloadListDb,\n    val partListDb: IDownloadPartListDb,\n    val settings: DownloadSettings,\n    val emptyFileCreator: EmptyFileCreator,\n    private val downloaderRegistry: DownloaderRegistry,\n    val downloadDataFolder: File\n) : DownloadManagerMinimalControl {\n\n    val scope = CoroutineScope(SupervisorJob())\n\n    private val bootGuard = suspendGuardedEntry()\n    suspend fun awaitBoot() {\n        bootGuard.awaitDone()\n    }\n\n    //make ready to resume download\n    suspend fun boot() {\n        bootGuard.action {\n            createJobForPendingDownloads()\n        }\n    }\n\n    private val contextContainer = ContextProvider()\n\n    private suspend fun createJobForPendingDownloads() {\n        dlListDb.getAll().filter {\n            it.status != DownloadStatus.Completed\n        }.forEach {\n            createJob(it).boot()\n        }\n    }\n\n    var downloadJobs = listOf<DownloadJob>()\n        private set\n\n    private val dbAddSync = Mutex()\n\n    suspend fun addDownload(\n        props: NewDownloadItemProps\n    ): Long {\n        val newItem = props.downloadItem\n        val onDuplicateStrategy = props.onDuplicateStrategy\n        val context = props.context\n        val extraConfig = props.extraConfig\n        newItem.validateItem()\n        require(PathValidator.isValidPath(newItem.folder)) { \"folder of new download is not valid: ${newItem.folder}\" }\n        require(PathValidator.canWriteToThisPath(newItem.folder)) { \"can't write to this new download's folder: ${newItem.folder}\" }\n        require(FileNameValidator.isValidFileName(newItem.name)) { \"name of new download is not valid: ${newItem.name}\" }\n//        thisLogger().info(\"adding download\")\n        val job = dbAddSync.withLock {\n            val allDownloads = dlListDb.getAll()\n            val duplicateFinder = DuplicateFilterByPath(File(newItem.folder, newItem.name))\n            val foundItems = allDownloads.filter(duplicateFinder::isDuplicate)\n            var removedItems = emptyList<IDownloadItem>()\n            if (foundItems.isNotEmpty()) {\n                when (onDuplicateStrategy) {\n                    AddNumbered -> {\n                        //we do nothing here instead we increment file name after all if necessary\n                    }\n\n                    OverrideDownload -> {\n                        foundItems.forEach {\n                            deleteDownload(it.id, { true }, RemovedBy(DuplicateRemoval))\n                        }\n                        removedItems = foundItems\n                    }\n\n                    Abort -> {\n                        error(\"Aborting add download that already exists\")\n                    }\n                }\n            }\n\n            val name = FileNameUtil.numberedIfExists(\n                File(newItem.folder, newItem.name)\n            ).first { candidateNewFile ->\n                val withSameDestination = allDownloads\n                    .filter { it !in removedItems }\n                    .find {\n                        it.name == candidateNewFile.name && it.folder == candidateNewFile.parent\n                    }\n                withSameDestination == null\n            }.name\n\n            val id = dlListDb.getLastId() + 1\n            val dateAdded = newItem.dateAdded\n                .takeIf { it != 0L }\n                ?: System.currentTimeMillis()\n\n            val downloadItem = newItem.copy(\n                id = Some(id),\n                name = Some(name),\n                dateAdded = Some(dateAdded),\n                startTime = Some(null),\n                completeTime = Some(null),\n                status = Some(DownloadStatus.Added)\n            )\n            dlListDb.add(downloadItem)\n            createJob(downloadItem)\n                .apply { boot() }\n                .apply {\n                    extraConfig?.let {\n                        extraConfigsReceived(it)\n                    }\n                }\n        }\n        contextContainer.setContext(job.id, context)\n        onDownloadAdded(job.downloadItem)\n//        thisLogger().info(\"this download added $downloadItem\")\n//        println(\"download created ${job.id}\")\n        return job.id\n    }\n\n    private val jobModificationLock = Any()\n    private fun createJob(downloadItem: IDownloadItem): DownloadJob {\n        val job = downloaderRegistry.createJob(\n            downloadItem,\n            this,\n        )\n//        thisLogger().info(\"download job for $id created\")\n        downloadJobs = downloadJobs + job\n        return job\n    }\n\n    suspend fun deleteDownload(\n        id: Long,\n        alsoRemoveFile: (IDownloadItem) -> Boolean,\n        context: DownloadItemContext = EmptyContext,\n    ) {\n        kotlin.runCatching { pause(id) }\n        val itemToDelete = dlListDb.getById(id) ?: return\n        val job = getDownloadJob(id) ?: run {\n            createJob(itemToDelete).apply { boot() }\n        }\n        // at this point: job will be created (and booted) if it was not created before\n        contextContainer.updateContext(id) { it + context }\n        job.downloadRemoved(\n            removeOutputFile = if (itemToDelete.status == DownloadStatus.Completed) {\n                alsoRemoveFile(itemToDelete)\n            } else {\n                // always remove file if download is not finished!\n                true\n            },\n        )\n        deleteJob(job.id)\n        dlListDb.remove(itemToDelete)\n        partListDb.removeParts(id)\n        listOfJobsEvents.tryEmit(\n            DownloadManagerEvents.OnJobRemoved(itemToDelete, contextContainer.getContext(id))\n        )\n        contextContainer.removeContext(id)\n    }\n\n    private fun deleteJob(\n        id: Long,\n    ) {\n        synchronized(jobModificationLock) {\n            val jobToDelete = downloadJobs.find {\n                it.id == id\n            }\n            jobToDelete?.let {\n                it.close()\n                downloadJobs = downloadJobs.minusElement(it)\n            }\n        }\n    }\n\n    suspend fun pause(id: Long, context: DownloadItemContext = EmptyContext) {\n        val job = getDownloadJob(id) ?: return\n        contextContainer.updateContext(id) { it + context }\n        job.pause()\n    }\n\n    suspend fun resume(id: Long, context: DownloadItemContext = EmptyContext) {\n        val job = getDownloadJob(id) ?: run {\n            dlListDb.getById(id)?.let {\n                createJob(it)\n            }\n        }\n        job?.let {\n            contextContainer.updateContext(id) { it + context }\n            it.resume()\n        }\n    }\n\n    suspend fun reset(id: Long, context: DownloadItemContext = EmptyContext) {\n        val job = getDownloadJob(id) ?: run {\n            dlListDb.getById(id)?.let {\n                createJob(it)\n            }\n        }\n        job?.let {\n            contextContainer.updateContext(id) { it + context }\n            it.reset()\n        }\n    }\n\n    private fun getDownloadJob(id: Long): DownloadJob? {\n//        thisLogger().info(\"finding job for $id\")\n        return downloadJobs.find {\n            it.id == id\n        }.also {\n            if (it == null) {\n//                thisLogger().info(\"there is no job for dl_$id\")\n            } else {\n//                thisLogger().info(\"job found for dl_$id\")\n            }\n        }\n    }\n\n    suspend fun getDownloadList(): List<IDownloadItem> {\n        return dlListDb.getAll()\n    }\n\n    fun onDownloadResuming(downloadItem: IDownloadItem) {\n        listOfJobsEvents.tryEmit(\n            DownloadManagerEvents.OnJobStarting(\n                downloadItem,\n                contextContainer.getContext(downloadItem.id)\n            )\n        )\n    }\n\n    fun onDownloadResumed(downloadItem: IDownloadItem) {\n        listOfJobsEvents.tryEmit(\n            DownloadManagerEvents.OnJobStarted(\n                downloadItem,\n                contextContainer.getContext(downloadItem.id)\n            )\n        )\n    }\n\n    fun onDownloadAdded(downloadItem: IDownloadItem) {\n        listOfJobsEvents.tryEmit(\n            DownloadManagerEvents.OnJobAdded(\n                downloadItem,\n                contextContainer.getContext(downloadItem.id)\n            )\n        )\n    }\n\n    fun onDownloadCanceled(downloadItem: IDownloadItem, throwable: Throwable) {\n        listOfJobsEvents.tryEmit(\n            DownloadManagerEvents.OnJobCanceled(\n                downloadItem,\n                contextContainer.getContext(downloadItem.id), throwable\n            )\n        )\n    }\n\n    fun onDownloadFinished(downloadItem: IDownloadItem) {\n        scope.launch {\n            listOfJobsEvents.tryEmit(\n                DownloadManagerEvents.OnJobCompleted(\n                    downloadItem,\n                    contextContainer.getContext(downloadItem.id)\n                )\n            )\n            deleteJob(downloadItem.id)\n        }\n    }\n\n    fun onDownloadItemChange(downloadItem: IDownloadItem) {\n        scope.launch {\n            listOfJobsEvents.tryEmit(\n                DownloadManagerEvents.OnJobChanged(\n                    downloadItem,\n                    contextContainer.getContext(downloadItem.id)\n                )\n            )\n        }\n    }\n\n    override suspend fun startJob(id: Long, context: DownloadItemContext) {\n        resume(id, context)\n    }\n\n    override suspend fun stopJob(id: Long, context: DownloadItemContext) {\n        pause(id, context)\n    }\n\n    override fun canActivateJob(id: Long): Boolean {\n        val job = downloadJobs.find { id == it.id }\n        val status = job?.status?.value\n//        println(\"job status $status\")\n        return status is DownloadJobStatus.CanBeResumed\n    }\n\n    suspend fun stopAll(\n        context: DownloadItemContext = EmptyContext,\n    ) {\n        downloadJobs.filter {\n            it.status.value == DownloadJobStatus.Downloading\n        }.map {\n            scope.async {\n                pause(it.id, context)\n            }\n        }.awaitAll()\n    }\n\n    fun getActiveCount(): Int {\n        return downloadJobs.filter {\n            it.status.value is DownloadJobStatus.IsActive\n        }.size\n    }\n\n    fun calculateOutputFile(downloadItem: IDownloadItem): File {\n        return File(downloadItem.folder, downloadItem.name)\n    }\n\n    fun getJobStatusOf(id: Long): DownloadJobStatus? {\n        return downloadJobs.find {\n            it.id == id\n        }?.status?.value\n    }\n\n    override val listOfJobsEvents: MutableSharedFlow<DownloadManagerEvents> =\n        MutableSharedFlow(extraBufferCapacity = 64)\n\n    //global speed limiter\n    internal val throttler = Throttler()\n    fun limitGlobalSpeed(bytePerSecond: Long) {\n        throttler.bytesPerSecond(bytePerSecond)\n    }\n\n    fun reloadSetting() {\n        for (downloadJob in downloadJobs) {\n            downloadJob.reloadSettings()\n        }\n    }\n\n    suspend fun updateDownloadItem(\n        id: Long,\n        downloadJobExtraConfig: DownloadJobExtraConfig?,\n        updater: (IDownloadItem) -> Unit,\n    ) {\n        var wasCreated = false\n        val job = getDownloadJob(id) ?: run {\n            dlListDb.getById(id)?.let {\n                wasCreated = true\n                createJob(it)\n            }\n        } ?: return\n        val updated = job.changeConfig(updater, downloadJobExtraConfig)\n        if (wasCreated && updated.status == DownloadStatus.Completed) {\n            deleteJob(job.id)\n        }\n        onDownloadItemChange(updated)\n    }\n\n}\n\nprivate class ContextProvider {\n    val contexts = mutableMapOf<Long, DownloadItemContext>()\n    fun getContext(id: Long): DownloadItemContext {\n        return contexts.getOrDefault(id, EmptyContext)\n    }\n\n    fun setContext(id: Long, context: DownloadItemContext) {\n        if (context == EmptyContext) {\n            removeContext(id)\n            return\n        }\n        contexts[id] = context\n    }\n\n    fun removeContext(id: Long) {\n        contexts.remove(id)\n    }\n\n    fun updateContext(id: Long, block: (DownloadItemContext) -> DownloadItemContext) {\n        setContext(id, getContext(id).let(block))\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/DownloadManagerMinimalControl.kt",
    "content": "package ir.amirab.downloader\n\nimport ir.amirab.downloader.downloaditem.DownloadItemContext\nimport ir.amirab.downloader.downloaditem.EmptyContext\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.flow.SharedFlow\n\nsealed interface DownloadManagerEvents {\n    val downloadItem: IDownloadItem\n    val context: DownloadItemContext\n\n    data class OnJobAdded(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n\n    data class OnJobChanged(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n\n    data class OnJobStarting(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n\n    data class OnJobStarted(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n\n    data class OnJobCompleted(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n\n    data class OnJobCanceled(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext,\n        val e: Throwable\n    ) : DownloadManagerEvents\n\n    data class OnJobRemoved(\n        override val downloadItem: IDownloadItem,\n        override val context: DownloadItemContext\n    ) : DownloadManagerEvents\n}\n\ninterface DownloadManagerMinimalControl {\n    suspend fun startJob(id: Long, context: DownloadItemContext = EmptyContext)\n    suspend fun stopJob(id: Long, context: DownloadItemContext = EmptyContext)\n    fun canActivateJob(id: Long): Boolean\n    val listOfJobsEvents: SharedFlow<DownloadManagerEvents>\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/DownloadSettings.kt",
    "content": "package ir.amirab.downloader\n\ndata class DownloadSettings(\n    //can be changed after boot!\n    var defaultThreadCount: Int = 8,\n    var dynamicPartCreationMode: Boolean = true,\n    var useServerLastModifiedTime: Boolean = false,\n    var globalSpeedLimit: Long = 0,//unlimited\n    var useSparseFileAllocation: Boolean = true,\n    val minPartSize: Long = 2048,//2kB\n    var maxDownloadRetryCount: Int = 0,\n    // WARNING: this is used in boot so make sure to update it before booting\n    // make it val or add a way to reload it properly\n    var appendExtensionToIncompleteDownloads: Boolean = false,\n)\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/Downloader.kt",
    "content": "package ir.amirab.downloader\n\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.serialization.KSerializer\nimport kotlin.reflect.KClass\n\ninterface Downloader<\n        TDownloadItem : IDownloadItem,\n        TDownloadJob : DownloadJob,\n        TDownloadCredentials : IDownloadCredentials,\n        > {\n    fun createJob(\n        item: TDownloadItem,\n        downloadManager: DownloadManager,\n    ): TDownloadJob\n\n    /**\n     * accept if and only if [IDownloadItem] is [TDownloadItem]\n     * */\n    fun accept(item: IDownloadItem): Boolean\n    val downloadItemClass: KClass<TDownloadItem>\n    val downloadCredentialsClass: KClass<TDownloadCredentials>\n    val downloadJobClass: KClass<TDownloadJob>\n\n    val downloadItemSerializer: KSerializer<TDownloadItem>\n    val downloadCredentialsSerializer: KSerializer<TDownloadCredentials>\n}\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/DownloaderRegistry.kt",
    "content": "package ir.amirab.downloader\n\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\nclass DownloaderRegistry {\n    private val list = mutableSetOf<Downloader<IDownloadItem, DownloadJob, IDownloadCredentials>>()\n    fun add(downloader: Downloader<*, *, *>) {\n        @Suppress(\"UNCHECKED_CAST\")\n        list.add(downloader as Downloader<IDownloadItem, DownloadJob, IDownloadCredentials>)\n    }\n\n    fun remove(downloader: Downloader<*, *, *>) {\n        list.remove(downloader)\n    }\n\n    fun createJob(\n        downloadItem: IDownloadItem,\n        downloadManager: DownloadManager,\n    ): DownloadJob {\n        val downloader = requireNotNull(\n            list.firstOrNull {\n                it.accept(downloadItem)\n            }\n        ) {\n            \"Download item '${downloadItem::class.qualifiedName}' not supported!\"\n        }\n        return downloader.createJob(downloadItem, downloadManager)\n    }\n\n    fun getAll() = list.toList()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/NewDownloadItemProps.kt",
    "content": "package ir.amirab.downloader\n\nimport ir.amirab.downloader.downloaditem.DownloadItemContext\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\n\ndata class NewDownloadItemProps(\n    val downloadItem: IDownloadItem,\n    val extraConfig: DownloadJobExtraConfig?,\n    val onDuplicateStrategy: OnDuplicateStrategy,\n    val context: DownloadItemContext,\n)\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/anntation/Markers.kt",
    "content": "package ir.amirab.downloader.anntation\n\n\n/**\n * annotate that a method have long time operation and\n * should not used in main thread\n */\n@Retention(AnnotationRetention.SOURCE)\nannotation class HeavyCall"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/Connection.kt",
    "content": "package ir.amirab.downloader.connection\n\nimport okio.Source\nimport java.io.Closeable\n\ndata class Connection<out TResponseInfo : IResponseInfo>(\n    val source: Source,\n    val contentLength: Long,\n    val responseInfo: TResponseInfo,\n) : Closeable {\n    override fun close() {\n        source.close()\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/HttpDownloaderClient.kt",
    "content": "package ir.amirab.downloader.connection\n\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.http.IHttpBasedDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.IHttpDownloadCredentials\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.downloader.utils.throwIf\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nabstract class HttpDownloaderClient {\n    /**\n     * these headers will be placed at first and maybe overridden by another header\n     */\n    fun defaultHeadersInFirst() = linkedMapOf<String, String>(\n        //empty for now!\n    )\n\n    /**\n     * these headers will be added after others so they override existing headers\n     */\n    fun defaultHeadersInLast() = linkedMapOf(\n        \"accept-encoding\" to \"identity\",\n    )\n\n\n    protected abstract suspend fun actualHead(\n        credentials: IHttpDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): HttpResponseInfo\n\n    protected abstract suspend fun actualConnect(\n        credentials: IHttpBasedDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): Connection<HttpResponseInfo>\n\n    suspend fun head(\n        credentials: IHttpDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): HttpResponseInfo {\n        return usingNetwork {\n            actualHead(credentials, start, end)\n        }\n    }\n\n    suspend fun connect(\n        credentials: IHttpBasedDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): Connection<HttpResponseInfo> {\n        return usingNetwork {\n            actualConnect(credentials, start, end)\n        }\n    }\n\n    suspend fun test(credentials: IHttpDownloadCredentials): HttpResponseInfo {\n        try {\n            val rangeStart = 0L\n            val rangeEnd = 255L\n            val rangeLength = rangeEnd - rangeStart + 1 // 256\n            val response = head(credentials, rangeStart, rangeEnd)\n\n            if (response.isSuccessFul && response.totalLength != rangeLength) {\n                return response\n            }\n        } catch (e: Exception) {\n            e.throwIf { ExceptionUtils.isNormalCancellation(e) }\n            // some servers may reset the connection (ECONNRESET) if we ask for bytes=0-255\n            // so we don't provide resume support for them\n        }\n        // server may return un-standard response we use headless (without resuming support)\n        return head(credentials, null, null)\n    }\n\n    private suspend fun <T> usingNetwork(block: suspend () -> T): T {\n        return withContext(Dispatchers.IO) {\n            block()\n        }\n    }\n\n    companion object {\n        fun createRangeHeader(start: Long, end: Long?) = \"Range\" to \"bytes=$start-${end ?: \"\"}\"\n        fun getDefaultUserAgent(): String = UserAgent.getDefault()\n    }\n}\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/IResponseInfo.kt",
    "content": "package ir.amirab.downloader.connection\n\ninterface IResponseInfo {\n    val isSuccessFul: Boolean\n    val requiresAuth: Boolean\n    val requireBasicAuth: Boolean\n    val resumeSupport: Boolean\n    val isWebPage: Boolean\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/OkHttpHttpDownloaderClient.kt",
    "content": "package ir.amirab.downloader.connection\n\nimport ir.amirab.downloader.connection.proxy.*\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.http.IHttpBasedDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.IHttpDownloadCredentials\nimport ir.amirab.downloader.utils.await\nimport okhttp3.*\nimport java.net.InetSocketAddress\nimport java.net.Proxy\nimport java.net.ProxySelector\n\nclass OkHttpHttpDownloaderClient(\n    private val okHttpClient: OkHttpClient,\n    private val customUserAgentProvider: UserAgentProvider,\n    private val proxyStrategyProvider: ProxyStrategyProvider,\n    private val systemProxySelectorProvider: SystemProxySelectorProvider,\n    private val autoConfigurableProxyProvider: AutoConfigurableProxyProvider,\n) : HttpDownloaderClient() {\n    private fun newCall(\n        downloadCredentials: IHttpBasedDownloadCredentials,\n        start: Long?,\n        end: Long?,\n        extraBuilder: Request.Builder.() -> Unit,\n    ): Call {\n        val rangeHeader = start?.let {\n            createRangeHeader(start, end)\n        }\n        return okHttpClient\n            .applyProxy(downloadCredentials)\n            .newCall(\n                Request.Builder()\n                    .url(downloadCredentials.link)\n                    .apply {\n                        defaultHeadersInFirst().forEach { (k, v) ->\n                            header(k, v)\n                        }\n                        // we don't to add something that we sure that it will be overridden later\n                        if (downloadCredentials.userAgent == null) {\n                            // only add default user agent if we don't specify it\n                            val customUserAgent = customUserAgentProvider.getUserAgent()\n                                ?: getDefaultUserAgent()\n                            header(\"User-Agent\", customUserAgent)\n                        }\n                        downloadCredentials.headers\n                            ?.filter {\n                                //OkHttp handles this header and if we override it,\n                                //makes redirected links to have this \"Host\" instead of their own!, and cause error\n                                !it.key.equals(\"Host\", true)\n                            }\n                            ?.forEach { (k, v) ->\n                                header(k, v)\n                            }\n                        defaultHeadersInLast().forEach { (k, v) ->\n                            header(k, v)\n                        }\n                        val username = downloadCredentials.username\n                        val password = downloadCredentials.password\n                        if (username?.isNotBlank() == true && password?.isNotBlank() == true) {\n                            header(\"Authorization\", Credentials.basic(username, password))\n                        }\n                        downloadCredentials.userAgent?.let { userAgent ->\n                            header(\"User-Agent\", userAgent)\n                        }\n                    }\n                    .apply(extraBuilder)\n                    .apply {\n                        if (rangeHeader != null) {\n                            header(rangeHeader.first, rangeHeader.second)\n                        }\n                    }\n                    .build()\n            )\n    }\n\n    private fun OkHttpClient.applyProxy(\n        downloadCredentials: IHttpBasedDownloadCredentials,\n    ): OkHttpClient {\n        return when (\n            val strategy = proxyStrategyProvider.getProxyStrategyFor(downloadCredentials.link)\n        ) {\n            ProxyStrategy.Direct -> return this\n            ProxyStrategy.UseSystem -> {\n                newBuilder()\n                    .proxySelector(\n                        systemProxySelectorProvider.getSystemProxySelector()\n                            ?: ProxySelector.getDefault()\n                    )\n                    .build()\n            }\n\n            is ProxyStrategy.ByScript -> {\n                val proxySelector = autoConfigurableProxyProvider.getAutoConfigurableProxy(strategy.scriptPath)\n                if (proxySelector != null) {\n                    newBuilder()\n                        .proxySelector(proxySelector)\n                        .build()\n                } else {\n                    this\n                }\n            }\n\n            is ProxyStrategy.ManualProxy -> {\n                val proxy = strategy.proxy\n                return newBuilder()\n                    .proxy(\n                        Proxy(\n                            when (proxy.type) {\n                                ProxyType.HTTP -> Proxy.Type.HTTP\n                                ProxyType.SOCKS -> Proxy.Type.SOCKS\n                            },\n                            InetSocketAddress(proxy.host, proxy.port)\n                        )\n                    ).let {\n                        if (proxy.username != null && proxy.type == ProxyType.HTTP) {\n                            it.proxyAuthenticator { _, r ->\n                                val credentials = Credentials.basic(\n                                    proxy.username,\n                                    proxy.password.orEmpty()\n                                )\n                                r.request\n                                    .newBuilder()\n                                    .header(\"Proxy-Authorization\", credentials)\n                                    .build()\n                            }\n                        } else {\n                            it\n                        }\n                    }.build()\n            }\n        }\n    }\n\n\n    override suspend fun actualHead(\n        credentials: IHttpDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): HttpResponseInfo {\n        newCall(\n            downloadCredentials = credentials,\n            start = start,\n            end = end,\n            extraBuilder = {\n//                head()\n            }\n        ).await().use { response ->\n//            println(response.headers)\n            return createFileInfo(response)\n        }\n    }\n\n    private fun createFileInfo(response: Response): HttpResponseInfo {\n        return HttpResponseInfo(\n            statusCode = response.code,\n            message = response.message,\n            requestUrl = response.request.url.toString(),\n            requestHeaders = response.request.headers.associate { (key, value) ->\n                key.lowercase() to value\n            },\n            responseHeaders = response.headers.associate { (key, value) ->\n                key.lowercase() to value\n            },\n        )\n    }\n\n    override suspend fun actualConnect(\n        credentials: IHttpBasedDownloadCredentials,\n        start: Long?,\n        end: Long?,\n    ): Connection<HttpResponseInfo> {\n        val response = newCall(\n            downloadCredentials = credentials,\n            start = start,\n            end = end,\n            extraBuilder = {\n                get()\n            }\n        ).await()\n        val body = runCatching {\n            requireNotNull(response.body) {\n                \"body is null\"\n            }\n        }.onFailure {\n            response.close()\n        }.getOrThrow()\n        return Connection(\n            source = body.source(),\n            contentLength = body.contentLength(),\n            responseInfo = createFileInfo(response)\n        )\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/UserAgent.kt",
    "content": "package ir.amirab.downloader.connection\n\nobject UserAgent {\n    const val DEFAULT_USER_AGENT =\n        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n\n    fun getDefault(): String {\n        return DEFAULT_USER_AGENT\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/UserAgentProvider.kt",
    "content": "package ir.amirab.downloader.connection\n\ninterface UserAgentProvider {\n    fun getUserAgent(): String?\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/AutoConfigurableProxyProvider.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\nimport java.net.ProxySelector\nimport java.net.URI\n\ninterface AutoConfigurableProxyProvider {\n    fun getAutoConfigurableProxy(\n        uri: String\n    ): ProxySelector?\n\n    class NoOp : AutoConfigurableProxyProvider {\n        override fun getAutoConfigurableProxy(uri: String): ProxySelector? {\n            return null\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/Proxy.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Proxy(\n    val type: ProxyType,\n    val host: String,\n    val port: Int,\n    val username: String?,\n    val password: String?,\n) {\n    companion object {\n        fun default() = Proxy(\n            type = ProxyType.HTTP,\n            host = \"127.0.0.1\",\n            port = 2080,\n            username = null,\n            password = null,\n        )\n    }\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategy.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\nsealed interface ProxyStrategy {\n    data object Direct : ProxyStrategy\n    data object UseSystem : ProxyStrategy\n    data class ManualProxy(val proxy: Proxy) : ProxyStrategy\n    data class ByScript(val scriptPath: String) : ProxyStrategy\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategyProvider.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\ninterface ProxyStrategyProvider {\n    fun getProxyStrategyFor(url: String): ProxyStrategy\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/ProxyType.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\nimport kotlinx.serialization.SerialName\n\nenum class ProxyType {\n    @SerialName(\"http\")\n    HTTP,\n\n    @SerialName(\"socks\")\n    SOCKS;\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/proxy/SystemProxySelectorProvider.kt",
    "content": "package ir.amirab.downloader.connection.proxy\n\nimport java.net.ProxySelector\n\ninterface SystemProxySelectorProvider {\n    fun getSystemProxySelector(): ProxySelector?\n}\n\nclass NoopSystemProxySelectorProvider : SystemProxySelectorProvider {\n    override fun getSystemProxySelector(): ProxySelector? {\n        println(\"System proxy not available\")\n        return null\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/response/HttpResponseInfo.kt",
    "content": "package ir.amirab.downloader.connection.response\n\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.connection.response.headers.getContentRange\nimport ir.amirab.downloader.connection.response.headers.extractFileNameFromContentDisposition\nimport ir.amirab.downloader.exception.UnSuccessfulResponseException\nimport ir.amirab.downloader.utils.FileNameUtil\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.ifThen\n\ndata class HttpResponseInfo(\n    val statusCode: Int,\n    val message: String,\n    val requestUrl: String,\n    val requestHeaders: Map<String, String> = linkedMapOf(),\n    val responseHeaders: Map<String, String> = linkedMapOf(),\n) : IResponseInfo {\n\n    override val isSuccessFul by lazy {\n        statusCode in 200..299\n    }\n\n    val contentLength by lazy {\n        responseHeaders[\"content-length\"]?.toLongOrNull()?.takeIf { it >= 0L }\n    }\n\n    val contentRange by lazy {\n        getContentRange()\n    }\n\n    //total length of whole file even if it is partial content\n    val totalLength by lazy {\n        val responseLength = contentLength ?: return@lazy null\n        // partial length only valid when we have content-length header\n        if (isPartial) {\n            contentRange?.fullSize ?: responseLength\n        } else responseLength\n    }\n    override val requiresAuth by lazy {\n        statusCode == 401\n    }\n\n    override val requireBasicAuth by lazy {\n        requiresAuth && (responseHeaders[\"www-authenticate\"]?.contains(\"basic\", true) ?: false)\n    }\n\n    val isPartial by lazy {\n        statusCode == 206\n    }\n\n    override val resumeSupport by lazy {\n        // maybe server does not give us content-length or content-range, so we ignore resume support\n        isPartial && contentLength != null && contentRange?.fullSize != null\n    }\n\n    override val isWebPage: Boolean by lazy {\n        responseHeaders[\"content-type\"].orEmpty().contains(\"text/html\", ignoreCase = true)\n    }\n\n    val fileName: String? by lazy {\n        run {\n            val nameFromHeader = responseHeaders[\"content-disposition\"]?.let {\n                extractFileNameFromContentDisposition(it)\n            }\n            nameFromHeader ?: HttpUrlUtils.extractNameFromLink(requestUrl)\n        }\n            .orEmpty()\n            .ifThen(isWebPage) {\n                FileNameUtil.replaceExtension(\n                    this,\n                    \"html\",\n                    true\n                )\n            }\n            .takeIf { it.isNotEmpty() }\n    }\n\n    // It is good to use these properties to check file is valid\n    // for now we depend on size\n    val lastModified: String? by lazy {\n        responseHeaders[\"last-modified\"]\n    }\n    val etag: String? by lazy {\n        responseHeaders[\"etag\"]\n    }\n}\n\n\nfun HttpResponseInfo.expectSuccess() = apply {\n    if (!isSuccessFul) {\n        throw UnSuccessfulResponseException(statusCode, message)\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/response/headers/RangeHeaderExtractor.kt",
    "content": "package ir.amirab.downloader.connection.response.headers\n\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\n\ndata class ContentRangeValue(\n    val range: LongRange?,\n    val fullSize: Long?,\n)\n\nfun HttpResponseInfo.getContentRange(): ContentRangeValue? {\n    val value = responseHeaders[\"content-range\"] ?: return null\n    val actualValue = runCatching {\n        // some servers don't append \"bytes \" to the start of the value\n        value.removePrefix(\"bytes \")\n    }.getOrNull() ?: return null\n    if (actualValue.isBlank()) {\n        return null\n    }\n\n    val (rangeString, sizeString) = actualValue\n        .split(\"/\")\n        .takeIf { it.size >= 2 } ?: return null\n\n    val range = try {\n        if (rangeString != \"*\") {\n            rangeString.split(\"-\").map {\n                it.toLong()\n            }.let {\n                it[0]..it[1]\n            }\n        } else {\n            null\n        }\n    } catch (e: Exception) {\n        // NumberFormatException or IndexOutOfBoundException\n        return null\n    }\n\n    val size: Long? = if (sizeString != \"*\") {\n        // some servers not returning * nor integer value.\n        sizeString.toLongOrNull() ?: return null\n    } else null\n\n    return ContentRangeValue(\n        range = range,\n        fullSize = size,\n    )\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/connection/response/headers/fileNameExtractor.kt",
    "content": "package ir.amirab.downloader.connection.response.headers\n\nimport ir.amirab.util.FilenameDecoder\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\nfun extractFileNameFromContentDisposition(contentDispositionValue: String): String? {\n    utf8FileNameRegex.find(contentDispositionValue)\n        ?.groups?.get(\"fileName\")\n        ?.value?.let {\n            runCatching { FilenameDecoder.decode(it, Charsets.UTF_8) }\n                .getOrNull()\n        }?.let {\n            return it\n        }\n    asciiFileNameRegex.find(contentDispositionValue)\n        ?.groups\n        ?.get(\"fileName\")\n        ?.value?.let {\n            var fileName = it\n            fileName = runCatching {\n                EmailMimeWordDecoder.decode(fileName)\n            }.getOrNull() ?: fileName\n            runCatching { FilenameDecoder.decode(fileName, Charsets.UTF_8) }\n                .getOrNull()\n        }?.let {\n            return it\n        }\n    return null\n}\n\nprivate val asciiFileNameRegex = \"\"\"filename=([\"']?)(?<fileName>.*?[^\\\\])\\1(?:; ?|$)\"\"\"\n    .toRegex(RegexOption.IGNORE_CASE)\n\nprivate val utf8FileNameRegex = \"\"\"filename\\*=UTF-8''(?<fileName>[^;\\s]+)(?:; ?|$)\"\"\"\n    .toRegex(RegexOption.IGNORE_CASE)\n\n/**\n * we use this class to decode the filename in content-disposition header in mail servers\n * RFC 2047\n */\nprivate object EmailMimeWordDecoder {\n    fun decode(string: String): String {\n        return decodeMimeEncodedFilename(string)\n    }\n\n    private val regex by lazy {\n        \"\"\"=\\?(?<charset>[^?]+)\\?(?<encoding>[BQ])\\?(?<encodedText>[^?]+)\\?=\"\"\"\n            .toRegex(RegexOption.IGNORE_CASE)\n    }\n\n    @OptIn(ExperimentalEncodingApi::class)\n    private fun decodeMimeEncodedFilename(input: String): String {\n        return regex.replace(input) {\n            runCatching {\n                val match = it.groups\n\n                val charset = match.requireName(\"charset\").value\n                val encoding = match.requireName(\"encoding\").value.uppercase()\n                val encodedText = match.requireName(\"encodedText\").value\n\n                val bytes = when (encoding) {\n                    \"B\" -> Base64.decode(encodedText)\n                    \"Q\" -> decodeMimeQuotedPrintable(encodedText)\n                    else -> return@replace input\n                }\n                String(bytes, charset(charset))\n            }.getOrNull() ?: it.value\n        }\n    }\n\n    private fun decodeMimeQuotedPrintable(encoded: String): ByteArray {\n        val sb = StringBuilder()\n\n        var i = 0\n        while (i < encoded.length) {\n            val c = encoded[i]\n            when {\n                c == '=' && i + 2 < encoded.length -> {\n                    val hex = encoded.substring(i + 1, i + 3)\n                    val byte = hex.toIntOrNull(16)?.toChar()\n                    if (byte != null) {\n                        sb.append(byte)\n                        i += 3\n                    } else {\n                        sb.append(c)\n                        i++\n                    }\n                }\n\n                c == '_' -> {\n                    sb.append(' ') // _ represents space in Q encoding\n                    i++\n                }\n\n                else -> {\n                    sb.append(c)\n                    i++\n                }\n            }\n        }\n        return sb.toString().toByteArray(Charsets.ISO_8859_1)\n    }\n\n    private fun MatchGroupCollection.requireName(name: String): MatchGroup {\n        return requireNotNull(this[name]) {\n            \"Group $name not found\"\n        }\n    }\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/DownloadListFileStorage.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.utils.SuspendLockList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nclass DownloadListFileStorage(\n    private val downloadListFolder: File,\n    private val fileSaver: TransactionalFileSaver,\n) : IDownloadListDb {\n\n    private val fileLocks = SuspendLockList<Long>()\n\n    fun getDownloadItemFile(id: Long): File {\n        return downloadListFolder.resolve(\"$id.json\")\n    }\n\n    override suspend fun getAll(): List<IDownloadItem> {\n        return withContext(Dispatchers.IO) {\n            val jsonExtension = \".json\"\n            downloadListFolder.listFiles()\n                ?.mapNotNull { file ->\n                    file.name\n                        .takeIf { it.endsWith(jsonExtension) }\n                        ?.removeSuffix(jsonExtension)\n                        ?.toLongOrNull()\n                        ?.let { get(file, it) }\n                }.orEmpty()\n        }\n    }\n\n    private suspend fun get(file: File, id: Long): IDownloadItem? {\n        return fileLocks.withLock(id) {\n            fileSaver.readObject(file)\n        }\n    }\n\n    override suspend fun getById(id: Long): IDownloadItem? {\n        return withContext(Dispatchers.IO) {\n            get(getDownloadItemFile(id), id)\n        }\n    }\n\n    private val addLock = Mutex()\n    override suspend fun add(item: IDownloadItem) {\n        withContext(Dispatchers.IO) {\n            addLock.withLock {\n                fileLocks.withLock(item.id) {\n                    fileSaver.writeObject(getDownloadItemFile(item.id), item)\n                    val lastId = getLastId()\n                    if (lastId < item.id) {\n                        setLastId(item.id)\n                    }\n                }\n            }\n        }\n    }\n\n    override suspend fun update(item: IDownloadItem) {\n        withContext(Dispatchers.IO) {\n            // we don't use same lock for all items , but create lock for each item\n            fileLocks.withLock(item.id) {\n                fileSaver.writeObject(getDownloadItemFile(item.id), item)\n            }\n        }\n    }\n\n    override suspend fun removeById(itemId: Long) {\n        getDownloadItemFile(itemId).delete()\n    }\n\n    override suspend fun remove(item: IDownloadItem) {\n        removeById(item.id)\n    }\n\n\n    private val lastIdFile = downloadListFolder.resolve(\"last_id.txt\")\n    private fun setLastId(id: Long) {\n        fileSaver.writeObject(lastIdFile, id)\n    }\n\n    override suspend fun getLastId(): Long {\n        return withContext(Dispatchers.IO) {\n            var lastId = fileSaver.readObject<Long>(lastIdFile)\n            if (lastId == null) {\n                lastId = getLastIdFromFiles()\n                setLastId(lastId)\n            }\n            lastId\n        }\n    }\n\n    private fun getLastIdFromFiles(): Long {\n        return downloadListFolder.listFiles()!!.filter {\n            it.name.endsWith(\".json\") && it.isFile\n        }.maxOfOrNull {\n            it.name\n                .substring(0, it.name.length - \".json\".length)\n                .toLong()\n        } ?: -1L\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/DownloadQueuePersistedDataAccess.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.queue.ScheduleTimes\nimport kotlinx.serialization.Serializable\n\n/**\n * this is all states of queue that need to be persisted\n * */\n@Serializable\ndata class QueueModel(\n    val id:Long,\n    val name: String,\n    val maxConcurrent: Int = 2,\n    val queueItems: List<Long> = emptyList(),\n    val scheduledTimes: ScheduleTimes = ScheduleTimes.default(),\n    val stopQueueOnEmpty:Boolean=false,\n)\n/**\n * CRUD all queues\n * */\ninterface IDownloadQueueDatabase {\n    suspend fun getAllQueueIds(): List<Long>\n    suspend fun getAllQueues(): List<QueueModel>\n    suspend fun setAllQueues(queues: List<QueueModel>)\n    suspend fun deleteAllQueues()\n    suspend fun getQueue(queueId:Long):QueueModel\n    suspend fun deleteQueue(queue: Long)\n    suspend fun updateQueue(queue: QueueModel)\n    suspend fun addQueue(queue: QueueModel)\n}\n\n/**\n * update a single queue (it is a view of a single queue in database)\n * this is passed to queue for access its persistent data and update it\n * setters must be implemented thread safe\n */\ninterface DownloadQueuePersistedDataAccess {\n\n    suspend fun setModel(queue: QueueModel)\n    suspend fun getModel():QueueModel\n\n    suspend fun update(update: (QueueModel) -> QueueModel) {\n        setModel(\n            update(\n                getModel()\n            )\n        )\n    }\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/IDownloadListDb.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\n\ninterface IDownloadListDb {\n    // modification/add implementations must be thread safe\n\n    suspend fun getAll(): List<IDownloadItem>\n    suspend fun getById(id: Long): IDownloadItem?\n    suspend fun add(item: IDownloadItem)\n    suspend fun update(item: IDownloadItem)\n    suspend fun remove(item: IDownloadItem)\n    suspend fun removeById(itemId: Long)\n    suspend fun getLastId(): Long\n\n//    suspend fun allAsFlow(): Flow<List<DownloadItem>>\n}\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/IDownloadPartListDb.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.part.Parts\n\ninterface IDownloadPartListDb {\n    suspend fun getParts(id: Long): Parts?\n    suspend fun setParts(id: Long, parts: Parts)\n    suspend fun clear()\n    suspend fun removeParts(id: Long)\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/MemoryDownloadListDB.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\n\nclass MemoryDownloadListDB : IDownloadListDb {\n\n    private val list: MutableList<IDownloadItem> = mutableListOf()\n    override suspend fun getAll(): List<IDownloadItem> {\n        return list.toList()\n    }\n\n    override suspend fun getById(id: Long): IDownloadItem? {\n        return list.find { it.id == id }\n    }\n\n    override suspend fun add(item: IDownloadItem) {\n        require(list.all { it.id != item.id }) {\n            \"duplicate download id\"\n        }\n        list.add(item)\n    }\n\n    override suspend fun update(item: IDownloadItem) {\n        list.indexOfFirst {\n            it.id == item.id\n        }.takeIf { it != -1 }?.let { index ->\n            list.set(index, item)\n        }\n    }\n\n    override suspend fun remove(item: IDownloadItem) {\n        removeById(item.id)\n    }\n\n    override suspend fun removeById(itemId: Long) {\n        val index = list.indexOfFirst {\n            it.id == itemId\n        }\n        list.removeAt(index)\n    }\n\n    override suspend fun getLastId(): Long {\n        return list.maxByOrNull {\n            it.id\n        }?.id ?: -1\n    }\n\n    private val flow = MutableSharedFlow<List<IDownloadItem>>(\n        replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n\n    suspend fun onDbUpdate() {\n        flow.tryEmit(getAll())\n    }\n\n    suspend fun allAsFlow() = flow\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/MemoryDownloadPartStatesDB.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.part.Parts\n\nclass MemoryDownloadPartStatesDB : IDownloadPartListDb {\n    private val list = mutableMapOf<Long, Parts>()\n    override suspend fun getParts(id: Long): Parts? {\n        return list[id]\n    }\n\n    override suspend fun setParts(id: Long, parts: Parts) {\n        list[id] = parts.clone()\n    }\n\n    override suspend fun removeParts(id: Long) {\n        list.remove(id)\n    }\n\n    override suspend fun clear() {\n        list.clear()\n    }\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/PartListFileStorage.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.downloader.part.Parts\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nclass PartListFileStorage(\n    val folder: File,\n    val fileSaver: TransactionalFileSaver,\n) : IDownloadPartListDb {\n    fun getFileForId(id: Long): File {\n        val resolve = folder.resolve(\"$id.json\")\n        return resolve\n    }\n\n    override suspend fun getParts(id: Long): Parts? {\n        return withContext(Dispatchers.IO) {\n            fileSaver.readObject(getFileForId(id))\n        }\n    }\n\n    override suspend fun setParts(id: Long, parts: Parts) {\n        withContext(Dispatchers.IO) {\n            kotlin.runCatching {\n                val file = getFileForId(id)\n                fileSaver.writeObject(file, parts)\n            }\n        }\n    }\n\n    override suspend fun removeParts(id: Long) {\n        getFileForId(id).delete()\n    }\n\n    override suspend fun clear() {\n        kotlin.runCatching {\n            folder.listFiles()\n        }.getOrNull()?.let {\n            for (file in it) {\n                file.delete()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/QueueFileStorage.kt",
    "content": "package ir.amirab.downloader.db\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nprivate const val queueExtension = \"json\"\n\nclass DownloadQueueFileStorageDatabase(\n    val fileSaver: TransactionalFileSaver,\n    val queueFolder: File,\n) : IDownloadQueueDatabase {\n    private val lock = Mutex()\n    private fun getFileOfQueue(queue: QueueModel): File {\n        return getFileOfQueue(queue.id)\n    }\n\n    private fun getFileOfQueue(id: Long): File {\n        return queueFolder.resolve(\"$id.json\")\n    }\n\n    private fun getQueueFiles(): List<File> {\n        return queueFolder.listFiles()\n            .filter {\n                it.isFile && it.extension == queueExtension\n            }\n    }\n\n\n    override suspend fun getAllQueueIds(): List<Long> {\n        return withContext(Dispatchers.IO) {\n            getQueueFiles().map {\n                it.name.substring(0,it.name.length-\".$queueExtension\".length)\n            }.mapNotNull {\n                it.toLongOrNull()\n            }\n        }\n    }\n\n    override suspend fun getQueue(queueId: Long): QueueModel {\n        return withContext(Dispatchers.IO) {\n            requireNotNull(fileSaver.readObject(\n                getFileOfQueue(queueId)\n            )\n            ) {\n                \"Queue with $queueId returned null\"\n            }\n        }\n    }\n\n    override suspend fun getAllQueues(): List<QueueModel> {\n        return getAllQueueIds().mapNotNull {\n            kotlin.runCatching {\n                getQueue(it)\n            }.getOrNull()\n        }\n    }\n\n    override suspend fun deleteAllQueues() {\n        getQueueFiles().forEach {\n            it.delete()\n        }\n    }\n\n    override suspend fun setAllQueues(queues: List<QueueModel>) {\n        lock.withLock {\n            deleteAllQueues()\n            queues.forEach { addQueue(it) }\n        }\n    }\n\n    override suspend fun deleteQueue(queueId: Long) {\n        lock.withLock {\n            getFileOfQueue(queueId).delete()\n        }\n    }\n\n    override suspend fun updateQueue(queue: QueueModel) {\n        lock.withLock {\n            withContext(Dispatchers.IO) {\n                fileSaver.writeObject(\n                    getFileOfQueue(queue),\n                    queue\n                )\n            }\n        }\n    }\n\n    override suspend fun addQueue(queue: QueueModel) {\n        val fileOfQueue = getFileOfQueue(queue)\n        withContext(Dispatchers.IO) {\n            fileSaver.writeObject(\n                fileOfQueue,\n                queue,\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/db/TransactionalFileSaver.kt",
    "content": "package ir.amirab.downloader.db\n\nimport ir.amirab.util.tryAtomicMove\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.Serializer\nimport kotlinx.serialization.json.Json\nimport okio.FileSystem\nimport okio.Path.Companion.toOkioPath\nimport java.io.File\n\nclass TransactionalFileSaver(\n    val json: Json,\n) {\n    fun getBakFile(file: File) = File(\"$file.tmp\")\n    inline fun <reified T> writeObject(file: File, t: T) {\n        val text = json.encodeToString(t)\n        writeText(file, text)\n    }\n\n    fun <T> writeObject(file: File, t: T, kSerializer: KSerializer<T>) {\n        val text = json.encodeToString(kSerializer, t)\n        writeText(file, text)\n    }\n\n    fun writeText(file: File, text: String) {\n        val bakFile = getBakFile(file)\n        kotlin.runCatching {\n            FileSystem.SYSTEM.write(\n                file = bakFile.toOkioPath()\n            ) {\n                writeUtf8(text)\n            }\n        }.onSuccess {\n            bakFile.tryAtomicMove(file)\n        }.getOrThrow()\n    }\n\n    fun readText(file: File): String? {\n        return runCatching {\n            FileSystem.SYSTEM.read(file.toOkioPath()) {\n                readUtf8()\n            }\n        }.getOrNull()\n    }\n\n    inline fun <reified T> readObject(file: File): T? {\n        return kotlin.runCatching {\n            val text = readText(file)!!\n            json.decodeFromString<T>(text)\n        }.getOrNull()\n    }\n\n    fun <T> readObject(file: File, serializer: KSerializer<T>): T? {\n        return kotlin.runCatching {\n            val text = readText(file)!!\n            json.decodeFromString(serializer, text)\n        }.getOrNull()\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/destination/DestWriter.kt",
    "content": "package ir.amirab.downloader.destination\n\nimport ir.amirab.downloader.anntation.HeavyCall\nimport okio.Buffer\nimport okio.FileHandle\nimport okio.FileSystem\nimport okio.Sink\nimport java.io.File\n\n/**\n * this class provide an interface for independent write buffer by Part Manager Crew\n * */\nclass DestWriter(\n    val id: Long,\n    val file: File,\n    var seekPos: Long,\n    val writer: FileHandle,\n) {\n\n    private var status: Status = Status.NotPrepared\n\n\n    @Transient\n    private var sink: Sink? = null\n\n    @HeavyCall\n    @Synchronized\n    fun prepare() {\n        if (status != Status.NotPrepared) {\n            error(\"already prepared : status=$status\")\n        }\n        if (!file.exists()) {\n            error(\"file not exists, can't prepare file\")\n        }\n\n        status = Status.Preparing\n        sink = writer.sink(seekPos)\n        status = Status.Prepared\n//        println(\"part #$id started to write from $seekPos\")\n    }\n\n    @Synchronized\n    fun release() {\n        sink?.close()\n        status = Status.NotPrepared\n//        println(\"part #$id stopped to write to $seekPos\")\n    }\n\n    fun write(buffer: Buffer, length: Long = buffer.size) {\n        val currentStatus = status\n        if (currentStatus == Status.NotPrepared) {\n            throw Exception(\"first prepare\")\n        }\n        if (currentStatus == Status.Finished) {\n            throw Exception(\"finished still writing?\")\n        }\n        if (currentStatus == Status.Prepared) {\n            status = Status.Writing\n        }\n        sink!!.write(buffer, length)\n        seekPos += length\n//    println(\"seek :$seekPos\")\n    }\n\n    enum class Status { NotPrepared, Preparing, Prepared, Writing, Finished }\n\n    fun use(block: (DestWriter) -> Unit) {\n//        println(\"using dest\")\n        prepare()\n        try {\n            block(this)\n        } catch (e: Exception) {\n            throw e\n        } finally {\n            try {\n//                println(\"release dest\")\n                release()\n            } catch (_: Exception) {\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/destination/DownloadDestination.kt",
    "content": "package ir.amirab.downloader.destination\n\nimport ir.amirab.downloader.part.DownloadPart\nimport ir.amirab.util.tryAtomicMove\nimport java.io.File\n\nabstract class DownloadDestination(\n    outputFile: File,\n) {\n    val outputFile = outputFile.canonicalFile.absoluteFile\n\n    protected val fileParts = mutableListOf<DestWriter>()\n    protected var allPartsDownloaded = false\n    protected var requestedToChangeLastModified: Long? = null\n\n    protected open fun onAllFilePartsRemoved() {\n        updateLastModified()\n    }\n\n    open fun onAllPartsCompleted(\n        onProgressUpdate: (Int?) -> Unit = {}\n    ) {\n        allPartsDownloaded = true\n        cleanUpJunkFiles()\n        updateLastModified()\n    }\n\n    open fun cleanUpJunkFiles() {}\n\n    abstract fun getWriterFor(part: DownloadPart): DestWriter\n    abstract fun canGetFileWriter(): Boolean\n\n    fun returnIfAlreadyHaveWriter(partId: Long): DestWriter? {\n        synchronized(this) {\n            return fileParts.find {\n                val condition = it.id == partId\n//      if (condition) {\n//        logger.info(\"part id$partId already have an associated file\")\n//      }\n                condition\n            }\n        }\n    }\n\n    open fun deleteOutputFile() {\n        outputFile.delete()\n    }\n\n    abstract suspend fun prepareFile(onProgressUpdate: (Int?) -> Unit)\n    abstract suspend fun isDownloadedPartsIsValid(): Boolean\n    abstract fun flush()\n\n    open fun onPartCancelled(part: DownloadPart) {\n        synchronized(this) {\n            val cleanAny = fileParts.removeAll {\n                it.id == part.getID()\n            }\n            if (cleanAny) {\n                if (fileParts.isEmpty()) {\n                    onAllFilePartsRemoved()\n                }\n            }\n        }\n    }\n\n    /**\n     * specify last modified time to be used when this destination finish its work\n     * for example file paused / finished\n     */\n    fun setLastModified(timestamp: Long?) {\n        requestedToChangeLastModified = timestamp\n    }\n\n    protected open fun updateLastModified() {\n        kotlin.runCatching {\n            requestedToChangeLastModified?.let {\n                outputFile.setLastModified(it)\n            }\n        }\n    }\n\n    /**\n     * after you use this method this class must be recreated\n     */\n    open fun moveOutput(to: File) {\n        if (outputFile.exists()) {\n            try {\n                outputFile.tryAtomicMove(to)\n            } catch (e: Exception) {\n                throw IllegalStateException(\n                    \"Failed to move output file to the new destination: ${e.localizedMessage}\",\n                    e,\n                )\n            }\n        }\n    }\n\n    companion object {\n        fun prepareDestinationFolder(\n            outputFile: File,\n        ) {\n            outputFile.parentFile.let {\n                it.canonicalFile.mkdirs()\n                if (!it.exists()) {\n                    error(\"can't create folder for destination file $it\")\n                }\n\n                if (!it.isDirectory) {\n                    error(\"${outputFile.parentFile} is not a directory\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/destination/IncompleteFileUtil.kt",
    "content": "package ir.amirab.downloader.destination\n\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isWindows\nimport java.io.File\n\nobject IncompleteFileUtil {\n    private const val SYSTEM_MAXIMUM_FILE_LENGTH = 255\n    private const val SYSTEM_MAXIMUM_FULL_PATH_LENGTH = 259\n    private fun createExtension(id: Long): String {\n        return \".dl-$id.abdm.part\"\n    }\n\n    fun addIncompleteIndicator(file: File, id: Long): File {\n        val ext = createExtension(id)\n        if (!file.name.endsWith(ext)) {\n            // if the file name is too long, we need to trim it\n            // so that the full path length does not exceed the system limit\n            // this is a workaround for Windows systems which have a maximum path length of 260 characters\n            // see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation\n            val trimmedFileName = if (Platform.isWindows()) {\n                // this + 1 is to account for last slash in the path\n                val parentPathLength = file.parentFile.path.length + 1\n                file.name.take(\n                    (SYSTEM_MAXIMUM_FULL_PATH_LENGTH - (parentPathLength + ext.length))\n                        // maybe the remaining length is negative which means the file name is too long even after trim!\n                        // we hope that filesystem will allow us to create such a file otherwise we will crash!\n                        // in that case the user must reduce the path length and try again\n                        .coerceAtLeast(0)\n                )\n            } else {\n                // and some other systems which have a maximum file name length of 255 characters\n                file.name.take(SYSTEM_MAXIMUM_FILE_LENGTH - ext.length)\n            }\n\n            return file.parentFile.resolve(trimmedFileName + ext)\n        }\n        return file\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/destination/SegmentedDownloadDestination.kt",
    "content": "package ir.amirab.downloader.destination\n\nimport ir.amirab.downloader.part.DownloadPart\nimport ir.amirab.downloader.utils.calcPercent\nimport okhttp3.internal.closeQuietly\nimport okio.FileSystem\nimport okio.Path.Companion.toOkioPath\nimport java.io.File\n\nclass SegmentedDownloadDestination(\n    // a directory unique for this item!\n    val tempDirectory: File,\n    val getFileName: (DownloadPart) -> String,\n    val getAllParts: () -> List<DownloadPart>,\n    val appendMode: Boolean,\n    outputFile: File,\n) : DownloadDestination(outputFile) {\n    private fun getFileOfPart(downloadPart: DownloadPart): File {\n        val id = downloadPart.getID()\n        return tempDirectory.resolve(\"$id\")\n    }\n\n    override fun getWriterFor(part: DownloadPart): DestWriter {\n        tempDirectory.mkdirs()\n        val tFile = getFileOfPart(part)\n        val writer = FileSystem.SYSTEM.openReadWrite(tFile.toOkioPath(), mustCreate = false, mustExist = false)\n        val newSize = if (appendMode) {\n            writer.size()\n        } else {\n            // part starts from the beginning\n            0\n        }\n        writer.resize(newSize)\n        val destWriter = DestWriter(\n            id = part.getID(),\n            file = tFile,\n            seekPos = newSize,\n            writer = writer,\n        )\n        synchronized(this) {\n            fileParts.add(destWriter)\n        }\n        return destWriter\n    }\n\n    override fun onPartCancelled(part: DownloadPart) {\n        super.onPartCancelled(part)\n        val id = part.getID()\n        synchronized(this) {\n            fileParts\n                .find { it.id == id }\n                ?.writer\n                ?.closeQuietly()\n        }\n    }\n\n\n    override fun canGetFileWriter(): Boolean {\n        return true\n    }\n\n    override suspend fun prepareFile(onProgressUpdate: (Int?) -> Unit) {\n\n    }\n\n    override suspend fun isDownloadedPartsIsValid(): Boolean {\n        return tempDirectory.exists()\n    }\n\n    fun isDownloadPartValid(part: DownloadPart): Boolean {\n        return getFileOfPart(part).exists()\n    }\n\n    override fun cleanUpJunkFiles() {\n        runCatching {\n            FileSystem.SYSTEM.deleteRecursively(tempDirectory.toOkioPath())\n        }\n    }\n\n    override fun onAllPartsCompleted(onProgressUpdate: (Int?) -> Unit) {\n        assemble(\n            getAllParts()\n                .sortedBy { it.getID() }\n                .map { tempDirectory.resolve(getFileName(it)) },\n            outputFile,\n            onProgressUpdate\n        )\n        super.onAllPartsCompleted(onProgressUpdate)\n    }\n\n    fun assemble(\n        sources: List<File>,\n        destination: File,\n        onProgress: (Int) -> Unit\n    ) {\n        DownloadDestination.prepareDestinationFolder(outputFile)\n        val totalLength = sources.sumOf { it.length() }\n        var totalWritten = 0L\n        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)\n        var percent = 0\n        destination.outputStream().use { dst ->\n            sources.forEach { sourceFile ->\n                sourceFile.inputStream().use { src ->\n                    onProgress(percent)\n                    while (true) {\n                        val len = src.read(buffer)\n                        if (len == -1) break\n                        dst.write(buffer, 0, len)\n                        totalWritten += len\n\n                        val newPercent = calcPercent(totalWritten, totalLength)\n                        if (newPercent != percent) {\n                            onProgress(newPercent)\n                            percent = newPercent\n                        }\n                    }\n                }\n            }\n        }\n        onProgress(100)\n    }\n\n    override fun flush() {\n        synchronized(this) {\n            fileParts.forEach {\n                it.writer.flush()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/destination/SimpleDownloadDestination.kt",
    "content": "package ir.amirab.downloader.destination\n\nimport ir.amirab.downloader.anntation.HeavyCall\nimport ir.amirab.downloader.part.DownloadPart\nimport ir.amirab.downloader.utils.EmptyFileCreator\nimport ir.amirab.util.tryAtomicMove\nimport okio.FileHandle\nimport okio.FileSystem\nimport okio.Path.Companion.toOkioPath\nimport java.io.File\n\nclass SimpleDownloadDestination(\n    file: File,\n    val appendExtensionToIncompleteDownloads: Boolean,\n    val downloadId: Long,\n    private val emptyFileCreator: EmptyFileCreator,\n) : DownloadDestination(\n    outputFile = file,\n) {\n    // this is only used when appendExtensionToIncompleteDownloads is true\n    val incompleteFile by lazy {\n        IncompleteFileUtil.addIncompleteIndicator(outputFile, downloadId)\n    }\n\n    private val fileToWrite: File = if (appendExtensionToIncompleteDownloads) {\n        incompleteFile\n    } else {\n        outputFile\n    }\n\n    private var _fileHandle: FileHandle? = null\n    private val fileHandle: FileHandle\n        get() {\n            synchronized(this) {\n                if (_fileHandle == null) {\n                    return initFileHandle()\n                }\n            }\n            return _fileHandle!!\n        }\n\n    private fun initFileHandle(): FileHandle {\n        // let's open a file for writing to it!\n        // it will be removed when all parts are cancelled so this method\n        // maybe called multiple times\n        val handle = FileSystem.SYSTEM.openReadWrite(fileToWrite.toOkioPath())\n        _fileHandle = handle\n        return handle\n    }\n\n    private fun removeFileHandle() {\n        //close and release handle to unlock the file\n        synchronized(this) {\n            _fileHandle?.close()\n            _fileHandle = null\n        }\n    }\n\n    override fun onAllFilePartsRemoved() {\n        super.onAllFilePartsRemoved()\n//        println(\"release handle\")\n        removeFileHandle()\n    }\n\n    override fun onAllPartsCompleted(onProgressUpdate: (Int?) -> Unit) {\n        if (appendExtensionToIncompleteDownloads) {\n            // this function maybe called at some point that we may not even start download yet.\n            // for example when the download has already completed, the DownloadJob will call this function! so we should do nothing.\n            val incompleteFile = incompleteFile\n            if (!incompleteFile.exists()) {\n                return\n            }\n            val completeFile = outputFile\n            // delete old file if exists to override with new one!\n            if (completeFile.exists()) {\n                completeFile.delete()\n            }\n            try {\n                incompleteFile.tryAtomicMove(completeFile)\n            } catch (e: Exception) {\n                // prevent remove the part file if it can't be moved by us!\n                throw IllegalStateException(\n                    \"failed to move .part file to the actual output file! ${e.localizedMessage}\",\n                    e\n                )\n            }\n        }\n        // clean up junk files called in the super class\n        super.onAllPartsCompleted(onProgressUpdate)\n    }\n\n    var outputSize: Long = -1\n    override fun getWriterFor(\n        part: DownloadPart,\n    ): DestWriter {\n        if (!canGetFileWriter()) {\n            throw IllegalStateException(\"First check then ask for...\")\n        }\n        val outFile = fileToWrite\n        val returned = returnIfAlreadyHaveWriter(part.getID())\n        returned?.let { return it }\n        val writer = DestWriter(\n            part.getID(),\n            outFile,\n            part.current,\n            fileHandle,\n        )\n        synchronized(this) {\n            fileParts.add(writer)\n        }\n        return writer\n    }\n\n    override fun flush() {\n        runCatching {\n            _fileHandle?.flush()\n        }\n    }\n\n    fun prepareDestinationFolder() {\n        DownloadDestination.prepareDestinationFolder(fileToWrite)\n    }\n\n    @HeavyCall\n    override suspend fun prepareFile(onProgressUpdate: (Int?) -> Unit) {\n//        println(\"preparing file \")\n//        println(\"file info path=$outputFile size=${outputFile.runCatching { length() }.getOrNull()}\")\n        val incompleteFile = fileToWrite\n        prepareDestinationFolder()\n        emptyFileCreator\n            .prepareFile(incompleteFile, outputSize, onProgressUpdate)\n    }\n\n    /**\n     * restart download if file was deleted by user!\n     * this function will be called when the download is resumed, and it's not completed yet.\n     */\n    override suspend fun isDownloadedPartsIsValid(): Boolean {\n        val targetFile = fileToWrite\n        val fileExists = targetFile.exists()\n        val fileEqualToContentSize = targetFile.length() == outputSize\n        return fileExists && fileEqualToContentSize\n    }\n\n    override fun canGetFileWriter(): Boolean {\n        return true\n    }\n\n    override fun updateLastModified() {\n        runCatching {\n            requestedToChangeLastModified?.let {\n                fileToWrite.setLastModified(it)\n            }\n        }\n    }\n\n    override fun moveOutput(to: File) {\n        if (appendExtensionToIncompleteDownloads) {\n            val incompleteFile = incompleteFile\n            if (incompleteFile.exists()) {\n                try {\n                    incompleteFile.tryAtomicMove(IncompleteFileUtil.addIncompleteIndicator(to, downloadId))\n                } catch (e: Exception) {\n                    throw IllegalStateException(\n                        \"Failed to move .part file to the new destination: ${e.localizedMessage}\",\n                        e,\n                    )\n                }\n            }\n        }\n        super.moveOutput(to)\n    }\n\n    override fun cleanUpJunkFiles() {\n        // remove incomplete file if exists\n        val incompleteFile = incompleteFile\n        if (incompleteFile.exists()) {\n            incompleteFile.delete()\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/DownloadItemContext.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\ninterface DownloadItemContext {\n    operator fun <T : Element> get(key: Key<T>): T?\n\n    fun <T> fold(initial: T, operation: (acc: T, element: Element) -> T): T\n\n    fun minusKey(key: Key<*>): DownloadItemContext\n\n    operator fun plus(context: DownloadItemContext): DownloadItemContext {\n        if (context === EmptyContext) {\n            return this\n        }\n        return context.fold(this) { acc, element ->\n            //maybe same key alreadyExists\n            val removed = acc.minusKey(element.getKey())\n            if (removed === EmptyContext) {\n                element\n            } else {\n                CombinedContext(removed, element)\n            }\n        }\n    }\n\n    interface Key<T : Element>\n    interface Element : DownloadItemContext {\n\n        fun getKey(): Key<*>\n        override fun <T : Element> get(key: Key<T>): T? {\n            @Suppress(\"UNCHECKED_CAST\")\n            return if (getKey() === key) return this as T\n            else null\n        }\n\n        override fun minusKey(key: Key<*>): DownloadItemContext {\n            return if (key === getKey()) {\n                EmptyContext\n            } else {\n                this\n            }\n        }\n\n        override fun <T> fold(initial: T, operation: (acc: T, element: Element) -> T): T {\n            return operation(initial, this)\n        }\n    }\n}\n\ndata object EmptyContext : DownloadItemContext {\n    override fun <T : DownloadItemContext.Element> get(key: DownloadItemContext.Key<T>): T? {\n        return null\n    }\n\n    override fun <T> fold(initial: T, operation: (acc: T, element: DownloadItemContext.Element) -> T): T {\n        return initial\n    }\n\n    override fun plus(context: DownloadItemContext): DownloadItemContext {\n        return context\n    }\n\n    override fun minusKey(key: DownloadItemContext.Key<*>): DownloadItemContext {\n        return this\n    }\n}\n\nprivate data class CombinedContext(\n    val left: DownloadItemContext,\n    val element: DownloadItemContext.Element,\n) : DownloadItemContext {\n    override fun <T : DownloadItemContext.Element> get(key: DownloadItemContext.Key<T>): T? {\n        var cur = this\n        while (true) {\n            if (cur.element[key] != null) {\n                @Suppress(\"UNCHECKED_CAST\")\n                return cur.element as T\n            }\n            val next = cur.left\n            //make recursive function flatten\n            if (next is CombinedContext) {\n                cur = next\n            } else {\n                return next[key]\n            }\n        }\n    }\n\n    override fun <T> fold(initial: T, operation: (acc: T, element: DownloadItemContext.Element) -> T): T {\n        return operation(left.fold(initial, operation), element)\n    }\n\n    override fun minusKey(key: DownloadItemContext.Key<*>): DownloadItemContext {\n        if (element.getKey() === key) {\n            return left\n        }\n        val newLeft = left.minusKey(key)\n        return when {\n            newLeft === EmptyContext -> element\n            newLeft === left -> this\n            else -> CombinedContext(newLeft, element)\n        }\n    }\n\n    override fun toString(): String {\n        return fold(\"\") { acc, element ->\n            if (acc.isEmpty()) {\n                \"$element\"\n            } else {\n                \"$acc , $element\"\n            }\n        }.let { \" { $it } \" }\n    }\n}\n\n//extensions\nfun <T> DownloadItemContext.map(transform: (DownloadItemContext.Element) -> T): List<T> {\n    return fold(mutableListOf()) { acc, element ->\n        acc.apply { add(transform(element)) }\n    }\n}\n\nfun DownloadItemContext.minusKeys(vararg keys: DownloadItemContext.Key<*>) {\n    var cur = this\n    for (key in keys) {\n        cur = cur.minusKey(key)\n    }\n}\n\nfun DownloadItemContext.toList() = map { it }\n\nfun DownloadItemContext.keys() = map { it.getKey() }\n\nfun DownloadItemContext.isEmpty(): Boolean {\n    return this === EmptyContext\n}\n\nval DownloadItemContext.size: Int\n    get() {\n        return fold(0) { acc, _ -> acc + 1 }\n    }\n\nfun DownloadItemContext.iterator(): Iterator<DownloadItemContext.Element> {\n    return toList().iterator()\n}\n\nfun DownloadItemContext.contains(element: DownloadItemContext.Element): Boolean {\n    return contains(element.getKey())\n}\n\nfun DownloadItemContext.contains(key: DownloadItemContext.Key<*>): Boolean {\n    return this[key] != null\n}\n\nfun DownloadItemContext.containsAll(elements: Collection<DownloadItemContext.Element>): Boolean {\n    for (el in elements) {\n        if (!contains(el)) {\n            return false\n        }\n    }\n    return true\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/DownloadJob.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.destination.DownloadDestination\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.suspendGuardedEntry\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.job\nimport kotlinx.coroutines.launch\n\nabstract class DownloadJob(\n    val downloadManager: DownloadManager,\n) {\n    protected val _isDownloadActive = MutableStateFlow(false)\n    val isDownloadActive = _isDownloadActive.asStateFlow()\n\n    abstract val downloadItem: IDownloadItem\n    val id get() = downloadItem.id\n    val scope = CoroutineScope(SupervisorJob())\n    var activeDownloadScope: CoroutineScope? = null\n    abstract fun getDestination(): DownloadDestination\n    private val booted = suspendGuardedEntry()\n\n    protected val _status = MutableStateFlow<DownloadJobStatus>(DownloadJobStatus.IDLE)\n    val status = _status.asStateFlow()\n\n    suspend fun boot() {\n        booted.action {\n            actualBoot()\n        }\n    }\n\n    abstract suspend fun actualBoot()\n    abstract fun initializeDestination()\n    abstract suspend fun reset()\n    abstract suspend fun resume()\n    abstract suspend fun pause(throwable: Throwable = CancellationException())\n    abstract suspend fun saveState()\n    protected fun ensureBooted() {\n        require(booted.isDone()) {\n            \"DownloadJob is not booted! Call boot() before using this object.\"\n        }\n    }\n\n    protected fun startAutoSaver() {\n        activeDownloadScope?.launch(Dispatchers.IO) {\n            while (true) {\n                saveState()\n                delay(1000)\n            }\n        }\n    }\n\n    protected fun onDownloadResuming() {\n        _status.update {\n            DownloadJobStatus.Resuming\n        }\n        downloadManager.onDownloadResuming(downloadItem)\n    }\n\n    protected fun onDownloadResumed() {\n        _status.update { DownloadJobStatus.Downloading }\n        downloadManager.onDownloadResumed(downloadItem)\n    }\n\n    protected suspend fun onDownloadCanceled(throwable: Throwable) {\n        _status.update { DownloadJobStatus.Canceled(throwable) }\n        if (ExceptionUtils.isNormalCancellation(throwable)) {\n            downloadItem.status = DownloadStatus.Paused\n        } else {\n            downloadItem.status = DownloadStatus.Error\n        }\n        _isDownloadActive.update { false }\n        saveState()\n        downloadManager.onDownloadCanceled(downloadItem, throwable)\n    }\n\n    protected fun onDownloadFinished() {\n        scope.launch {\n            try {\n                getDestination().onAllPartsCompleted {\n                    _status.value = DownloadJobStatus.PreparingFile(it)\n                }\n            } catch (e: Exception) {\n                pause(e)\n                return@launch\n            }\n            downloadItem.status = DownloadStatus.Completed\n            downloadItem.completeTime = System.currentTimeMillis()\n            _status.value = DownloadJobStatus.Finished\n            _isDownloadActive.update { false }\n            onDownloadFinishedBeforeSave()\n            saveState()\n            downloadManager.onDownloadFinished(downloadItem)\n        }\n    }\n\n    open fun onDownloadFinishedBeforeSave() {}\n    abstract fun getDownloadedSize(): Long\n\n    fun downloadRemoved(\n        removeOutputFile: Boolean = true,\n    ) {\n        ensureBooted()\n        getDestination().cleanUpJunkFiles()\n        if (removeOutputFile) {\n            getDestination().deleteOutputFile()\n        }\n    }\n\n    abstract fun reloadSettings()\n\n    fun newScopeBasedOn(scope: CoroutineScope): CoroutineScope {\n        return CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext.job))\n    }\n\n    fun close() {\n        scope.cancel()\n    }\n\n    abstract suspend fun changeConfig(\n        updater: (IDownloadItem) -> Unit,\n        extraConfig: DownloadJobExtraConfig?\n    ): IDownloadItem\n    abstract suspend fun extraConfigsReceived(config: DownloadJobExtraConfig)\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/DownloadJobExtraConfig.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\ninterface DownloadJobExtraConfig\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/DownloadJobStatus.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\nimport ir.amirab.downloader.utils.ExceptionUtils\n\nsealed class DownloadJobStatus(\n    val order: Int,\n    private val downloadStatus: DownloadStatus\n) {\n    fun asDownloadStatus() = downloadStatus\n\n    data object Downloading : DownloadJobStatus(0, DownloadStatus.Downloading),\n        IsActive\n\n    data class Retrying(val timeUntilRetry: Long) : DownloadJobStatus(0, DownloadStatus.Paused),\n        IsActive\n\n    data object Resuming : DownloadJobStatus(0, DownloadStatus.Downloading),\n        IsActive\n\n    data class PreparingFile(val percent: Int?) : DownloadJobStatus(1, DownloadStatus.Downloading),\n        IsActive\n\n    data class Canceled(val e: Throwable) : DownloadJobStatus(\n        2,\n        if (ExceptionUtils.isNormalCancellation(e)) DownloadStatus.Paused else DownloadStatus.Error\n    ),\n        CanBeResumed\n\n    data object IDLE : DownloadJobStatus(2, DownloadStatus.Added),\n        CanBeResumed\n\n    data object Finished : DownloadJobStatus(3, DownloadStatus.Completed)\n\n    sealed interface IsActive\n    sealed interface CanBeResumed\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/DownloadStatus.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\nenum class DownloadStatus {\n    Error,\n    Added,\n    Paused,\n    Downloading,\n    Completed,\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/IDownloadCredentials.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\nimport arrow.core.None\nimport arrow.core.Option\nimport arrow.core.none\n\n\ninterface IDownloadCredentials {\n    val link: String\n    val downloadPage: String?\n\n    fun validateCredentials()\n\n    fun copy(\n        link: Option<String> = None,\n        downloadPage: Option<String?> = None,\n    ): IDownloadCredentials\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/IDownloadItem.kt",
    "content": "package ir.amirab.downloader.downloaditem\n\nimport arrow.core.None\nimport arrow.core.Option\n\ninterface IDownloadItem : IDownloadCredentials {\n    var id: Long\n    var folder: String\n    var name: String\n    override var link: String\n    var contentLength: Long\n    override var downloadPage: String?\n    var dateAdded: Long\n    var startTime: Long?\n    var completeTime: Long?\n    var status: DownloadStatus\n    var preferredConnectionCount: Int?\n    var speedLimit: Long\n    var fileChecksum: String?\n\n    fun copy(\n        id: Option<Long> = None,\n        folder: Option<String> = None,\n        name: Option<String> = None,\n        link: Option<String> = None,\n        contentLength: Option<Long> = None,\n        downloadPage: Option<String?> = None,\n        dateAdded: Option<Long> = None,\n        startTime: Option<Long?> = None,\n        completeTime: Option<Long?> = None,\n        status: Option<DownloadStatus> = None,\n        preferredConnectionCount: Option<Int?> = None,\n        speedLimit: Option<Long> = None,\n        fileChecksum: Option<String?> = None,\n    ): IDownloadItem\n\n    fun validateItem()\n    fun withCredentials(credentials: IDownloadCredentials): IDownloadItem\n\n    companion object {\n        const val LENGTH_UNKNOWN = -1L\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/contexts/DefaultContexts.kt",
    "content": "package ir.amirab.downloader.downloaditem.contexts\n\nimport ir.amirab.downloader.downloaditem.DownloadItemContext\n\ninterface CanPerformRemove\ninterface CanPerformResume\ninterface CanPerformPause\n\nobject User:CanPerformPause,CanPerformResume,CanPerformRemove\nobject DuplicateRemoval:CanPerformRemove\ndata class Queue(val queue:Long):CanPerformPause,CanPerformResume,CanPerformRemove\n\ndata class StoppedBy(\n    val by: CanPerformPause\n):DownloadItemContext.Element{\n    companion object Key : DownloadItemContext.Key<StoppedBy>\n    override fun getKey(): DownloadItemContext.Key<*> = Key\n}\n\ndata class ResumedBy(\n    val by: CanPerformPause\n):DownloadItemContext.Element{\n    companion object Key : DownloadItemContext.Key<ResumedBy>\n    override fun getKey(): DownloadItemContext.Key<*> = Key\n}\n\ndata class RemovedBy(\n    val by: CanPerformRemove\n):DownloadItemContext.Element{\n    companion object Key : DownloadItemContext.Key<RemovedBy>\n    override fun getKey(): DownloadItemContext.Key<*> = Key\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSDownloadCredentials.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@SerialName(\"hls\")\ndata class HLSDownloadCredentials(\n    override val headers: Map<String, String>? = null,\n    override val username: String? = null,\n    override val password: String? = null,\n    override val userAgent: String? = null,\n    override val link: String,\n    override val downloadPage: String? = null\n) : IHLSCredentials {\n    override fun copy(\n        link: Option<String>,\n        downloadPage: Option<String?>\n    ): IDownloadCredentials {\n        return copy(\n            link = link.getOrElse { this.link },\n            downloadPage = downloadPage.getOrElse { this.downloadPage },\n        )\n    }\n\n    override fun validateCredentials() {\n        HttpUrlUtils.isValidUrl(link)\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSDownloadItem.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.http.IHttpBasedDownloadCredentials\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.serialization.Serializable\n@Serializable\ndata class HLSDownloadItem(\n    override var id: Long,\n    override var folder: String,\n    override var name: String,\n    override var link: String,\n    override var contentLength: Long = -1,\n    override var downloadPage: String? = null,\n    override var dateAdded: Long,\n    override var startTime: Long? = null,\n    override var completeTime: Long? = null,\n    override var status: DownloadStatus = DownloadStatus.Added,\n    override var preferredConnectionCount: Int? = null,\n    override var speedLimit: Long = 0,\n    override var fileChecksum: String? = null,\n    override var headers: Map<String, String>? = null,\n    override var username: String? = null,\n    override var password: String? = null,\n    override var userAgent: String? = null,\n    var duration: Double? = null,\n) : IDownloadItem, IHLSCredentials {\n    override fun copy(\n        id: Option<Long>,\n        folder: Option<String>,\n        name: Option<String>,\n        link: Option<String>,\n        contentLength: Option<Long>,\n        downloadPage: Option<String?>,\n        dateAdded: Option<Long>,\n        startTime: Option<Long?>,\n        completeTime: Option<Long?>,\n        status: Option<DownloadStatus>,\n        preferredConnectionCount: Option<Int?>,\n        speedLimit: Option<Long>,\n        fileChecksum: Option<String?>\n    ): IDownloadItem {\n        return copy(\n            id = id.getOrElse { this.id },\n            folder = folder.getOrElse { this.folder },\n            name = name.getOrElse { this.name },\n            link = link.getOrElse { this.link },\n            contentLength = contentLength.getOrElse { this.contentLength },\n            downloadPage = downloadPage.getOrElse { this.downloadPage },\n            dateAdded = dateAdded.getOrElse { this.dateAdded },\n            startTime = startTime.getOrElse { this.startTime },\n            completeTime = completeTime.getOrElse { this.completeTime },\n            status = status.getOrElse { this.status },\n            preferredConnectionCount = preferredConnectionCount.getOrElse { this.preferredConnectionCount },\n            speedLimit = speedLimit.getOrElse { this.speedLimit },\n            fileChecksum = fileChecksum.getOrElse { this.fileChecksum },\n        )\n    }\n\n    override fun copy(\n        link: Option<String>,\n        downloadPage: Option<String?>\n    ): HLSDownloadItem {\n        return copy(\n            link = link.getOrElse { this.link },\n            downloadPage = downloadPage.getOrElse { this.downloadPage },\n        )\n    }\n\n    override fun validateItem() {\n        validateCredentials()\n    }\n\n    override fun withCredentials(credentials: IDownloadCredentials): HLSDownloadItem {\n        return if (credentials is IHLSCredentials) {\n            withHlsCredentials(credentials)\n        } else {\n            this\n        }\n    }\n\n    override fun validateCredentials() {\n        HttpUrlUtils.isValidUrl(link)\n    }\n\n    companion object {\n        fun createWithCredentials(\n            credentials: IHLSCredentials,\n            id: Long,\n            folder: String,\n            name: String,\n            contentLength: Long = IDownloadItem.LENGTH_UNKNOWN,\n            dateAdded: Long = 0,\n            startTime: Long? = null,\n            completeTime: Long? = null,\n            status: DownloadStatus = DownloadStatus.Added,\n            preferredConnectionCount: Int? = null,\n            speedLimit: Long = 0,\n            fileChecksum: String? = null,\n            duration: Double? = null,\n        ): HLSDownloadItem {\n            return HLSDownloadItem(\n                link = credentials.link,\n                headers = credentials.headers,\n                username = credentials.username,\n                password = credentials.password,\n                downloadPage = credentials.downloadPage,\n                userAgent = credentials.userAgent,\n                id = id,\n                folder = folder,\n                name = name,\n                contentLength = contentLength,\n                dateAdded = dateAdded,\n                startTime = startTime,\n                completeTime = completeTime,\n                status = status,\n                preferredConnectionCount = preferredConnectionCount,\n                speedLimit = speedLimit,\n                fileChecksum = fileChecksum,\n                duration = duration,\n            )\n        }\n    }\n}\n\nprivate fun HLSDownloadItem.withHlsCredentials(credentials: IHttpBasedDownloadCredentials) = apply {\n    link = credentials.link\n    headers = credentials.headers\n    username = credentials.username\n    password = credentials.password\n    downloadPage = credentials.downloadPage\n    userAgent = credentials.userAgent\n}\n\nfun HLSDownloadItem.applyFrom(other: HLSDownloadItem) {\n    link = other.link\n    headers = other.headers\n    username = other.username\n    password = other.password\n    downloadPage = other.downloadPage\n    userAgent = other.userAgent\n\n    id = other.id\n    folder = other.folder\n    name = other.name\n\n    contentLength = other.contentLength\n\n    dateAdded = other.dateAdded\n    startTime = other.startTime\n    completeTime = other.completeTime\n    status = other.status\n    preferredConnectionCount = other.preferredConnectionCount\n    speedLimit = other.speedLimit\n\n    fileChecksum = other.fileChecksum\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSDownloadJob.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport io.lindstrom.m3u8.model.MediaPlaylist\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.destination.DownloadDestination\nimport ir.amirab.downloader.destination.SegmentedDownloadDestination\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.exception.DownloadValidationException\nimport ir.amirab.downloader.exception.TooManyErrorException\nimport ir.amirab.downloader.part.*\nimport ir.amirab.downloader.utils.*\nimport ir.amirab.util.tryLocked\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport okio.Throttler\nimport java.util.concurrent.ConcurrentHashMap\n\n\n/**\n * alive object that responsible for download a file\n */\n\nclass HLSDownloadJob(\n    override val downloadItem: HLSDownloadItem,\n    downloadManager: DownloadManager,\n    val client: HttpDownloaderClient,\n) : DownloadJob(\n    downloadManager = downloadManager,\n) {\n    val listDb by downloadManager::dlListDb\n    val partListDb by downloadManager::partListDb\n    private val parts: MutableList<MediaSegment> = mutableListOf()\n\n    private lateinit var destination: SegmentedDownloadDestination\n\n    override fun getDestination(): DownloadDestination {\n        return destination\n    }\n\n    var serverLastModified: Long? = null\n        private set\n\n    override suspend fun actualBoot() {\n        initializeDestination()\n        loadPartState()\n        applySpeedLimit()\n        downloadedSizeBeforeRetry = getDownloadedSize()\n    }\n\n    override fun initializeDestination() {\n        val outFile = downloadManager.calculateOutputFile(downloadItem)\n        destination = SegmentedDownloadDestination(\n            outputFile = outFile,\n            getAllParts = {\n                getParts()\n            },\n            tempDirectory = downloadManager.downloadDataFolder.resolve(id.toString()),\n            getFileName = {\n                it.getID().toString()\n            },\n            appendMode = false,\n        )\n    }\n\n    private fun setParts(list: List<MediaSegment>) {\n        this.parts.clear()\n        list.forEach {\n            if (it.isCompleted) {\n                it.statusFlow.update { PartDownloadStatus.Completed }\n            }\n        }\n        this.parts.addAll(list)\n    }\n\n    val itemSaveLock = Mutex()\n    val partLock = Mutex()\n    private suspend fun loadPartState() {\n        val mediaSegments = partLock.withLock {\n            partListDb.getParts(id)\n        } as? MediaSegments\n        setParts(mediaSegments?.list.orEmpty())\n    }\n\n\n    override suspend fun reset() {\n        pause()\n        clearPartDownloaderList()\n        setParts(emptyList())\n        downloadItem.contentLength = IDownloadItem.LENGTH_UNKNOWN\n        downloadItem.status = DownloadStatus.Added\n        downloadItem.startTime = null\n        downloadItem.completeTime = null\n        downloadItem.duration = null\n        downloadedSizeBeforeRetry = 0 // nothing\n        saveState()\n        downloadManager.onDownloadItemChange(downloadItem)\n    }\n\n\n    override suspend fun resume() {\n        if (isDownloadActive.value) {\n            return\n        }\n        _isDownloadActive.update { true }\n        resumeWithNewScope(\n            newActiveScope = createAndInitializeDownloadScope(),\n            isInFirstResume = true\n        )\n    }\n\n    fun createAndInitializeDownloadScope(): CoroutineScope {\n        val newActiveScope = newScopeBasedOn(scope)\n            .also {\n                activeDownloadScope = it\n            }\n        return newActiveScope\n    }\n\n    private suspend fun resumeWithNewScope(\n        newActiveScope: CoroutineScope,\n        isInFirstResume: Boolean,\n    ) {\n\n//        println(parts.filter { !it.isCompleted })\n        return newActiveScope.launch {\n            //boot download item from storage!\n            boot()\n            // if download item is booted and parts is not empty it means that we resumed that file in some point\n            // but we should check if all parts are already downloaded to finish the job before hitting the server unnecessarily!\n            if (parts.isNotEmpty() && parts.all { it.isCompleted }) {\n                onDownloadFinished()\n                return@launch\n            }\n            onDownloadResuming()\n            try {\n                fetchDownloadInfoAndValidate()\n                createPartDownloaderList()\n//                println(\"part downloaders created\")\n                beginDownloadParts()\n                startAutoSaver()\n                downloadItem.status = DownloadStatus.Downloading\n                if (downloadItem.startTime == null) {\n                    downloadItem.startTime = System.currentTimeMillis()\n                }\n                saveState()\n                onDownloadResumed()\n            } catch (e: Exception) {\n                e.printStackIfNOtUsual()\n                val shouldStop = when {\n                    ExceptionUtils.isNormalCancellation(e) -> true\n                    e is DownloadValidationException -> e.isCritical()\n                    else -> false\n                }\n                if (shouldStop) {\n                    // this function called from activeDownloadScope\n                    // so we change the scope here to prevent cancel this suspend function\n                    scope.launch {\n                        pause(e)\n                    }\n                } else {\n                    downloadFailedRetryOrPause(\n                        e = e,\n                        isInFirstResume = isInFirstResume,\n                    )\n                }\n            }\n        }.join()\n    }\n\n\n    override fun getDownloadedSize(): Long {\n        return getParts().sumOf {\n            it.howMuchProceed()\n        }\n    }\n\n\n    fun onPreferredConnectionCountChanged() {\n        activeDownloadScope?.launch {\n            beginDownloadParts()\n        }\n    }\n\n    override suspend fun changeConfig(\n        updater: (IDownloadItem) -> Unit,\n        extraConfig: DownloadJobExtraConfig?\n    ): IDownloadItem {\n        boot()\n        val previousItem = downloadItem.copy()\n        val newItem = previousItem.copy().apply(updater)\n        val previousDestination = downloadManager.calculateOutputFile(previousItem)\n        val newDestination = downloadManager.calculateOutputFile(newItem)\n        val shouldUpdateDestination = previousDestination != newDestination\n        if (shouldUpdateDestination) {\n            if (isDownloadActive.value) {\n                pause()\n            }\n            destination.moveOutput(newDestination)\n        }\n        // if there is no error update the actual download item\n        downloadItem.applyFrom(newItem)\n        if (shouldUpdateDestination) {\n            // destination should be closed for now!\n            initializeDestination()\n        }\n        if (previousItem.preferredConnectionCount != downloadItem.preferredConnectionCount) {\n            onPreferredConnectionCountChanged()\n        }\n        if (previousItem.link != downloadItem.link) {\n            onLinkChanged()\n        }\n        applySpeedLimit()\n        extraConfig?.let {\n            extraConfigsReceived(extraConfig)\n        }\n        saveDownloadItem()\n        return downloadItem\n    }\n\n    private fun applySpeedLimit() {\n        jobThrottler.bytesPerSecond(bytesPerSecond = downloadItem.speedLimit)\n    }\n\n    fun onLinkChanged() {\n        scope.launch {\n            if (activeDownloadScope?.isActive == true) {\n                pause()\n                resume()\n            }\n\n        }\n    }\n\n    fun getRequestedThreadCount(): Int {\n        return downloadItem.preferredConnectionCount\n            ?: downloadManager.settings.defaultThreadCount\n    }\n\n\n    private val partLoopLock = Mutex()\n\n    //    private val c = AtomicInteger(0)\n    private fun beginDownloadParts() {\n        if (partLoopLock.isLocked) {\n            return\n        }\n        activeDownloadScope?.launch {\n            if (!partLoopLock.tryLock()) {\n                return@launch\n            }\n//            c.incrementAndGet()\n            try {\n                val activeCount = getPartDownloaderList()\n                    .count { it.active }\n                val howMuchCreate = getRequestedThreadCount() - activeCount\n                if (howMuchCreate > 0) {\n                    val mutableInactivePartDownloaderList = getPartDownloaderList()\n                        .filter { !it.active && !it.part.isCompleted }\n                        .sortedBy { it.part.getID() }\n                        .toMutableList()\n//                    println(mutableInactivePartDownloaderList)\n\n                    fun getPartDownloader(): HLSPartDownloader? {\n                        val inactivePart =\n                            runCatching { mutableInactivePartDownloaderList.removeAt(0) }.getOrNull()\n                        if (inactivePart != null) return inactivePart\n                        return null\n                    }\n                    for (i in 1..howMuchCreate) {\n                        val partDownloader = getPartDownloader()\n                        if (partDownloader == null) {\n//                            println(\"part downloader is null\")\n                            break\n                        }\n                        if (partDownloader.part.isCompleted) {\n//                            println(\"it seems part is downloaded!\")\n                            continue\n                        }\n//                        println(\"got new part downloader ${partDownloader.part}\")\n                        partDownloader.start()\n                    }\n                }\n                if (howMuchCreate < 0) {\n                    // as we restart the parts each time we don't pause the active ones\n//                    partDownloaderList.values\n//                        .toList()\n//                        .filter { it.active }\n//                        .sortedByDescending { it.part.getID() }\n//                        .take(-howMuchCreate)\n//                        .onEach {\n//                            it.stop()\n//                        }.onEach {\n//                            it.join()\n//                            it.awaitIdle()\n//                        }\n                }\n            } catch (e: Exception) {\n                throw e\n            } finally {\n//                println(\"C:\" + c)\n                partLoopLock.unlock()\n//                c.decrementAndGet()\n            }\n        }\n    }\n\n    private fun onPartHaveToManyError(throwable: Throwable) {\n        var paused = false\n        if (throwable is DownloadValidationException) {\n            if (throwable.isCritical()) {\n                //stop the whole job! as we have big problem here\n                paused = true\n                scope.launch {\n                    pause(throwable)\n                }\n            }\n        }\n        val allHaveError = partDownloaderList.values\n            .filter { it.active }\n            .all {\n                it.injured()\n            }\n        if (allHaveError && !paused) {\n//            println(\"all have error!\")\n            downloadFailedRetryOrPause(\n                e = throwable,\n                isInFirstResume = false,\n            )\n        }\n    }\n\n    // for this download job only, it has higher priority than download manager settings\n    var _maxAllowedRetries: Int? = null\n    fun getMaxAllowedRetries(): Int {\n        return _maxAllowedRetries ?: downloadManager.settings.maxDownloadRetryCount\n    }\n\n    var failedDownloadTries = 0\n    val delayForEachRetry = 3_000L\n    private var downloadedSizeBeforeRetry = 0L\n\n    private var retryJob: Job? = null\n\n    private val retryLock = Mutex()\n\n    // I have to improve this function to not allow accessing it concurrently\n    private fun downloadFailedRetryOrPause(\n        e: Throwable,\n        isInFirstResume: Boolean,\n    ) {\n        //moving to the main scope and request to cancel activeDownload scope!\n        scope.launch {\n            if (isInFirstResume && failedDownloadTries == 0 && shouldRetryIfInitialFailed()) {\n                if (ExceptionUtils.isNetworkError(e) || ExceptionUtils.isResponseError(e)) {\n                    pause(e)\n                    return@launch\n                }\n            }\n            // can't proceed\n            if (e is DownloadValidationException && e.isCritical()) {\n                pause(e)\n                return@launch\n            }\n            val downloadedSize = getDownloadedSize()\n            if (downloadedSize > downloadedSizeBeforeRetry) {\n                // download had progress! so we reset it\n                failedDownloadTries = 0\n            } else {\n                failedDownloadTries++\n            }\n            downloadedSizeBeforeRetry = downloadedSize\n\n            // we always have one try (the initial resume action), after that others are retries!\n            val retriedCount = (failedDownloadTries - 1).coerceAtLeast(0)\n            if (retriedCount < getMaxAllowedRetries()) {\n                retry(isInFirstResume)\n            } else {\n                pause(TooManyErrorException(e))\n            }\n        }\n    }\n\n    fun retry(isInFirstResume: Boolean) {\n        scope.launch {\n            val newScopeResult = retryLock.tryLocked {\n                val job = async {\n                    saveState()\n                    cancelDownloadScope()\n                    stopAllParts()\n                    _status.update { DownloadJobStatus.Retrying(delayForEachRetry) }\n                    delay(delayForEachRetry)\n                    createAndInitializeDownloadScope()\n                }\n                retryJob = job\n                job.await()\n            }\n            newScopeResult.getOrNull()?.let {\n                resumeWithNewScope(it, isInFirstResume)\n            }\n        }\n    }\n\n    fun shouldRetryIfInitialFailed(): Boolean {\n        return true\n    }\n\n\n    private fun onPartStatusChanged(\n        partDownloader: HLSPartDownloader,\n        partStatus: PartDownloadStatus,\n    ) {\n        when (partStatus) {\n            is PartDownloadStatus.Canceled -> {\n                destination.onPartCancelled(partDownloader.part)\n            }\n\n            PartDownloadStatus.Completed -> {\n                destination.onPartCancelled(partDownloader.part)\n                if (getParts().all { it.isCompleted }) {\n                    onDownloadFinished()\n                } else {\n                    scope.launch {\n                        beginDownloadParts()\n                    }\n                }\n            }\n\n            PartDownloadStatus.ReceivingData -> {}\n            PartDownloadStatus.Connecting -> {}\n            PartDownloadStatus.IDLE -> {}\n        }\n    }\n\n\n    @Synchronized\n    private fun createPartDownloaderList() {\n        synchronized(partDownloaderList) {\n            //        thisLogger().info(\"create part downloaders\")\n            parts.forEach {\n                getOrCreatePartDownloader(it)\n            }\n//            println(\"created N parts = \" + partDownloaderList.values.size)\n        }\n    }\n\n    private fun clearPartDownloaderList() {\n//        thisLogger().info(\"create part downloaders\")\n        parts.forEach {\n            destroyPartDownloader(it)\n        }\n    }\n\n    private val jobThrottler = Throttler()\n\n    private val partDownloaderList = ConcurrentHashMap<Long, HLSPartDownloader>()\n    private val listenerJobs: MutableMap<Long, Job> = ConcurrentHashMap<Long, Job>()\n    private fun getPartDownloaderList(): List<HLSPartDownloader> {\n        synchronized(partDownloaderList) {\n            return partDownloaderList.map { it.value }\n        }\n    }\n\n    private fun getOrCreatePartDownloader(part: MediaSegment): HLSPartDownloader {\n        synchronized(partDownloaderList) {\n            return partDownloaderList.getOrPut(part.getID()) {\n                HLSPartDownloader(\n                    baseURL = downloadItem.link,\n                    part = part,\n                    getDestWriter = {\n                        destination.getWriterFor(part)\n                    },\n                    client = client,\n                    speedLimiters = listOf(\n                        downloadManager.throttler,\n                        jobThrottler,\n                    ),\n                ).also { partDownloader: HLSPartDownloader ->\n                    partDownloader.onTooManyErrors = {\n                        onPartHaveToManyError(it)\n                    }\n                    //we should close that scope after we don't need it anymore!\n                    listenerJobs[part.getID()] = partDownloader.statusFlow.onEach { status ->\n                        //TODO probably bug here\n                        onPartStatusChanged(partDownloader, status)\n                    }.launchIn(scope)\n                }\n            }\n        }\n    }\n\n    private fun destroyPartDownloader(part: MediaSegment) {\n        listenerJobs.remove(part.getID())?.cancel()\n        partDownloaderList.remove(part.getID())\n    }\n\n    private suspend fun fetchDownloadInfoAndValidate(\n    ) {\n        if (parts.isEmpty()) {\n            initialParts()\n        }\n        // at this point we have all the parts we need either by hitting remote or they are already in destination\n        // before proceed we should check the downloaded parts and remove the \"isCompleted\" flag if they are missing\n        parts\n            .filter { it.isCompleted }\n            .forEach {\n                // redownload invalid parts\n                if (!destination.isDownloadPartValid(it)) {\n                    it.isCompleted = false\n                }\n            }\n\n        saveState()\n    }\n\n    // new segments received\n    // we should compare those and update them\n    // we reset the whole if we found different duration\n    fun updateParts(\n        playlist: MediaPlaylist\n    ) {\n        val newParts = playlist.mediaSegments()\n            .mapIndexed { index, segment ->\n                MediaSegment(\n                    segmentIndex = playlist.mediaSequence() + index,\n                    link = segment.uri(),\n                    duration = segment.duration()\n                )\n            }\n        val currentParts = parts\n        downloadItem.duration = newParts.sumOf { it.duration }\n        if (currentParts.isEmpty()) {\n            setParts(newParts)\n            return\n        }\n        val oldPartsMap = currentParts.associateBy {\n            it.segmentIndex\n        }\n        val newPartsToSave = ArrayList<MediaSegment>(newParts.size)\n        for (newPart in newParts) {\n            val oldPart = oldPartsMap[newPart.segmentIndex]\n            var newPartToAdd = newPart\n            if (oldPart != null) {\n                if (oldPart.duration == newPart.duration) {\n                    newPartToAdd = newPart.copy(\n                        isCompleted = oldPart.isCompleted\n                    )\n                } else {\n                    // inconsistencies found! reset the parts to the new one\n                    setParts(newParts)\n                    return\n                }\n            }\n            newPartsToSave.add(newPartToAdd)\n        }\n    }\n\n    private suspend fun initialParts(): HLSResponseInfo {\n        val response = client.connect(\n            downloadItem, null, null\n        ).use {\n            HLSResponseInfo.fromConnection(it)\n        }\n        updateParts(\n            response.hlsManifest\n        )\n        return response\n    }\n\n    suspend fun cancelDownloadScope() {\n        activeDownloadScope?.coroutineContext?.job?.cancelAndJoin()\n        activeDownloadScope = null\n    }\n\n    suspend fun cancelRetry() {\n        retryJob?.cancel()\n        retryJob = null\n    }\n\n    suspend fun stopAllParts() {\n        withContext(Dispatchers.IO) {\n            partDownloaderList.values.onEach {\n                it.stop()\n            }.onEach {\n                it.join()\n                it.awaitIdle()\n            }\n        }\n    }\n\n    override suspend fun pause(throwable: Throwable) {\n        boot()\n        failedDownloadTries = 0\n        cancelRetry()\n        cancelDownloadScope()\n        stopAllParts()\n        onDownloadCanceled(throwable)\n    }\n\n    override fun onDownloadFinishedBeforeSave() {\n        downloadItem.contentLength = destination.outputFile.length()\n    }\n\n    private var lastSavedDownloadItem: HLSDownloadItem? = null\n    private var lastSavedParts: List<MediaSegment>? = null\n\n    private suspend fun saveDownloadItem() {\n        itemSaveLock.withLock {\n            val copy = downloadItem.copy()\n            if (lastSavedDownloadItem != downloadItem) {\n                listDb.update(downloadItem)\n                lastSavedDownloadItem = copy\n            }\n        }\n    }\n\n    private suspend fun saveParts() {\n        partLock.withLock {\n            val copy = getParts().map { it.copy() }\n            if (lastSavedParts != copy) {\n                destination.flush()\n                partListDb.setParts(id, MediaSegments(copy))\n                lastSavedParts = copy\n            }\n        }\n    }\n\n    override suspend fun saveState() {\n        saveDownloadItem()\n        saveParts()\n    }\n\n    fun getParts(): List<MediaSegment> {\n        //Make a copy because of CMException\n        return parts.toList()\n    }\n\n    override fun reloadSettings() {\n        onPreferredConnectionCountChanged()\n    }\n\n    override suspend fun extraConfigsReceived(config: DownloadJobExtraConfig) {\n        if (config !is HLSDownloadJobExtraConfig) return\n        config.hlsManifest?.let {\n            updateParts(it)\n            saveParts()\n        }\n    }\n}\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSDownloadJobExtraConfig.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport io.lindstrom.m3u8.model.MediaPlaylist\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\n\ndata class HLSDownloadJobExtraConfig(\n    val hlsManifest: MediaPlaylist? = null\n) : DownloadJobExtraConfig\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSDownloader.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.Downloader\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.serialization.KSerializer\nimport kotlin.reflect.KClass\n\n\nclass HLSDownloader(\n    client: Lazy<HttpDownloaderClient>\n) : Downloader<HLSDownloadItem, HLSDownloadJob, HLSDownloadCredentials> {\n\n    val client: HttpDownloaderClient by client\n\n    override fun createJob(\n        item: HLSDownloadItem,\n        downloadManager: DownloadManager\n    ): HLSDownloadJob {\n        return HLSDownloadJob(\n            downloadItem = item,\n            downloadManager = downloadManager,\n            client = client,\n        )\n    }\n\n    override fun accept(item: IDownloadItem): Boolean {\n        return item is HLSDownloadItem\n    }\n\n    override val downloadItemClass: KClass<HLSDownloadItem> = HLSDownloadItem::class\n    override val downloadCredentialsClass: KClass<HLSDownloadCredentials> = HLSDownloadCredentials::class\n    override val downloadJobClass: KClass<HLSDownloadJob> = HLSDownloadJob::class\n    override val downloadItemSerializer: KSerializer<HLSDownloadItem> = HLSDownloadItem.serializer()\n    override val downloadCredentialsSerializer: KSerializer<HLSDownloadCredentials> =\n        HLSDownloadCredentials.serializer()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSPartDownloader.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport ir.amirab.downloader.connection.Connection\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.connection.response.expectSuccess\nimport ir.amirab.downloader.destination.DestWriter\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.part.MediaSegment\nimport ir.amirab.downloader.part.PartDownloader\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.isActive\nimport okio.Throttler\nimport kotlin.coroutines.cancellation.CancellationException\n\nclass HLSPartDownloader(\n    part: MediaSegment,\n    getDestWriter: () -> DestWriter,\n    private val baseURL: String,\n    private val client: HttpDownloaderClient,\n    private val speedLimiters: List<Throttler>,\n) : PartDownloader<\n        MediaSegment\n        >(\n    part = part,\n    getDestWriter = getDestWriter,\n) {\n    override fun howMuchCanRead(maxAllowed: Long): Long {\n        return maxAllowed\n    }\n\n    override suspend fun connectAndVerify(): Connection<HttpResponseInfo> {\n        val fullLink = part.link\n            .takeIf { HttpUrlUtils.isValidUrl(it) }\n            ?.let(HttpUrlUtils::createURL)\n            ?: HttpUrlUtils.createURL(baseURL).resolve(part.link)\n        requireNotNull(fullLink) {\n            \"link is incorrect! ${part.link}\"\n        }\n        val connect = client.connect(\n            HttpDownloadCredentials(fullLink.toString()),\n            null, null,\n        )\n        if (stop || !currentCoroutineContext().isActive) {\n            connect.close()\n            throw CancellationException()\n        }\n        if (!connect.responseInfo.isSuccessFul) {\n            connect.close()\n            throw CancellationException()\n        }\n        part.length = connect.responseInfo.contentLength\n        return connect.let {\n            it.copy(\n                source = speedLimiters.fold(it.source) { acc, thr ->\n                    thr.source(acc)\n                }\n            )\n        }\n\n    }\n\n    override fun onCanceled(e: Throwable) {\n        if (!part.isCompleted) {\n            // we should restart failed parts\n            part.resetCurrent()\n        }\n        super.onCanceled(e)\n    }\n\n    override fun onFinish() {\n        part.isCompleted = true\n        super.onFinish()\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/HLSResponseInfo.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport io.lindstrom.m3u8.model.KeyMethod\nimport io.lindstrom.m3u8.model.MediaPlaylist\nimport io.lindstrom.m3u8.parser.MediaPlaylistParser\nimport io.lindstrom.m3u8.parser.ParsingMode\nimport ir.amirab.downloader.connection.Connection\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.connection.response.expectSuccess\nimport ir.amirab.downloader.utils.FileNameUtil\nimport ir.amirab.util.HttpUrlUtils\nimport okio.buffer\nimport java.io.IOException\n\ndata class HLSResponseInfo(\n    val httpResponseInfo: HttpResponseInfo,\n    val hlsManifest: MediaPlaylist\n) : IResponseInfo {\n    val name: String?\n        get() = httpResponseInfo.fileName\n    val duration = hlsManifest.mediaSegments()\n        .sumOf { it.duration() }\n        .takeIf { it > 0 }\n\n    override val isSuccessFul: Boolean\n        get() = httpResponseInfo.isSuccessFul\n    override val requiresAuth: Boolean\n        get() = httpResponseInfo.requireBasicAuth\n    override val requireBasicAuth: Boolean\n        get() = httpResponseInfo.requireBasicAuth\n    override val isWebPage: Boolean\n        get() = httpResponseInfo.isWebPage\n    override val resumeSupport: Boolean\n        get() = true\n\n    companion object {\n        fun fromConnection(connection: Connection<HttpResponseInfo>): HLSResponseInfo {\n            expectSuccess(connection)\n            val data = connection.source.buffer().use { it.readUtf8() }\n            val playlist = try {\n                parseHLSAsMediaPlaylist(data)\n            } catch (e: Exception) {\n                throw BadHLSResponseException(\"can't parse HLS playlist\", e)\n            }\n            val mediaSegments = playlist.mediaSegments()\n            if (mediaSegments.isEmpty()) {\n                throw UnsupportedOperationException(\n                    \"playlist has no segments\"\n                )\n            }\n            val firstSegmentExtension = HttpUrlUtils\n                .createURL(connection.responseInfo.requestUrl)\n                .resolve(mediaSegments[0].uri())?.toString()\n                ?.let(HttpUrlUtils::extractNameFromLink)\n                ?.let(FileNameUtil::getExtensionOrNull)\n                ?.lowercase()\n            if (firstSegmentExtension != \"ts\") {\n                throw UnsupportedOperationException(\n                    \"Only HLS .ts segments supported at the moment, but '$firstSegmentExtension' provided\"\n                )\n            }\n            if (isMediaPlayListEncrypted(playlist)) {\n                throw UnsupportedOperationException(\n                    \"Encrypted HLS playlists are not supported\"\n                )\n            }\n            return HLSResponseInfo(\n                connection.responseInfo,\n                playlist,\n            )\n        }\n\n        private fun parseHLSAsMediaPlaylist(data: String): MediaPlaylist {\n            val playlistParser = MediaPlaylistParser(ParsingMode.LENIENT)\n            return playlistParser.readPlaylist(data)\n        }\n\n        private val HALS_POSSIBLE_HEADERS = listOf(\n            \"application/x-mpegurl\",\n            \"application/vnd.apple.mpegurl\",\n        )\n\n        /**\n         * if no hls content-size received we check this\n         */\n        private const val MAXIMUM_ALLOWED_SIZE = 2 * 1024 * 1024 // 2MiB\n        private fun expectSuccess(connection: Connection<HttpResponseInfo>) {\n            connection.responseInfo.expectSuccess()\n            val hlsPossibleHeaders = HALS_POSSIBLE_HEADERS\n            val contentType = connection.responseInfo.responseHeaders[\"content-type\"]\n            var error: String? = null\n            if (contentType == null) {\n                error = \"no content type is provided\"\n            } else {\n                val isHLSContentType = hlsPossibleHeaders.any { it.startsWith(contentType, ignoreCase = true) }\n                if (!isHLSContentType) {\n                    error = \"content type is not hls compatible: $contentType\"\n                }\n            }\n            if (error != null) {\n                val contentLength = connection.responseInfo.contentLength\n                if (contentLength == null) {\n                    error += \", and content length is unknown\"\n                } else {\n                    if (contentLength > MAXIMUM_ALLOWED_SIZE) {\n                        error += \", and returned content length is too big for hls playlist\"\n                    } else {\n                        error = null\n                    }\n                }\n            }\n            if (error != null) {\n                throw BadHLSResponseException(error)\n            }\n        }\n\n        private fun isMediaPlayListEncrypted(playlist: MediaPlaylist): Boolean {\n            return playlist.mediaSegments().any {\n                it.segmentKeys().any { key ->\n                    key.method() != KeyMethod.NONE\n                }\n            }\n        }\n    }\n}\n\nclass BadHLSResponseException(\n    message: String, cause: Throwable? = null\n) : IOException(message, cause)\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/hls/IHLSCredentials.kt",
    "content": "package ir.amirab.downloader.downloaditem.hls\n\nimport ir.amirab.downloader.downloaditem.http.IHttpBasedDownloadCredentials\n\ninterface IHLSCredentials : IHttpBasedDownloadCredentials\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/http/HttpDownloadCredentials.kt",
    "content": "package ir.amirab.downloader.downloaditem.http\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@SerialName(\"http\")\ndata class HttpDownloadCredentials(\n    override val link: String,\n    override val headers: Map<String, String>? = null,\n    override val username: String? = null,\n    override val password: String? = null,\n    override val downloadPage: String? = null,\n    override val userAgent: String? = null,\n) : IHttpDownloadCredentials {\n    override fun validateCredentials() {\n        validate(this)\n    }\n\n    override fun copy(\n        link: Option<String>,\n        downloadPage: Option<String?>\n    ): IDownloadCredentials {\n        return copy(\n            link = link.getOrElse { this.link },\n            downloadPage = downloadPage.getOrElse { this.downloadPage }\n        )\n    }\n\n    companion object {\n        fun empty() = HttpDownloadCredentials(\n            link = \"\"\n        )\n\n        fun from(credentials: IHttpDownloadCredentials): HttpDownloadCredentials {\n            credentials.run {\n                return when (this) {\n                    is HttpDownloadCredentials -> this\n                    else -> HttpDownloadCredentials(\n                        link = link,\n                        headers = headers,\n                        username = username,\n                        password = password,\n                        downloadPage = downloadPage,\n                        userAgent = userAgent,\n                    )\n                }\n            }\n        }\n\n        fun validate(credentials: IHttpDownloadCredentials) {\n            //make sure url is valid\n            require(HttpUrlUtils.isValidUrl(credentials.link)) {\n                \"url is not valid\"\n            }\n        }\n    }\n}\n\ninterface IHttpDownloadCredentials : IHttpBasedDownloadCredentials\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/http/HttpDownloadItem.kt",
    "content": "package ir.amirab.downloader.downloaditem.http\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.IDownloadItem.Companion.LENGTH_UNKNOWN\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@SerialName(\"http\")\ndata class HttpDownloadItem(\n    override var link: String,\n    override var headers: Map<String, String>? = null,\n    override var username: String? = null,\n    override var password: String? = null,\n    override var downloadPage: String? = null,\n    override var userAgent: String? = null,\n\n    var serverETag: String? = null,\n\n//    IDownloadItem\n    override var id: Long,\n    override var folder: String,\n    override var name: String,\n    override var contentLength: Long = LENGTH_UNKNOWN,\n    override var dateAdded: Long = 0,\n    override var startTime: Long? = null,\n    override var completeTime: Long? = null,\n    override var status: DownloadStatus = DownloadStatus.Added,\n    override var preferredConnectionCount: Int? = null,\n    override var speedLimit: Long = 0,//0 is unlimited\n    override var fileChecksum: String? = null,\n) : IDownloadItem, IHttpDownloadCredentials {\n    override fun copy(\n        id: Option<Long>,\n        folder: Option<String>,\n        name: Option<String>,\n        link: Option<String>,\n        contentLength: Option<Long>,\n        downloadPage: Option<String?>,\n        dateAdded: Option<Long>,\n        startTime: Option<Long?>,\n        completeTime: Option<Long?>,\n        status: Option<DownloadStatus>,\n        preferredConnectionCount: Option<Int?>,\n        speedLimit: Option<Long>,\n        fileChecksum: Option<String?>\n    ): HttpDownloadItem {\n        val id = id.getOrElse { this.id }\n        val folder = folder.getOrElse { this.folder }\n        val name = name.getOrElse { this.name }\n        val link = link.getOrElse { this.link }\n        val contentLength = contentLength.getOrElse { this.contentLength }\n        val downloadPage = downloadPage.getOrElse { this.downloadPage }\n        val dateAdded = dateAdded.getOrElse { this.dateAdded }\n        val startTime = startTime.getOrElse { this.startTime }\n        val completeTime = completeTime.getOrElse { this.completeTime }\n        val status = status.getOrElse { this.status }\n        val preferredConnectionCount = preferredConnectionCount.getOrElse { this.preferredConnectionCount }\n        val speedLimit = speedLimit.getOrElse { this.speedLimit }\n        val fileChecksum = fileChecksum.getOrElse { this.fileChecksum }\n        return copy(\n            id = id,\n            folder = folder,\n            name = name,\n            link = link,\n            contentLength = contentLength,\n            downloadPage = downloadPage,\n            dateAdded = dateAdded,\n            startTime = startTime,\n            completeTime = completeTime,\n            status = status,\n            preferredConnectionCount = preferredConnectionCount,\n            speedLimit = speedLimit,\n            fileChecksum = fileChecksum,\n        )\n    }\n\n    override fun copy(\n        link: Option<String>,\n        downloadPage: Option<String?>\n    ): HttpDownloadItem {\n        val link = link.getOrElse { this.link }\n        val downloadPage = downloadPage.getOrElse { this.downloadPage }\n        return copy(\n            link = link,\n            downloadPage = downloadPage,\n        )\n    }\n\n    override fun validateCredentials() {\n        //make sure url is valid\n        HttpDownloadCredentials.validate(this)\n    }\n\n    override fun validateItem() {\n        validateCredentials()\n    }\n\n    override fun withCredentials(credentials: IDownloadCredentials): HttpDownloadItem {\n        return if (credentials is HttpDownloadCredentials) {\n            withHttpCredentials(credentials)\n        } else {\n            this\n        }\n    }\n\n    companion object {\n        fun createWithCredentials(\n            credentials: HttpDownloadCredentials,\n            id: Long,\n            folder: String,\n            name: String,\n            contentLength: Long = IDownloadItem.LENGTH_UNKNOWN,\n            serverETag: String? = null,\n            dateAdded: Long = 0,\n            startTime: Long? = null,\n            completeTime: Long? = null,\n            status: DownloadStatus = DownloadStatus.Added,\n            preferredConnectionCount: Int? = null,\n            speedLimit: Long = 0,\n            fileChecksum: String? = null,\n        ): HttpDownloadItem {\n            return HttpDownloadItem(\n                link = credentials.link,\n                headers = credentials.headers,\n                username = credentials.username,\n                password = credentials.password,\n                downloadPage = credentials.downloadPage,\n                userAgent = credentials.userAgent,\n                id = id,\n                folder = folder,\n                name = name,\n                contentLength = contentLength,\n                serverETag = serverETag,\n                dateAdded = dateAdded,\n                startTime = startTime,\n                completeTime = completeTime,\n                status = status,\n                preferredConnectionCount = preferredConnectionCount,\n                speedLimit = speedLimit,\n                fileChecksum = fileChecksum,\n            )\n        }\n    }\n}\n\n\nfun HttpDownloadItem.applyFrom(other: HttpDownloadItem) {\n    link = other.link\n    headers = other.headers\n    username = other.username\n    password = other.password\n    downloadPage = other.downloadPage\n    userAgent = other.userAgent\n\n    id = other.id\n    folder = other.folder\n    name = other.name\n\n    contentLength = other.contentLength\n    serverETag = other.serverETag\n\n    dateAdded = other.dateAdded\n    startTime = other.startTime\n    completeTime = other.completeTime\n    status = other.status\n    preferredConnectionCount = other.preferredConnectionCount\n    speedLimit = other.speedLimit\n\n    fileChecksum = other.fileChecksum\n}\n\nfun HttpDownloadItem.withHttpCredentials(credentials: IHttpDownloadCredentials) = apply {\n    link = credentials.link\n    headers = credentials.headers\n    username = credentials.username\n    password = credentials.password\n    downloadPage = credentials.downloadPage\n    userAgent = credentials.userAgent\n}\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/http/HttpDownloadJob.kt",
    "content": "package ir.amirab.downloader.downloaditem.http\n\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.response.expectSuccess\nimport ir.amirab.downloader.destination.DownloadDestination\nimport ir.amirab.downloader.destination.SimpleDownloadDestination\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.exception.DownloadValidationException\nimport ir.amirab.downloader.exception.PrepareDestinationFailedException\nimport ir.amirab.downloader.exception.FileChangedException\nimport ir.amirab.downloader.exception.ServerResumeSupportChangeException\nimport ir.amirab.downloader.exception.TooManyErrorException\nimport ir.amirab.downloader.part.*\nimport ir.amirab.downloader.utils.*\nimport ir.amirab.util.tryLocked\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport okio.Throttler\nimport java.util.concurrent.ConcurrentHashMap\n\n\n/**\n * alive object that responsible for download a file\n */\n\nclass HttpDownloadJob(\n    override val downloadItem: HttpDownloadItem,\n    downloadManager: DownloadManager,\n    val client: HttpDownloaderClient,\n) : DownloadJob(\n    downloadManager = downloadManager,\n) {\n    val listDb by downloadManager::dlListDb\n    val partListDb by downloadManager::partListDb\n    private val parts: MutableList<RangedPart> = mutableListOf()\n    private lateinit var destination: SimpleDownloadDestination\n    override fun getDestination(): DownloadDestination {\n        return destination\n    }\n\n    var supportsConcurrent: Boolean? = null\n        private set\n\n    var serverLastModified: Long? = null\n        private set\n\n    override suspend fun actualBoot() {\n        initializeDestination()\n        loadPartState()\n        supportsConcurrent = when (getParts().size) {\n            in 2..Int.MAX_VALUE -> true\n            else -> null\n        }\n        applySpeedLimit()\n        downloadedSizeBeforeRetry = getDownloadedSize()\n    }\n\n    override fun initializeDestination() {\n        val outFile = downloadManager.calculateOutputFile(downloadItem)\n        destination = SimpleDownloadDestination(\n            file = outFile,\n            emptyFileCreator = downloadManager.emptyFileCreator,\n            appendExtensionToIncompleteDownloads = downloadManager.settings.appendExtensionToIncompleteDownloads,\n            downloadId = id\n        )\n    }\n\n    private fun setParts(list: List<RangedPart>) {\n        this.parts.clear()\n        list.forEach {\n            if (it.isCompleted) {\n                it.statusFlow.update { PartDownloadStatus.Completed }\n            }\n        }\n        this.parts.addAll(list)\n    }\n\n    val itemSaveLock = Mutex()\n    val partLock = Mutex()\n    private suspend fun loadPartState() {\n        val rangedParts = partLock.withLock {\n            partListDb.getParts(id)\n        } as? RangedParts\n        setParts(rangedParts?.list.orEmpty())\n    }\n\n\n    // if strict mode is false part downloader going to download data without any validation of content length\n    // this is only acceptable when resume is not supported and multiple get requests results multiple result\n    @Volatile\n    private var strictDownload = true\n\n    fun expectValid(size: Long, parts: List<LongRange>) {\n        val parts = parts.sortedBy { it.first }\n        require(parts.first().first == 0L)\n        require(parts.last().last == size - 1)\n        for (i in 1..<parts.size) {\n            val a = parts[i - 1]\n            val b = parts[i]\n            require(a.last + 1 == b.first)\n        }\n    }\n\n    override suspend fun reset() {\n        pause()\n        clearPartDownloaderList()\n        setParts(emptyList())\n        downloadItem.contentLength = IDownloadItem.LENGTH_UNKNOWN\n        downloadItem.serverETag = null\n        downloadItem.status = DownloadStatus.Added\n        downloadItem.startTime = null\n        downloadItem.completeTime = null\n        strictDownload = true\n        downloadedSizeBeforeRetry = 0 // nothing\n        saveState()\n        downloadManager.onDownloadItemChange(downloadItem)\n    }\n\n\n    override suspend fun resume() {\n        if (isDownloadActive.value) {\n            return\n        }\n        _isDownloadActive.update { true }\n        resumeWithNewScope(\n            newActiveScope = createAndInitializeDownloadScope(),\n            isInFirstResume = true\n        )\n    }\n\n    fun createAndInitializeDownloadScope(): CoroutineScope {\n        val newActiveScope = newScopeBasedOn(scope)\n            .also {\n                activeDownloadScope = it\n            }\n        return newActiveScope\n    }\n\n    private suspend fun resumeWithNewScope(\n        newActiveScope: CoroutineScope,\n        isInFirstResume: Boolean,\n    ) {\n\n//        println(parts.filter { !it.isCompleted })\n        return newActiveScope.launch {\n            //boot download item from storage!\n            boot()\n            // if download item is booted and parts is not empty it means that we resumed that file in some point\n            // but we should check if all parts are already downloaded to finish the job before hitting the server unnecessarily!\n            if (parts.isNotEmpty() && parts.all { it.isCompleted }) {\n                onDownloadFinished()\n                return@launch\n            }\n            onDownloadResuming()\n            try {\n                fetchDownloadInfoAndValidate()\n                createPartsIfNotCreated()\n                prepareDestination {\n                    _status.value = DownloadJobStatus.PreparingFile(it)\n                }\n                createPartDownloaderList()\n//                println(\"part downloaders created\")\n                beginDownloadParts()\n                startAutoSaver()\n                downloadItem.status = DownloadStatus.Downloading\n                if (downloadItem.startTime == null) {\n                    downloadItem.startTime = System.currentTimeMillis()\n                }\n                saveState()\n                onDownloadResumed()\n            } catch (e: Exception) {\n                e.printStackIfNOtUsual()\n                val shouldStop = when {\n                    ExceptionUtils.isNormalCancellation(e) -> true\n                    e is DownloadValidationException -> e.isCritical()\n                    else -> false\n                }\n                if (shouldStop) {\n                    // this function called from activeDownloadScope\n                    // so we change the scope here to prevent cancel this suspend function\n                    scope.launch {\n                        pause(e)\n                    }\n                } else {\n                    downloadFailedRetryOrPause(\n                        e = e,\n                        isInFirstResume = isInFirstResume,\n                    )\n                }\n            }\n        }.join()\n    }\n\n\n    private suspend fun prepareDestination(\n        onProgressUpdate: (Int?) -> Unit,\n    ) {\n        withContext(Dispatchers.IO) {\n            destination.outputSize = downloadItem.contentLength\n                .takeIf {\n                    // reset size if we have a non-strict download (webpage etc.\n                    strictDownload\n                }\n                ?.takeIf {\n                    // reset output file if we can't support the file\n                    supportsConcurrent != false\n                }\n                ?: IDownloadItem.LENGTH_UNKNOWN\n            // first we try to create the folder\n            // maybe the storage wasn't mounted yet, in that case we get an exception here\n            // it should be here to prevent resetting the download\n            try {\n                destination.prepareDestinationFolder()\n            } catch (e: Exception) {\n                e.throwIfCancelled()\n                throw PrepareDestinationFailedException(e)\n            }\n            if (!destination.isDownloadedPartsIsValid()) {\n                //file deleted or something!\n                parts.forEach { it.resetCurrent() }\n                saveState()\n            }\n//          thisLogger().info(\"preparing file\")\n            try {\n                destination.prepareFile(onProgressUpdate)\n            } catch (e: Exception) {\n                e.throwIfCancelled()\n                throw PrepareDestinationFailedException(e)\n            }\n            val lastModified = serverLastModified.takeIf { downloadManager.settings.useServerLastModifiedTime }\n            destination.setLastModified(lastModified)\n//            thisLogger().info(\"file prepared\")\n        }\n    }\n\n    override fun getDownloadedSize(): Long {\n        return getParts().sumOf {\n            it.howMuchProceed()\n        }\n//        return partDownloaderList.values.sumOf {\n//            it.progressFlow.value.value\n//        }\n    }\n\n\n    fun onPreferredConnectionCountChanged() {\n        activeDownloadScope?.launch {\n            beginDownloadParts()\n        }\n    }\n\n    override suspend fun changeConfig(\n        updater: (IDownloadItem) -> Unit,\n        extraConfig: DownloadJobExtraConfig?\n    ): IDownloadItem {\n        boot()\n        val previousItem = downloadItem.copy()\n        val newItem = previousItem.copy().apply(updater)\n        val previousDestination = downloadManager.calculateOutputFile(previousItem)\n        val newDestination = downloadManager.calculateOutputFile(newItem)\n        val shouldUpdateDestination = previousDestination != newDestination\n        if (shouldUpdateDestination) {\n            if (isDownloadActive.value) {\n                pause()\n            }\n            destination.moveOutput(newDestination)\n        }\n        // if there is no error update the actual download item\n        downloadItem.applyFrom(newItem)\n        if (shouldUpdateDestination) {\n            // destination should be closed for now!\n            initializeDestination()\n        }\n        if (previousItem.preferredConnectionCount != downloadItem.preferredConnectionCount) {\n            onPreferredConnectionCountChanged()\n        }\n        if (previousItem.link != downloadItem.link) {\n            onLinkChanged()\n        }\n        applySpeedLimit()\n        extraConfig?.let {\n            extraConfigsReceived(it)\n        }\n        saveDownloadItem()\n        return downloadItem\n    }\n\n    private fun applySpeedLimit() {\n        jobThrottler.bytesPerSecond(bytesPerSecond = downloadItem.speedLimit)\n    }\n\n    fun onLinkChanged() {\n        scope.launch {\n            if (activeDownloadScope?.isActive == true) {\n                pause()\n                resume()\n            }\n\n        }\n    }\n\n    fun getRequestedPartitionCount(): Int {\n        return downloadItem.preferredConnectionCount\n            ?: downloadManager.settings.defaultThreadCount\n    }\n\n    private suspend fun createPartsIfNotCreated() {\n        if (parts.isNotEmpty()) {\n            return\n        }\n        if (downloadItem.contentLength == IDownloadItem.LENGTH_UNKNOWN) {\n            setParts(\n                listOf(RangedPart(0, null, 0))\n            )\n        } else {\n            if (supportsConcurrent == true) {\n                //split parts\n                setParts(\n                    splitToRange(\n                        minPartSize = downloadManager.settings.minPartSize,\n                        maxPartCount = getRequestedPartitionCount().toLong(),\n                        size = downloadItem.contentLength,\n                    ).map {\n                        RangedPart(it.first, it.last)\n                    })\n            } else {\n                setParts(\n                    listOf(RangedPart(0, (downloadItem.contentLength - 1).takeIf { it >= 0 }, 0))\n                )\n            }\n\n        }\n\n//        thisLogger().info(\"dl_$id parts created $parts\")\n\n        saveState()\n    }\n\n    private val partSplitLock = Any()\n    private val partLoopLock = Mutex()\n\n    //    private val c = AtomicInteger(0)\n    private fun beginDownloadParts() {\n        if (partLoopLock.isLocked) {\n            return\n        }\n        activeDownloadScope?.launch {\n            if (!partLoopLock.tryLock()) {\n                return@launch\n            }\n//            c.incrementAndGet()\n            try {\n                val activeCount = getPartDownloaderList()\n                    .count { it.active }\n                val howMuchCreate = getRequestedPartitionCount() - activeCount\n                if (howMuchCreate > 0) {\n                    val mutableInactivePartDownloaderList = getPartDownloaderList()\n                        .filter { !it.active && !it.part.isCompleted }\n                        .sortedBy { it.part.from }\n                        .toMutableList()\n//                    println(mutableInactivePartDownloaderList)\n\n                    fun getPartDownloader(): HttpPartDownloader? {\n                        val inactivePart =\n                            runCatching { mutableInactivePartDownloaderList.removeAt(0) }.getOrNull()\n                        if (inactivePart != null) return inactivePart\n                        if (supportsConcurrent == true && downloadManager.settings.dynamicPartCreationMode) {\n                            synchronized(partSplitLock) {\n                                val candidates = getPartDownloaderList()\n                                    .toList()\n                                    .filter { it.canBeSplit() }\n                                    .sortedByDescending {\n                                        it.part.remainingLength\n                                    }\n                                for (i in candidates) {\n                                    val newPart = i.splitPart()\n                                    if (newPart != null) {\n//                                        println(\"a part split\")\n                                        parts.add(newPart)\n                                        parts.sortBy { it.from }\n                                        return getOrCreatePartDownloader(newPart)\n                                    }\n                                }\n                            }\n                        }\n                        return null\n                    }\n                    for (i in 1..howMuchCreate) {\n                        val partDownloader = getPartDownloader()\n                        if (partDownloader == null) {\n//                            println(\"part downloader is null\")\n                            break\n                        }\n                        if (partDownloader.part.isCompleted) {\n//                            println(\"it seems part is downloaded!\")\n                            continue\n                        }\n//                        println(\"got new part downloader ${partDownloader.part}\")\n                        partDownloader.start()\n                    }\n                }\n                if (howMuchCreate < 0) {\n                    partDownloaderList.values\n                        .toList()\n                        .filter { it.active }\n                        .sortedByDescending { it.part.from }\n                        .take(-howMuchCreate)\n                        .onEach {\n                            it.stop()\n                        }.onEach {\n                            it.join()\n                            it.awaitIdle()\n                        }\n                }\n            } catch (e: Exception) {\n                throw e\n            } finally {\n//                println(\"C:\" + c)\n                partLoopLock.unlock()\n//                c.decrementAndGet()\n            }\n        }\n    }\n\n    private fun onPartHaveToManyError(throwable: Throwable) {\n        var paused = false\n        if (throwable is DownloadValidationException) {\n            if (throwable.isCritical()) {\n                //stop the whole job! as we have big problem here\n                paused = true\n                scope.launch {\n                    pause(throwable)\n                }\n            }\n        }\n        val allHaveError = partDownloaderList.values\n            .filter { it.active }\n            .all {\n                it.injured()\n            }\n        if (allHaveError && !paused) {\n//            println(\"all have error!\")\n            downloadFailedRetryOrPause(\n                e = throwable,\n                isInFirstResume = false,\n            )\n        }\n    }\n\n    // for this download job only, it has higher priority than download manager settings\n    var _maxAllowedRetries: Int? = null\n    fun getMaxAllowedRetries(): Int {\n        return _maxAllowedRetries ?: downloadManager.settings.maxDownloadRetryCount\n    }\n\n    var failedDownloadTries = 0\n    val delayForEachRetry = 3_000L\n    private var downloadedSizeBeforeRetry = 0L\n\n    private var retryJob: Job? = null\n\n    private val retryLock = Mutex()\n\n    // I have to improve this function to not allow accessing it concurrently\n    private fun downloadFailedRetryOrPause(\n        e: Throwable,\n        isInFirstResume: Boolean,\n    ) {\n        //moving to the main scope and request to cancel activeDownload scope!\n        scope.launch {\n            if (isInFirstResume && failedDownloadTries == 0 && shouldRetryIfInitialFailed()) {\n                if (ExceptionUtils.isNetworkError(e) || ExceptionUtils.isResponseError(e)) {\n                    pause(e)\n                    return@launch\n                }\n            }\n            // can't proceed\n            if (e is DownloadValidationException && e.isCritical()) {\n                pause(e)\n                return@launch\n            }\n            val downloadedSize = getDownloadedSize()\n            if (downloadedSize > downloadedSizeBeforeRetry) {\n                // download had progress! so we reset it\n                failedDownloadTries = 0\n            } else {\n                failedDownloadTries++\n            }\n            downloadedSizeBeforeRetry = downloadedSize\n\n            // we always have one try (the initial resume action), after that others are retries!\n            val retriedCount = (failedDownloadTries - 1).coerceAtLeast(0)\n            if (retriedCount < getMaxAllowedRetries()) {\n                retry(isInFirstResume)\n            } else {\n                pause(TooManyErrorException(e))\n            }\n        }\n    }\n\n    fun retry(isInFirstResume: Boolean) {\n        scope.launch {\n            val newScopeResult = retryLock.tryLocked {\n                val job = async {\n                    saveState()\n                    cancelDownloadScope()\n                    stopAllParts()\n                    _status.update { DownloadJobStatus.Retrying(delayForEachRetry) }\n                    delay(delayForEachRetry)\n                    createAndInitializeDownloadScope()\n                }\n                retryJob = job\n                job.await()\n            }\n            newScopeResult.getOrNull()?.let {\n                resumeWithNewScope(it, isInFirstResume)\n            }\n        }\n    }\n\n    fun shouldRetryIfInitialFailed(): Boolean {\n        return true\n    }\n\n\n    private fun onPartStatusChanged(\n        partDownloader: HttpPartDownloader,\n        partStatus: PartDownloadStatus,\n    ) {\n        when (partStatus) {\n            is PartDownloadStatus.Canceled -> {\n                destination.onPartCancelled(partDownloader.part)\n            }\n\n            PartDownloadStatus.Completed -> {\n                destination.onPartCancelled(partDownloader.part)\n                if (getParts().all { it.isCompleted }) {\n                    onDownloadFinished()\n                } else {\n                    scope.launch {\n                        beginDownloadParts()\n                    }\n                }\n            }\n\n            PartDownloadStatus.ReceivingData -> {}\n            PartDownloadStatus.Connecting -> {}\n            PartDownloadStatus.IDLE -> {}\n        }\n    }\n\n\n    @Synchronized\n    private fun createPartDownloaderList() {\n        synchronized(partDownloaderList) {\n            //        thisLogger().info(\"create part downloaders\")\n            parts.forEach {\n                getOrCreatePartDownloader(it)\n            }\n//            println(\"created N parts = \" + partDownloaderList.values.size)\n        }\n    }\n\n    private fun clearPartDownloaderList() {\n//        thisLogger().info(\"create part downloaders\")\n        parts.forEach {\n            destroyPartDownloader(it)\n        }\n    }\n\n    private val jobThrottler = Throttler()\n\n    private val partDownloaderList = ConcurrentHashMap<Long, HttpPartDownloader>()\n    private val listenerJobs: MutableMap<Long, Job> = ConcurrentHashMap<Long, Job>()\n    private fun getPartDownloaderList(): List<HttpPartDownloader> {\n        synchronized(partDownloaderList) {\n            return partDownloaderList.map { it.value }\n        }\n    }\n\n    private fun getOrCreatePartDownloader(part: RangedPart): HttpPartDownloader {\n        synchronized(partDownloaderList) {\n            return partDownloaderList.getOrPut(part.from) {\n                HttpPartDownloader(\n                    credentials = downloadItem,\n                    part = part,\n                    getDestWriter = {\n                        destination.getWriterFor(part)\n                    },\n                    client = client,\n                    speedLimiters = listOf(\n                        downloadManager.throttler,\n                        jobThrottler,\n                    ),\n                    strictMode = strictDownload,\n                    partSplitLock = partSplitLock\n                ).also { partDownloader: HttpPartDownloader ->\n                    partDownloader.onTooManyErrors = {\n                        onPartHaveToManyError(it)\n                    }\n                    //we should close that scope after we don't need it anymore!\n                    listenerJobs[part.from] = partDownloader.statusFlow.onEach { status ->\n                        //TODO probably bug here\n                        onPartStatusChanged(partDownloader, status)\n                    }.launchIn(scope)\n                }\n            }\n        }\n    }\n\n    private fun destroyPartDownloader(part: RangedPart) {\n        listenerJobs.remove(part.from)?.cancel()\n        partDownloaderList.remove(part.from)\n    }\n\n    private fun isDownloadItemIsAWebpage(): Boolean {\n        return downloadItem.name.endsWith(\".html\", true)\n    }\n\n    private suspend fun fetchDownloadInfoAndValidate(\n    ) {\n//        println(\"fetch download \")\n\n//        thisLogger().info(\"fetchDownloadInfoAndValidate\")\n        val response = client.test(downloadItem).expectSuccess()\n\n        supportsConcurrent?.let { previouslyConcurrentWasSupported ->\n            if (previouslyConcurrentWasSupported && !response.resumeSupport) {\n                // server at some point tell us it supports resuming, and we created more than 1 part!\n                // and now it says not resuming isn't supported!\n                // we must stop here!\n                // user must manually restart download or we should retry\n                throw ServerResumeSupportChangeException()\n            }\n        }\n\n        supportsConcurrent = response.resumeSupport\n        serverLastModified = runCatching {\n            response.lastModified?.let(TimeUtils::convertLastModifiedHeaderToTimestamp)\n        }.getOrNull()\n        if (response.isWebPage) {\n            if (isDownloadItemIsAWebpage()) {\n                // don't strict if it's a webpage let it download without checks\n                strictDownload = false\n\n                // this makes the file not resume able\n                // we don't want to page downloaded with multi connection\n                // so the download will be restarted [@see prepareDestination]\n                supportsConcurrent = false\n                downloadItem.contentLength = IDownloadItem.LENGTH_UNKNOWN\n                downloadItem.serverETag = null\n            } else {\n                // if download was not a webpage and now this is a webpage\n                // it means maybe user have to change its download link\n                // we should not restart download here!\n                throw FileChangedException.GotAWebPage()\n            }\n        }\n        val totalLength = response.totalLength\n        val oldServerETag = downloadItem.serverETag\n        val newServerETag = response.etag\n        if (downloadItem.contentLength == IDownloadItem.LENGTH_UNKNOWN) {\n            //new download / or restart\n            downloadItem.contentLength = totalLength ?: -1\n            downloadItem.serverETag = newServerETag\n        } else {\n            // check if we file not changed from remote\n            if (totalLength != downloadItem.contentLength) {\n                throw FileChangedException.LengthChangedException(downloadItem.contentLength, totalLength ?: -1)\n            }\n            if (oldServerETag != null && newServerETag != null) {\n                // we already know that sizes are the same,\n                // but we also have etag header\n                // so, we have chance to compare file contents of local and server\n                if (oldServerETag != newServerETag) {\n                    throw FileChangedException.ETagChangedException(oldServerETag, newServerETag)\n                }\n            }\n        }\n//            thisLogger().info(\"fetchDownloadInfoAndValidate :${response.code},${response.headers} \")\n        saveState()\n    }\n\n    suspend fun cancelDownloadScope() {\n        activeDownloadScope?.coroutineContext?.job?.cancelAndJoin()\n        activeDownloadScope = null\n    }\n\n    suspend fun cancelRetry() {\n        retryJob?.cancel()\n        retryJob = null\n    }\n\n    suspend fun stopAllParts() {\n        withContext(Dispatchers.IO) {\n            partDownloaderList.values.onEach {\n                it.stop()\n            }.onEach {\n                it.join()\n                it.awaitIdle()\n            }\n        }\n    }\n\n    override suspend fun pause(throwable: Throwable) {\n        boot()\n        failedDownloadTries = 0\n        cancelRetry()\n        cancelDownloadScope()\n        stopAllParts()\n        onDownloadCanceled(throwable)\n    }\n\n    override fun onDownloadFinishedBeforeSave() {\n        if (downloadItem.contentLength == IDownloadItem.LENGTH_UNKNOWN) {\n            //in case of blind part, update download item length\n            if (parts.size == 1) {\n                downloadItem.contentLength = parts[0].howMuchProceed()\n            }\n        }\n    }\n\n    private var lastSavedDownloadItem: HttpDownloadItem? = null\n    private var lastSavedParts: List<RangedPart>? = null\n\n    private suspend fun saveDownloadItem() {\n        itemSaveLock.withLock {\n            val copy = downloadItem.copy()\n            if (lastSavedDownloadItem != downloadItem) {\n                listDb.update(downloadItem)\n                lastSavedDownloadItem = copy\n            }\n        }\n    }\n\n    private suspend fun saveParts() {\n        partLock.withLock {\n            val copy = getParts().map { it.copy() }\n            if (lastSavedParts != copy) {\n                destination.flush()\n                partListDb.setParts(id, RangedParts(copy))\n                lastSavedParts = copy\n            }\n        }\n    }\n\n    override suspend fun saveState() {\n        saveDownloadItem()\n        saveParts()\n    }\n\n    fun getParts(): List<RangedPart> {\n        //Make a copy because of CMException\n        return parts.toList()\n    }\n\n    override fun reloadSettings() {\n        onPreferredConnectionCountChanged()\n    }\n\n    override suspend fun extraConfigsReceived(config: DownloadJobExtraConfig) {\n        // we don't have extra configs\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/http/HttpDownloader.kt",
    "content": "package ir.amirab.downloader.downloaditem.http\n\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.Downloader\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.serialization.KSerializer\nimport kotlin.reflect.KClass\n\nclass HttpDownloader(\n    httpDownloaderClient: Lazy<HttpDownloaderClient>\n) : Downloader<HttpDownloadItem, HttpDownloadJob, HttpDownloadCredentials> {\n    val httpDownloaderClient by httpDownloaderClient\n    override fun createJob(\n        item: HttpDownloadItem,\n        downloadManager: DownloadManager,\n    ): HttpDownloadJob {\n        return HttpDownloadJob(\n            item,\n            downloadManager,\n            httpDownloaderClient,\n        )\n    }\n\n    override fun accept(item: IDownloadItem): Boolean {\n        return item is HttpDownloadItem\n    }\n\n    override val downloadItemClass: KClass<HttpDownloadItem> = HttpDownloadItem::class\n    override val downloadCredentialsClass: KClass<HttpDownloadCredentials> = HttpDownloadCredentials::class\n    override val downloadJobClass: KClass<HttpDownloadJob> = HttpDownloadJob::class\n    override val downloadItemSerializer: KSerializer<HttpDownloadItem> = HttpDownloadItem.serializer()\n    override val downloadCredentialsSerializer: KSerializer<HttpDownloadCredentials> =\n        HttpDownloadCredentials.serializer()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/downloaditem/http/IHttpBasedDownloadCredentials.kt",
    "content": "package ir.amirab.downloader.downloaditem.http\n\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\n\ninterface IHttpBasedDownloadCredentials : IDownloadCredentials {\n    val headers: Map<String, String>?\n    val username: String?\n    val password: String?\n    val userAgent: String?\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/DownloadNotSuccessFullException.kt",
    "content": "package ir.amirab.downloader.exception\n\nimport java.io.IOException\n\nclass UnSuccessfulResponseException(val code:Int,msg:String):IOException(\n    \"$code | $msg\"\n)"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/DownloadValidationException.kt",
    "content": "package ir.amirab.downloader.exception\n\nabstract class DownloadValidationException(\n    msg: String,\n    cause: Throwable? = null,\n) : Exception(msg, cause) {\n    abstract fun isCritical(): Boolean\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/FileChangedException.kt",
    "content": "package ir.amirab.downloader.exception\n\n\nsealed class FileChangedException(msg: String) : DownloadValidationException(msg) {\n    override fun isCritical(): Boolean {\n        // download must stop immediately\n        return true\n    }\n\n    class LengthChangedException(\n        val lastContentLength: Long,\n        val newContentLength: Long\n    ) : FileChangedException(\n        \"File size changed since last download! last time was $lastContentLength now it's $newContentLength\"\n    )\n\n    class ETagChangedException(\n        val oldETag: String,\n        val newETag: String\n    ) : FileChangedException(\n        \"File content changed since last download! last time was $oldETag now it's $newETag\"\n    )\n\n    class GotAWebPage : FileChangedException(\n        \"link is a webpage\"\n    )\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/NoSpaceInStorageException.kt",
    "content": "package ir.amirab.downloader.exception\n\nclass NoSpaceInStorageException(\n    val available: Long,\n    val required: Long\n) : DownloadValidationException(\n    \"No space available required=$required , available=$available\"\n) {\n    override fun isCritical(): Boolean {\n        // there is no space in users file system so we should stop\n        return true\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/PartTooManyErrorException.kt",
    "content": "package ir.amirab.downloader.exception\n\nimport ir.amirab.downloader.part.DownloadPart\n\nclass PartTooManyErrorException(\n    part: DownloadPart,\n    override val cause: Throwable\n) : Exception(\n        \"this part $part have too many errors\",\n    cause,\n)\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/PrepareDestinationFailedException.kt",
    "content": "package ir.amirab.downloader.exception\n\nclass PrepareDestinationFailedException(\n    e: Exception\n) : DownloadValidationException(\n    \"Problem in preparing output: ${e.localizedMessage}\",\n    e,\n) {\n    override fun isCritical(): Boolean {\n        // there is a problem when preparing destination. retry doesn't work here\n        return true\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/ServerPartIsNotTheSameAsWeExpectException.kt",
    "content": "package ir.amirab.downloader.exception\n\n//it should not happen unless web server is not respect our header\nclass ServerPartIsNotTheSameAsWeExpectException(\n    start:Long,\n    end:Long?,\n    expectedLength:Long?,\n    actualLength:Long?,\n) : DownloadValidationException (\n    \"Response Length not match.expecting '${expectedLength}',but we got '$actualLength',requested range is range is ${start}-${end}\"\n//            + \"\\n request headers ${conn.responseInfo.requestHeaders}\"\n//            + \"\\n response headers ${conn.responseInfo.responseHeaders}\"\n){\n    override fun isCritical(): Boolean {\n        // some webservers somehow does not return the expected size at the first place\n        // but after some try... they do!!!\n        // because of them, I have to make this error non-critical\n        // I have to investigate why!\n        return false\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/ServerResumeSupportChangeException.kt",
    "content": "package ir.amirab.downloader.exception\n\n// this happens on some CDN (multiple servers/load balancers, serving the same file)\n// let's say we ask for 10 connections 7 times they say support resuming and 3 time they say I'm not support resuming.\n// if the first initial connection sees that the download doesn't support resuming. so the app shows you it won't support resuming at all.\n// if we are detected that the file supports resume we throw this exception\n// we shouldn't automatically reset the download, we should retry, or user needs to manually restart the download\nclass ServerResumeSupportChangeException : DownloadValidationException(\n    \"Server resume support changed, please restart the download manually\"\n) {\n    override fun isCritical(): Boolean {\n        return false\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/exception/TooManyErrorException.kt",
    "content": "package ir.amirab.downloader.exception\n\nclass TooManyErrorException(\n    override val cause: Throwable\n) : Exception(\n    \"Download is stopped because all parts exceeds max retries\",\n) {\n    fun findActualDownloadErrorCause(): Throwable {\n        return when (cause) {\n            is PartTooManyErrorException -> cause.cause\n            else -> cause\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/DownloadPart.kt",
    "content": "package ir.amirab.downloader.part\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface DownloadPart {\n    var current: Long\n\n    // internal usage do not change it!\n    val statusFlow: MutableStateFlow<PartDownloadStatus>\n    val status get() = statusFlow.value\n    val isCompleted: Boolean\n    val percent: Int?\n    fun howMuchProceed(): Long\n    fun resetCurrent()\n\n    // an id which also is sortable among the other parts\n    // 1, 2, 3, ...\n    fun getID(): Long\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/HttpPartDownloader.kt",
    "content": "package ir.amirab.downloader.part\n\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.Connection\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.connection.response.expectSuccess\nimport ir.amirab.downloader.destination.DestWriter\nimport ir.amirab.downloader.downloaditem.http.IHttpDownloadCredentials\nimport ir.amirab.downloader.exception.ServerPartIsNotTheSameAsWeExpectException\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.isActive\nimport okio.*\n\n\n/**\n * @param strictMode\n *  `false` - this is not the purpose of its app, so we don't strict here\n *\n *  download part without checking for length validation\n *  this is where we want to only copy data arrived from server\n *  for example, a web page link that maybe get us different response length\n *  we only need to download it no matter what is inside\n *\n *  `true` - main purpose of this class\n *\n *  validate download size before trying to write to the filesystem\n */\nclass HttpPartDownloader(\n    val credentials: IHttpDownloadCredentials,\n    getDestWriter: () -> DestWriter,\n    part: RangedPart,\n    val client: HttpDownloaderClient,\n    val speedLimiters: List<Throttler>,\n    val strictMode: Boolean,\n    partSplitLock: Any,\n) : PartDownloader<RangedPart>(\n    part = part,\n    getDestWriter = getDestWriter\n) {\n\n\n    private suspend fun establishConnection(\n        from: Long,\n        to: Long?,\n    ): Connection<HttpResponseInfo> {\n        val connect = client.connect(credentials, from, to)\n        // make sure this is a 2xx response\n        kotlin.runCatching {\n            connect.responseInfo.expectSuccess()\n        }\n            .onFailure {\n                // close connection before throwing exception\n                kotlin.runCatching {\n                    connect.close()\n                }\n            }\n            .getOrThrow()\n        val source = speedLimiters.fold<Throttler, Source>(connect.source) { acc, throttler ->\n            throttler.source(acc)\n        }\n        return connect.copy(\n            source = source\n        )\n    }\n\n    override fun onFinish() {\n        synchronized(partSplitSupport) {\n            if (part.isBlind) {\n                part.setBlindAsCompleted()\n            }\n        }\n        super.onFinish()\n    }\n\n    private val partSplitSupport = PartSplitSupport(part, partSplitLock)\n\n    //this method is invoked only in one thread for every instance\n    override fun howMuchCanRead(maxAllowed: Long): Long {\n        return partSplitSupport.howMuchCanRead(\n            expandToBufferSize = maxAllowed,\n            tryToExtendSafeZone = true\n        )\n    }\n\n    fun canBeSplit(): Boolean {\n        return partSplitSupport.canSplit()\n    }\n\n    override suspend fun connectAndVerify(): Connection<HttpResponseInfo> {\n        //        thisLogger().info(\"going to copy data to destination\")\n        //we copy part because maybe part::to property will change during part split,\n        //so we make backup of current part to validate http response\n        val partCopy = part.copy()\n        val conn = establishConnection(partCopy.current, partCopy.to)\n//        thisLogger().info(\"connection established\")\n        if (stop || !currentCoroutineContext().isActive) {\n            conn.close()\n            throw CancellationException()\n        }\n        val contentLength = conn.contentLength.let {\n            if (it == -1L) {\n                //in case of no end is come from headers\n                null\n            } else {\n                it\n            }\n        }\n        if (contentLength != partCopy.remainingLength) {\n            var throwServerPartIsNotTheSameAsWeExpectException: Boolean\n            if (strictMode) {\n                throwServerPartIsNotTheSameAsWeExpectException = true\n                // allow pass through if the request range start and response range start are the same\n                conn.responseInfo.contentRange?.range?.let { range ->\n                    if (range.first == partCopy.current) {\n                        // if I request from 1..10 then I expect that server give me 1-X the X is not important\n                        // but the start should be the same as requested otherwise we can't trust the server response\n                        // X may be smaller/bigger than our requested range however we download it as much as we want if it wasn't enough we re request again later\n                        throwServerPartIsNotTheSameAsWeExpectException = false\n                    }\n                }\n            } else {\n                // just download it we don't want to validate anything here\n                throwServerPartIsNotTheSameAsWeExpectException = true\n            }\n            val serverPartIsNotTheSameAsWeExpectException = ServerPartIsNotTheSameAsWeExpectException(\n                start = partCopy.current,\n                end = partCopy.to,\n                expectedLength = partCopy.remainingLength,\n                actualLength = contentLength\n            )\n            if (throwServerPartIsNotTheSameAsWeExpectException) {\n                conn.close()\n                throw serverPartIsNotTheSameAsWeExpectException\n            } else {\n                println(\"WARNING: ${serverPartIsNotTheSameAsWeExpectException.message}\")\n            }\n        }\n        return conn\n    }\n\n\n    //should be sync with part split lock\n    fun splitPart(): RangedPart? {\n        return partSplitSupport.splitPart()\n    }\n\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/MediaSegment.kt",
    "content": "package ir.amirab.downloader.part\n\nimport ir.amirab.downloader.utils.calcPercent\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n@Serializable\ndata class MediaSegment(\n    val segmentIndex: Long,\n    val link: String,\n    var duration: Double,\n    override var isCompleted: Boolean = false,\n    var length: Long? = null,\n    @Transient\n    override var current: Long = 0,\n) : DownloadPart {\n\n    override fun howMuchProceed() = current\n\n    override fun resetCurrent() {\n        current = 0\n    }\n\n    @Transient\n    override val statusFlow = MutableStateFlow<PartDownloadStatus>(PartDownloadStatus.IDLE)\n\n    override val percent: Int?\n        get() = length?.let {\n            calcPercent(howMuchProceed(), it)\n        }\n\n    override fun getID(): Long {\n        return segmentIndex.toLong()\n    }\n\n    companion object {\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/PartDownloadStatus.kt",
    "content": "package ir.amirab.downloader.part\n\nimport ir.amirab.downloader.utils.ExceptionUtils\n\nsealed class PartDownloadStatus {\n    interface IsActive\n    interface IsInactive\n\n\n    override fun toString(): String {\n        return this::class.simpleName!!\n    }\n\n    object IDLE : PartDownloadStatus(),IsInactive\n    data class Canceled(val e: Throwable) : PartDownloadStatus(),IsInactive {\n        fun isNormalCancellation(): Boolean {\n            return ExceptionUtils.isNormalCancellation(e)\n        }\n    }\n\n    object Completed : PartDownloadStatus(),IsInactive\n    object Connecting : PartDownloadStatus(), IsActive\n    object ReceivingData : PartDownloadStatus(),IsActive\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/PartDownloader.kt",
    "content": "package ir.amirab.downloader.part\n\nimport ir.amirab.downloader.anntation.HeavyCall\nimport ir.amirab.downloader.connection.Connection\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.destination.DestWriter\nimport ir.amirab.downloader.exception.DownloadValidationException\nimport ir.amirab.downloader.exception.PartTooManyErrorException\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.downloader.utils.printStackIfNOtUsual\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.NonCancellable\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.withTimeoutOrNull\nimport okio.Buffer\nimport okio.Source\nimport okio.use\nimport kotlin.concurrent.thread\nimport kotlin.math.min\n\nconst val PART_MAX_TRIES = 10\nconst val RetryDelay = 1_000L\n\nabstract class PartDownloader<\n        TPart : DownloadPart\n        >(\n    val part: TPart,\n    val getDestWriter: () -> DestWriter\n) {\n    private var thread: Thread? = null\n    private var scope: CoroutineScope? = null\n    private val _statusFlow = part.statusFlow\n    val statusFlow = _statusFlow.asStateFlow()\n\n    @Volatile\n    internal var active = false\n\n    abstract fun howMuchCanRead(maxAllowed: Long): Long\n\n    @Volatile\n    internal var tries = 0\n\n    // make sure to not lake resource in this exception\n    @Volatile\n    private var lastCriticalException: Throwable? = null\n\n    // make sure to not lake resource in this exception\n    @Volatile\n    private var lastException: Throwable? = null\n\n    //just turn on (fast)\n    fun start() {\n        synchronized(this) {\n            if (active) {\n                return\n            }\n            stop = false\n            active = true\n        }\n        val scope = CoroutineScope(SupervisorJob()).also {\n            this.scope = it\n        }\n        scope.launch {\n            tries = 0\n            lastCriticalException = null\n            lastException = null\n            val result = runCatching {\n                while (coroutineContext.isActive || !stop) {\n                    if (tries > 0) {\n                        delay(RetryDelay)\n//                        println(\"#${part.from}retrying $tries\")\n                    }\n                    if (haveToManyErrors()) {\n//                        println(\"tell them we have error!\")\n                        iCantRetryAnymore(\n                            PartTooManyErrorException(\n                                part,\n                                lastException\n                                    ?: Exception(\"BUG : if you see me please report it to the developer! when we encounter error so it have to be a least one last exception\"),\n                            )\n                        )\n                    }\n                    if (part.isCompleted) {\n                        println(\"WARNING $part is completed\")\n                    }\n                    try {\n                        download()\n                    } catch (e: Exception) {\n                        tries++\n                        onCanceled(e)\n                        when (canRetry(e)) {\n                            CanRetryResult.Yes -> continue\n                            CanRetryResult.No -> {}\n                            CanRetryResult.NoAndStopDownloadJob -> iCantRetryAnymore(e)\n                        }\n                        break\n                    }\n                    //download progress started, but maybe we have errors\n                    //wait for a finish/error event...\n                    //await for cancel status to be emitted!\n                    val status = withContext(NonCancellable) {\n                        awaitFinishOrError()\n                    }\n                    when (status) {\n                        is PartDownloadStatus.Canceled -> {\n                            tries++\n                            when (canRetry(status.e)) {\n                                CanRetryResult.Yes -> continue\n                                CanRetryResult.No -> {}\n                                CanRetryResult.NoAndStopDownloadJob -> iCantRetryAnymore(status.e)\n                            }\n                            break\n                        }\n\n                        PartDownloadStatus.Completed -> break\n                        else -> throw ShouldNotHappened(\"should not happened!\")\n                    }\n                }\n            }\n\n            active = false\n            if (!part.isCompleted) {\n                part.statusFlow.value = PartDownloadStatus.IDLE\n            }\n\n            result.onFailure {\n                if (it is ShouldNotHappened) {\n                    throw it\n                }\n            }\n        }\n    }\n\n    @Volatile\n    var stop = false\n    fun stop() {\n        stop = true\n        thread?.interrupt()\n        scope?.coroutineContext?.job?.cancel()\n    }\n\n    suspend fun join() {\n        withContext(Dispatchers.IO) {\n            scope?.coroutineContext?.job?.join()\n            thread?.join()\n        }\n    }\n\n\n    private fun canRetry(e: Throwable): CanRetryResult {\n        return when {\n            ExceptionUtils.isNormalCancellation(e) -> {\n                CanRetryResult.No\n            }\n\n            e is DownloadValidationException -> if (e.isCritical()) {\n                //download validation occurs, and also it is critical,\n                //so we can't proceed any further\n                CanRetryResult.NoAndStopDownloadJob\n            } else {\n                CanRetryResult.Yes\n            }\n\n            else -> {\n                CanRetryResult.Yes\n            }\n        }\n    }\n\n    lateinit var onTooManyErrors: ((Throwable) -> Unit)\n    private fun iCantRetryAnymore(throwable: Throwable) {\n        lastCriticalException = throwable\n        GlobalScope.launch {\n            onTooManyErrors(throwable)\n        }\n    }\n\n    private fun haveToManyErrors(): Boolean {\n        return tries >= PART_MAX_TRIES\n    }\n\n    private fun haveCriticalError(): Boolean {\n        return lastCriticalException != null\n    }\n\n    internal fun injured(): Boolean {\n        return haveToManyErrors() || haveCriticalError()\n    }\n\n    abstract suspend fun connectAndVerify(): Connection<*>\n\n    private suspend fun download() {\n        onNewStatus(PartDownloadStatus.Connecting)\n        val conn = connectAndVerify()\n        thread = thread {\n            if (stop || Thread.currentThread().isInterrupted) {\n                conn.close()\n                onCanceled(kotlinx.coroutines.CancellationException())\n                return@thread\n            }\n//            thisLogger().info(\"going to copy data to destination $conn\")\n            try {\n                conn.use {\n                    // connection automatically closes the source\n                    val connectionStream = it.source\n                    getDestWriter().use { writer ->\n                        copyDataSync(connectionStream, writer)\n                    }\n                }\n            } catch (e: Exception) {\n                onCanceled(e)\n            } finally {\n                thread = null\n            }\n        }\n    }\n\n    protected open fun onCanceled(e: Throwable) {\n        lastException = e\n        val canceled = PartDownloadStatus.Canceled(e)\n        onNewStatus(canceled)\n        e.printStackIfNOtUsual()\n//        if (!canceled.isNormalCancellation()) {\n//            e.printStackTrace()\n//        } else {\n//            println(\"part cancelled because of ${e.localizedMessage ?: e::class.simpleName}\")\n//            e.printStackTrace()\n//        }\n    }\n\n    protected open fun onFinish() {\n        onNewStatus(PartDownloadStatus.Completed)\n    }\n\n    fun onNewStatus(partDownloadStatus: PartDownloadStatus) {\n        _statusFlow.value = partDownloadStatus\n    }\n\n    @HeavyCall\n    private fun copyDataSync(source: Source, destWriter: DestWriter) {\n//        println(\"copying range to file --- ${part.current}-${part.to}\")\n        val buffer = Buffer()\n        var totalReadCount = 0L\n        var firstLoop = true\n        val bufferSize = DEFAULT_BUFFER_SIZE.toLong()\n        while (true) {\n            if (stop || Thread.currentThread().isInterrupted) {\n                onCanceled(kotlinx.coroutines.CancellationException())\n                break\n            }\n            val howMuchICanReadAllowed = howMuchCanRead(bufferSize)\n            val homMuchReadFromBuffer = min(bufferSize, howMuchICanReadAllowed)\n//            require(part.current + homMuchReadFromBuffer <= part.maxAllowedCurrent) {\n//                \"\"\"$partSplitSupport\n//                canRead:${homMuchReadFromBuffer}\"\"\"\n//            }\n//            require(part.current + homMuchReadFromBuffer <= partSplitSupport.safeZone + 1) {\n//                \"\"\"a\n//                    part=${part} isCompleted =${part.isCompleted}\n//                    split part $partSplitSupport\n//                    howMuch:${homMuchReadFromBuffer}\n//                    actual:${part.current + homMuchReadFromBuffer}\n//                    expected:${partSplitSupport.safeZone}\n//                \"\"\".trimIndent()\n//            }\n            if (howMuchICanReadAllowed <= 0) {\n                if (part.isCompleted) {\n                    onFinish()\n                } else {\n                    onCanceled(kotlinx.coroutines.CancellationException(\"it seems our part was split so we are canceled $part\"))\n                }\n                break\n            }\n            val readCount = source.read(buffer, homMuchReadFromBuffer)\n//            require(readCount <= homMuchReadFromBuffer) {\n//                \"read count $readCount is bigger than homMuchReadFromBuffer $homMuchReadFromBuffer\"\n//            }\n            if (readCount == -1L) {\n                onFinish()\n                break\n            }\n            destWriter.write(buffer, readCount)\n            totalReadCount += readCount\n            part.current += readCount\n//                require (part.current-part.from == totalReadCount)\n            if (firstLoop) {\n                tries = 0\n                onNewStatus(PartDownloadStatus.ReceivingData)\n                firstLoop = false\n            }\n        }\n    }\n\n    suspend fun awaitFinishOrError(): PartDownloadStatus {\n        return statusFlow.filter {\n            when (it) {\n                PartDownloadStatus.Completed,\n                is PartDownloadStatus.Canceled,\n                    -> true\n\n                PartDownloadStatus.ReceivingData,\n                PartDownloadStatus.Connecting,\n                PartDownloadStatus.IDLE,\n                    -> false\n            }\n        }.first()\n    }\n\n    suspend fun awaitToEnsureDataBeingTransferred(): Boolean {\n        return withTimeoutOrNull(5_000) {\n            val isThatOk = statusFlow.filter {\n                when (it) {\n                    PartDownloadStatus.Completed,\n                    PartDownloadStatus.ReceivingData,\n                        -> true\n\n                    is PartDownloadStatus.Canceled,\n                    PartDownloadStatus.Connecting,\n                    PartDownloadStatus.IDLE,\n                        -> false\n                }\n            }.first().let {\n                when (it) {\n                    is PartDownloadStatus.Canceled -> false\n                    PartDownloadStatus.Completed -> true\n                    PartDownloadStatus.ReceivingData -> true\n                    PartDownloadStatus.Connecting,\n                    PartDownloadStatus.IDLE,\n                        -> error(\"should not happen\")\n                }\n            }\n            isThatOk\n        } ?: false\n    }\n\n    suspend fun awaitIdle() {\n        statusFlow.filter {\n            when (it) {\n                is PartDownloadStatus.Canceled,\n                PartDownloadStatus.Completed,\n                PartDownloadStatus.IDLE,\n                    -> true\n\n                PartDownloadStatus.Connecting,\n                PartDownloadStatus.ReceivingData,\n                    -> false\n            }\n        }.first()\n    }\n\n    class ShouldNotHappened(msg: String?) : RuntimeException(msg)\n    private sealed interface CanRetryResult {\n        data object Yes : CanRetryResult\n        data object No : CanRetryResult\n        data object NoAndStopDownloadJob : CanRetryResult\n    }\n}\n\n\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/PartSplitSupport.kt",
    "content": "package ir.amirab.downloader.part\n\nimport kotlin.math.min\n\n//fun main() {\n//  split = PartSplitSupport(part=Part(from=9530669, to=10565655, current=9664288), safeZone=9694508, remainingSafe=30221 ,old = Part(from=9530669, to=11436801, current=9664288) ,changed = Part(from=9530669, to=10565655, current=9664288) ,newPart= Part(from=10565655, to=11436801, current=10565655) ,valid = false ,\n//  split = PartSplitSupport(part=Part(from=9530669, to=10565655, current=9664288), safeZone=9694508, remainingSafe=30221 ,old = Part(from=9530669, to=11436801, current=9664288) ,changed = Part(from=9530669, to=10565655, current=9664288) ,newPart= Part(from=10565655, to=11436801, current=10565655) ,valid = false ,\n//    val p1 =Part(from=0, to=0, current=0)\n//    val ps = PartSplitSupport(p1).apply {\n//        safeZone = 9694508\n//    }\n//    println(ps.splitPart())\n//}\n\nclass PartSplitSupport(\n    val part: RangedPart,\n    private val partEndLock: Any = Any(),\n) {\n    //initial remainingSafe will be 0\n    @Volatile\n    var safeZone = part.current - 1\n\n    //pure\n    fun remainingSafe(): Long {\n        // +1 is because of end inclusive\n        return (safeZone + 1 - part.current).coerceAtLeast(0)\n    }\n\n    fun howMuchCanRead(\n        expandToBufferSize: Long? = null,\n        tryToExtendSafeZone: Boolean = expandToBufferSize != null,\n    ): Long {\n        val defaultRemaining = remainingSafe()\n        if (tryToExtendSafeZone) {\n            if (expandToBufferSize != null) {\n                if (defaultRemaining < expandToBufferSize) {\n                    if (extendSafeZone()) {\n                        return remainingSafe()\n                    }\n                }\n            } else if (defaultRemaining == 0L) {\n                if (extendSafeZone()) {\n                    return remainingSafe()\n                }\n            }\n        }\n        return defaultRemaining\n    }\n\n    fun extendSafeZone(): Boolean {\n        synchronized(partEndLock) {\n            //remaining\n            val remaining = part.remainingLength?:Long.MAX_VALUE\n            if (remaining == 0L) {\n                return false\n            }\n            val oldSafeZone = safeZone\n            val newSafeZone = (oldSafeZone + min(remaining, SAFE_ZONE_SIZE))\n                .coerceAtMost(part.to ?: Long.MAX_VALUE)\n\n            if (oldSafeZone==newSafeZone){\n                return false\n            }\n            safeZone=newSafeZone\n//            require(safeZone<=part.to)\n            return true\n        }\n    }\n\n    fun splitPart(): RangedPart? {\n        synchronized(partEndLock) {\n            if (!canSplit()) return null\n\n            val delta = part.to!! - safeZone\n            val safeZoneToEnd = safeZone + (delta / 2) + delta % 2\n//            val oldPart = part.copy()\n            if (safeZoneToEnd + 1 > part.to!!) {\n                //new part will exceed current part boundaries\n                return null\n            }\n            val newPart = RangedPart(\n                from = safeZoneToEnd + 1,\n                to = part.to!!\n            )\n            part.to = safeZoneToEnd\n//            val isValid = isSplitValid(oldPart, part, newPart)\n//            val safeZoneRespected = safeZone <= part.to!!\n//            println(\n//                \"split = $this ,\" +\n//                        \"safezone respected =$safeZoneRespected ,\"+\n//                        \"old = $oldPart ,\" +\n//                        \"changed = ${part.copy()} ,\" +\n//                        \"newPart= ${newPart.copy()} ,\" +\n//                        \"valid = $isValid ,\"\n//            )\n            return newPart\n        }\n    }\n\n    fun canSplit(): Boolean {\n        if (part.to == null) {\n            return false\n        }\n        val delta = part.to!! - safeZone\n        //We only want split a part that worth it!\n        return delta >= SAFE_ZONE_SIZE\n    }\n\n    override fun toString(): String {\n        return \"PartSplitSupport(part=$part, safeZone=$safeZone, remainingSafe=${remainingSafe()}\"\n    }\n\n    companion object {\n        private fun isSplitValid(\n            oldPart: RangedPart,\n            reducedPart: RangedPart,\n            newPart: RangedPart,\n        ): Boolean {\n            return (reducedPart.to == newPart.from - 1) && (oldPart.to == newPart.to)\n        }\n\n        //        const val SAFE_ZONE_SIZE: Long = 5\n        const val SAFE_ZONE_SIZE: Long = 128 * 8192\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/Parts.kt",
    "content": "package ir.amirab.downloader.part\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * the base interface for any downloader that may have parts\n * it is saved into storage for future resuming support\n */\n@Serializable\nsealed interface Parts {\n    fun clone(): Parts\n}\n\n/**\n * this type is used for downloaders that split download into multiple byte ranges,\n * like http\n */\n@SerialName(\"ranges\")\n@Serializable\ndata class RangedParts(\n    val list: List<RangedPart>\n) : Parts {\n    override fun clone(): Parts {\n        return copy()\n    }\n}\n\n/**\n * this type is used for downloaders that contains multiple links to download like hls\n */\n@SerialName(\"mediaSegments\")\n@Serializable\ndata class MediaSegments(\n    val list: List<MediaSegment>\n) : Parts {\n    override fun clone(): Parts {\n        return copy()\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/part/RangedPart.kt",
    "content": "package ir.amirab.downloader.part\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n@Serializable\ndata class RangedPart(\n    var from: Long,\n    @Volatile\n    var to: Long?,\n    @Volatile\n    override var current: Long = from,\n) : DownloadPart {\n\n    override fun howMuchProceed(): Long {\n        return current - from\n    }\n\n    override fun resetCurrent() {\n        current = from\n    }\n\n    @Transient\n    override val statusFlow = MutableStateFlow<PartDownloadStatus>(PartDownloadStatus.IDLE)\n\n    val remainingLength\n        get() = to?.let {\n            (it - current) + 1\n        }\n    val partLength\n        get() = to?.let {\n            (it - from) + 1\n        }\n\n    override val percent\n        get() = run {\n            val partLength = partLength ?: return@run null\n            (howMuchProceed().toDouble() / partLength.toDouble()) * 100\n        }?.toInt()\n\n    //    because of end inclusive completed position will be $to + 1\n    override val isCompleted: Boolean\n        get() = to?.let {\n            current == it + 1\n        } ?: false\n\n    fun setBlindAsCompleted() {\n        to = current - 1\n    }\n\n    val range\n        get() = to?.let {\n            from..it\n        }\n\n    val isBlind get() = to == null\n    override fun getID(): Long {\n        return from\n    }\n    companion object {\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/queue/DownloadQueue.kt",
    "content": "package ir.amirab.downloader.queue\n\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.db.DownloadQueuePersistedDataAccess\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.downloaditem.contexts.Queue\nimport ir.amirab.downloader.downloaditem.contexts.ResumedBy\nimport ir.amirab.downloader.downloaditem.contexts.StoppedBy\nimport ir.amirab.downloader.utils.swap\nimport ir.amirab.downloader.utils.swapped\nimport ir.amirab.util.coroutines.debounce\nimport ir.amirab.util.guardedEntry\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\n\n\nclass DownloadQueue(\n    persistedModel: QueueModel,\n    val persistedData: DownloadQueuePersistedDataAccess,\n    val downloadEvents: DownloadManagerMinimalControl,\n    val onQueueEvent: (QueueEvent) -> Unit,\n) {\n    private val scope = CoroutineScope(SupervisorJob())\n\n    //    private val mutex = Mutex()\n    private var booted = guardedEntry()\n\n    private val _queueModel = MutableStateFlow(\n        persistedModel\n    )\n    val queueModel = _queueModel.asStateFlow()\n    fun getQueueModel() = queueModel.value\n\n    // this must not change\n    val id: Long = getQueueModel().id\n    private val stopQueueOnEmpty: Boolean\n        get() = getQueueModel().stopQueueOnEmpty\n    private val maxConcurrent\n        get() = getQueueModel().maxConcurrent\n    private val scheduleTimes: ScheduleTimes\n        get() = getQueueModel().scheduledTimes\n\n    private val activeItems = mutableSetOf<Long>()\n    private val canceledItems = mutableSetOf<Long>()\n    private val trimmedItems = mutableSetOf<Long>()\n\n    private val _queueActiveFlow = MutableStateFlow(false)\n    val activeFlow = _queueActiveFlow.asStateFlow()\n    val isQueueActive: Boolean get() = activeFlow.value\n\n    fun onEvent(event: QueueEvent) {\n        this.onQueueEvent(event)\n    }\n\n    fun boot() {\n        booted.action {\n            startListenerJob()\n            setupAutoStartAndStop()\n            setupAutoSave()\n        }\n    }\n\n    private fun setupAutoSave() {\n        queueModel.onEach {\n            persist()\n        }.launchIn(scope)\n    }\n\n    private fun setupAutoStartAndStop() {\n        setUpAutoStartJob()\n        setUpAutoStopJob()\n    }\n\n    private fun setActive(v: Boolean) {\n        _queueActiveFlow.value = v\n        if (v) {\n//            println(\"start queue job\")\n        } else {\n//            println(\"stop queue job\")\n        }\n    }\n\n    private fun onDownloadCanceled(id: Long, e: Throwable) {\n        val removed = activeItems.remove(id)\n        if (isQueueActive) {\n            if (!trimmedItems.remove(id)) {\n                canceledItems.add(id)\n            }\n        }\n        shake(\n            itemChangeHappened = removed,\n        )\n    }\n\n\n    private fun onDownloadFinished(id: Long) {\n        removeFromQueue(id)\n        shake(\n            itemChangeHappened = true,\n        )\n    }\n\n    fun start() {\n        if (stopping) return\n//        println(\"on start queue\")\n        canceledItems.clear()\n        trimmedItems.clear()\n        ensureBooted()\n        setActive(true)\n//        println(\"starting\")\n        shake(\n            delayed = false\n        )\n        return\n    }\n\n    private fun ensureBooted() {\n        if (!booted.isDone()) {\n            error(\"queue is not booted!\")\n        }\n    }\n\n    private fun shake(\n        itemChangeHappened: Boolean = false,\n        delayed: Boolean = true,\n    ) {\n        if (delayed) {\n            debouncedShake(itemChangeHappened)\n        } else {\n            actualShake(itemChangeHappened)\n        }\n    }\n\n    private fun actualShake(\n        itemChangeHappened: Boolean = false\n    ): Boolean {\n//        println(\"shake queue\")\n        return when {\n            !isQueueActive -> false\n            activeItems.isEmpty() && (getDownloadableItemFromQueue() == null) -> {\n                if (stopQueueOnEmpty) {\n                    stop()\n                }\n                if (itemChangeHappened) {\n                    onEvent(QueueEvent.OnQueueBecomesEmpty(id))\n                }\n                false\n            }\n\n            else -> {\n                if (activeItems.size < maxConcurrent) {\n                    extend()\n                } else if (activeItems.size > maxConcurrent) {\n                    trim()\n                }\n                true\n            }\n        }\n    }\n\n    // Note: it mostly happens in ManualDownloadQueue. couldn't fully reproduce it here yet. adding it just for safety.\n    // If multiple downloads are canceled at the same time, multiple `shake` calls may occur.\n    // In these situations, there is an issue where other downloads might resume\n    // (even though they are already being stopped but their events have not been received yet).\n    // So we use a debounced call to ensure all events are received first.\n    val debouncedShake = scope.debounce(\n        fn = ::actualShake,\n        delayMillis = 500,\n        previousValueMerge = { wasItemChangeHappened, itemChangeHappened ->\n            wasItemChangeHappened || itemChangeHappened\n        }\n    )\n\n    private var listenerJob: Job? = null\n    private fun startListenerJob() {\n        listenerJob = downloadEvents.listOfJobsEvents.onEach {\n            if (!getQueueModel().queueItems.contains(it.downloadItem.id)) {\n                //skip this event\n                return@onEach\n            }\n//            println(\"we (${getQueueModel().name}) found ${it.downloadItem.id} in ${getQueueModel().queueItems} command ${it}\")\n            when (it) {\n                is DownloadManagerEvents.OnJobAdded -> {\n                }\n\n                is DownloadManagerEvents.OnJobCanceled -> onDownloadCanceled(\n                    it.downloadItem.id,\n                    it.e\n                )\n\n                is DownloadManagerEvents.OnJobCompleted -> onDownloadFinished(it.downloadItem.id)\n                is DownloadManagerEvents.OnJobStarted -> {}\n                is DownloadManagerEvents.OnJobStarting -> {}\n                is DownloadManagerEvents.OnJobChanged -> {}\n                is DownloadManagerEvents.OnJobRemoved -> onDownloadRemoved(it.downloadItem.id)\n            }\n        }.launchIn(scope)\n    }\n\n    private var autoStartJob: Job? = null\n    private fun cancelAutoStartJob() {\n        autoStartJob?.cancel()\n        autoStartJob = null\n    }\n\n    private fun setUpAutoStartJob() {\n        cancelAutoStartJob()\n        val scheduleTimes = scheduleTimes\n        if (scheduleTimes.enabledStartTime) {\n            autoStartJob = scope.launch {\n                val now = System.currentTimeMillis()\n                delay(scheduleTimes.getNearestTimeToStart() - now)\n                val wasActive = isQueueActive\n                onEvent(QueueEvent.OnQueueStartTimeReached(id, wasActive))\n                start()\n                //wait a little\n                delay(1000)\n                //for tomorrow\n                setUpAutoStartJob()\n            }\n        }\n    }\n\n    private var autoStopJob: Job? = null\n    private fun cancelAutoStopJob() {\n        autoStopJob?.cancel()\n        autoStopJob = null\n    }\n\n    private fun setUpAutoStopJob() {\n        cancelAutoStopJob()\n        val scheduleTimes = scheduleTimes\n        if (scheduleTimes.enabledEndTime) {\n            autoStopJob = scope.launch {\n                val now = System.currentTimeMillis()\n                delay(scheduleTimes.getNearestTimeToStop() - now)\n                val wasActive = isQueueActive\n                onEvent(QueueEvent.QueueEndTimeReached(id, wasActive))\n                stop()\n                //wait a little\n                delay(1000)\n                //for tomorrow\n                setUpAutoStopJob()\n            }\n        }\n    }\n\n\n    private fun onDownloadRemoved(id: Long) {\n        removeFromQueue(id)\n    }\n\n    fun stop() {\n        scope.launch {\n//            println(\"stopping\")\n            stopAsync()\n        }\n    }\n\n    @Volatile\n    var stopping = false\n    suspend fun stopAsync() {\n        if (stopping) return\n        setActive(false)\n        stopping = true\n        //active item is a synchronized list so we should iterate over it FAST!\n        val stopJobs = activeItems.map {\n            scope.async {\n                downloadEvents.stopJob(it, StoppedBy(me))\n            }\n        }\n        kotlin.runCatching {\n            stopJobs.awaitAll()\n        }.onFailure {\n            // should not happen!\n//            it.printStackTrace()\n        }\n        stopping = false\n    }\n\n\n    fun setScheduledTimes(\n        updater: ScheduleTimes.() -> ScheduleTimes\n    ) {\n        _queueModel.update {\n            it.copy(scheduledTimes = updater(it.scheduledTimes))\n        }\n        setupAutoStartAndStop()\n    }\n\n    fun setName(newValue: String) {\n        _queueModel.update {\n            it.copy(name = newValue)\n        }\n    }\n\n    fun setMaxConcurrent(value: Int) {\n        _queueModel.update {\n            it.copy(maxConcurrent = value)\n        }\n        shake()\n    }\n\n    fun setStopQueueOnEmpty(enabled: Boolean) {\n        _queueModel.update {\n            it.copy(\n                stopQueueOnEmpty = enabled\n            )\n        }\n        shake()\n    }\n\n    fun move(listOfIds: List<Long>, diff: Int) {\n        if (diff == 0) return\n        _queueModel.update { q ->\n            val movingIndexes = listOfIds.mapNotNull {\n                q.queueItems.indexOf(it).takeIf { index -> index != -1 }\n            }\n                //from big to small\n                .sortedDescending()\n                .let {\n                    if (diff < 0) it.reversed()\n                    else it\n                }\n            if (movingIndexes.isEmpty()) {\n                return@update q\n            }\n            val m = q.queueItems.toMutableList()\n            val queueIndices = q.queueItems.indices\n            val dontMovedPositions = mutableSetOf<Int>()\n//            println(\"moving indexes $movingIndexes\")\n            for (index in movingIndexes) {\n                val newPosition = index + diff\n                //don't move out of list index\n                if (newPosition !in queueIndices) {\n//                    println(\"we don't move index $index to $newPosition\")\n                    dontMovedPositions.add(index)\n                    continue\n                }\n                //we don't want to swap to an item that already wants swap\n                if (newPosition in dontMovedPositions) {\n//                    println(\"we don't move index $index to $newPosition cause of $dontMovedPositions\")\n                    dontMovedPositions.add(index)\n                    continue\n                }\n                m.swap(index, newPosition)\n            }\n            q.copy(\n                queueItems = m.toList()\n            )\n        }\n    }\n\n    fun moveUp(listOfIds: List<Long>) {\n        move(listOfIds, -1)\n    }\n\n    fun moveDown(listOfIds: List<Long>) {\n        move(listOfIds, 1)\n    }\n\n    fun swapOrders(order: Int, toOrder: (List<Long>) -> Int) {\n        _queueModel.update {\n            it.copy(\n                queueItems = it.queueItems.swapped(order, toOrder(it.queueItems))\n            )\n        }\n    }\n\n    fun swapQueueItem(item: Long, toPosition: (List<Long>) -> Int) {\n        _queueModel.update {\n            val q = it.queueItems\n            val currentIndex = q.indexOf(item).takeIf { it >= 0 }\n            if (currentIndex == null) {\n                it\n            } else {\n                val swapToThisPosition = toPosition(q)\n                val modifiedItems = q\n                    .swapped(currentIndex, swapToThisPosition)\n                it.copy(\n                    queueItems = modifiedItems\n                )\n            }\n\n        }\n//        println(\"going to swipe $currentIndex , ${queue.size}\")\n    }\n\n\n    private fun trim() {\n        val count = activeItems.size - maxConcurrent\n        repeat(count) {\n            if (!removeAnActiveQueueItem()) {\n                return\n            }\n        }\n    }\n\n    private val me by lazy { Queue(id) }\n    private fun removeAnActiveQueueItem(): Boolean {\n        val id = activeItems.lastOrNull() ?: return false\n        trimmedItems.add(id)\n        val result = activeItems.remove(id)\n        scope.launch { downloadEvents.stopJob(id, StoppedBy(me)) }\n        return result\n    }\n\n    private fun extend() {\n        repeat(maxConcurrent - activeItems.size) {\n            val queueItemStarted = downloadAQueueItemIfPossible()\n            if (!queueItemStarted) {\n                return\n            }\n        }\n    }\n\n    /**\n     * @return is any item from queue started ?\n     */\n    private fun downloadAQueueItemIfPossible(): Boolean {\n        return getDownloadableItemFromQueue()?.let {\n            activeItems.add(it)\n            scope.launch {\n                downloadEvents.startJob(it, ResumedBy(me))\n            }\n            true\n        } ?: false\n    }\n\n    private fun getDownloadableItemFromQueue(): Long? {\n//        println(queue)\n        return getQueueModel().queueItems.firstOrNull {\n            when {\n                it in activeItems -> {\n//                    println(\"it is not in active items\")\n                    false\n                }\n\n                it in canceledItems -> {\n//                    println(\"it is in canceled items\")\n                    false\n                }\n\n                !downloadEvents.canActivateJob(it) -> {\n//                    println(\"it is not cultivatable\")\n                    false\n                }\n\n                else -> true\n            }\n        }.also {\n//            println(\"found downloadable queue item $it\")\n        }\n    }\n\n\n    private suspend fun persist() {\n        val queue = getQueueModel()\n        persistedData.setModel(queue)\n    }\n\n    //    suspend fun swapQueueItemToEnd(item: Long){\n//        val currentIndex = queue.indexOf(item).takeIf { it > 0 } ?: return\n//        queue.removeAt(currentIndex)\n//        queue.add(item)\n//        saveQueue()\n//    }\n\n    fun getOrder(item: Long): Int {\n        return getQueueModel().queueItems.indexOf(item)\n    }\n\n    fun getQueueItemFromOrder(order: Int): Long {\n        return getQueueModel().queueItems.toList()[order]\n    }\n\n    suspend fun addToQueue(item: Long) {\n        _queueModel.update {\n            it.copy(\n                queueItems = it.queueItems\n                    .plus(item)\n                    .distinct()\n            )\n        }\n        shake(\n            itemChangeHappened = true\n        )\n    }\n\n    fun clearQueue() {\n        _queueModel.update {\n            it.copy(queueItems = emptyList())\n        }\n        activeItems.clear()\n        canceledItems.clear()\n        trimmedItems.clear()\n    }\n\n    fun removeFromQueue(ids: List<Long>) {\n        _queueModel.update {\n            it.copy(\n                queueItems = it.queueItems.filter {\n                    it !in ids\n                }.distinct()\n            )\n        }\n        for (id in ids) {\n            activeItems.remove(id)\n            canceledItems.remove(id)\n            trimmedItems.remove(id)\n        }\n    }\n\n    fun removeFromQueue(id: Long) {\n        removeFromQueue(listOf(id))\n    }\n\n    fun dispose() {\n        cancelAutoStartJob()\n        cancelAutoStopJob()\n        listenerJob?.cancel()\n        scope.cancel()\n        listenerJob = null\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/queue/ManualDownloadQueue.kt",
    "content": "package ir.amirab.downloader.queue\n\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.downloaditem.contexts.ResumedBy\nimport ir.amirab.downloader.downloaditem.contexts.StoppedBy\nimport ir.amirab.downloader.downloaditem.contexts.User\nimport ir.amirab.downloader.utils.swap\nimport ir.amirab.downloader.utils.swapped\nimport ir.amirab.util.coroutines.debounce\nimport ir.amirab.util.guardedEntry\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport java.util.Collections\n\n/**\n * this queue is used to limit global concurrent download limit\n */\n\nclass ManualDownloadQueue(\n    private val downloadEvents: DownloadManagerMinimalControl,\n    private val scope: CoroutineScope,\n) {\n\n    private var booted = guardedEntry()\n\n    // how many downloads can be active at the same time\n    // 0 or fewer means unlimited!\n    // this is protected with a set method\n    private var maxConcurrent = Int.MAX_VALUE\n\n    // downloads that are currently active\n    private val activeItemsFlow: MutableStateFlow<List<Long>> = MutableStateFlow(emptyList())\n\n    // just a shortcut\n    private val activeItems get() = activeItemsFlow.value\n\n    // this is called from the pause function which can be called from any thread\n    // so we should make it thread safe\n    private val trimmedItems = Collections.synchronizedCollection(mutableSetOf<Long>())\n    private var totalItemsFlow: MutableStateFlow<List<Long>> = MutableStateFlow(emptyList())\n    val pendingItems = combine(\n        activeItemsFlow.map { it.toSet() },\n        totalItemsFlow,\n    ) { active, total ->\n        total - active\n    }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n\n    fun boot() {\n        booted.action {\n            startListenerJob()\n        }\n    }\n\n    private fun removeActiveItem(id: Long): Boolean {\n        var removed = false\n        activeItemsFlow.update {\n            val size = it.size\n            val newList = it - id\n            if (size != newList.size) {\n                removed = true\n            }\n            newList\n        }\n        return removed\n    }\n\n    private fun addAnActiveItem(id: Long) {\n        return activeItemsFlow.update {\n            (it + id).distinct()\n        }\n    }\n\n    private fun clearActiveItems() {\n        activeItemsFlow.update { emptyList() }\n    }\n\n    private fun clearTotalItems() {\n        totalItemsFlow.update { emptyList() }\n    }\n\n    private fun onDownloadCanceled(id: Long, e: Throwable) {\n        if (trimmedItems.remove(id)) {\n            // it was trimmed so we shouldn't remove it from queue\n            // it should be processed after\n            removeActiveItem(id)\n        } else {\n            removeFromQueue(id)\n        }\n        shake()\n    }\n\n\n    private fun onDownloadFinished(id: Long) {\n        removeFromQueue(id)\n        shake()\n    }\n\n    private fun ensureBooted() {\n        if (booted.isDone()) {\n            error(\"headless queue is not booted!\")\n        }\n    }\n\n    private fun actualShake(): Boolean {\n        val activeItems = activeItems\n        val maxConcurrent = maxConcurrent\n        if (activeItems.size < maxConcurrent) {\n            extend()\n        } else if (activeItems.size > maxConcurrent) {\n            trim()\n        }\n        return true\n    }\n\n    // If multiple downloads are canceled at the same time, multiple `shake` calls may occur.\n    // In these situations, there is an issue where other downloads might resume\n    // (even though they are already being stopped but their events have not been received yet).\n    // So we use a debounced call to ensure all events are received first.\n    private val shakeDebounce = scope.debounce(\n        ::actualShake,\n        500, // this should be enough even for slow devices\n    )\n\n    private fun shake(delayed: Boolean = true) {\n        if (delayed) {\n            shakeDebounce()\n        } else {\n            actualShake()\n        }\n    }\n\n    private var listenerJob: Job? = null\n    private fun startListenerJob() {\n        listenerJob = downloadEvents.listOfJobsEvents.onEach {\n            if (!totalItemsFlow.value.contains(it.downloadItem.id)) {\n                //skip this event\n                return@onEach\n            }\n            when (it) {\n                is DownloadManagerEvents.OnJobAdded -> {\n                }\n\n                is DownloadManagerEvents.OnJobCanceled -> onDownloadCanceled(\n                    it.downloadItem.id,\n                    it.e\n                )\n\n                is DownloadManagerEvents.OnJobCompleted -> onDownloadFinished(it.downloadItem.id)\n                is DownloadManagerEvents.OnJobStarted -> {}\n\n                is DownloadManagerEvents.OnJobStarting -> {\n                    if (it.context[ResumedBy]?.by != me) {\n                        // someone else resumed the download\n                        // we just remove it\n                        removeFromQueue(it.downloadItem.id)\n                    }\n                }\n\n                is DownloadManagerEvents.OnJobChanged -> {}\n                is DownloadManagerEvents.OnJobRemoved -> onDownloadRemoved(it.downloadItem.id)\n            }\n        }.launchIn(scope)\n    }\n\n    private fun onDownloadRemoved(id: Long) {\n        removeFromQueue(id)\n    }\n\n\n    fun setMaxConcurrent(value: Int) {\n        maxConcurrent = if (value <= 0) {\n            Int.MAX_VALUE\n        } else {\n            value\n        }\n        shake()\n    }\n\n    fun move(listOfIds: List<Long>, diff: Int) {\n        if (diff == 0) return\n        totalItemsFlow.update { queueItems ->\n            val movingIndexes = listOfIds.mapNotNull {\n                queueItems.indexOf(it).takeIf { index -> index != -1 }\n            }\n                //from big to small\n                .sortedDescending()\n                .let {\n                    if (diff < 0) it.reversed()\n                    else it\n                }\n            if (movingIndexes.isEmpty()) {\n                return@update queueItems\n            }\n            val m = queueItems.toMutableList()\n            val queueIndices = queueItems.indices\n            val dontMovedPositions = mutableSetOf<Int>()\n//            println(\"moving indexes $movingIndexes\")\n            for (index in movingIndexes) {\n                val newPosition = index + diff\n                //don't move out of list index\n                if (newPosition !in queueIndices) {\n//                    println(\"we don't move index $index to $newPosition\")\n                    dontMovedPositions.add(index)\n                    continue\n                }\n                //we don't want to swap to an item that already wants swap\n                if (newPosition in dontMovedPositions) {\n//                    println(\"we don't move index $index to $newPosition cause of $dontMovedPositions\")\n                    dontMovedPositions.add(index)\n                    continue\n                }\n                m.swap(index, newPosition)\n            }\n            m.toList()\n        }\n    }\n\n    fun moveUp(listOfIds: List<Long>) {\n        move(listOfIds, -1)\n    }\n\n    fun moveDown(listOfIds: List<Long>) {\n        move(listOfIds, 1)\n    }\n\n    fun swapOrders(order: Int, toOrder: (List<Long>) -> Int) {\n        totalItemsFlow.update { items ->\n            items.swapped(order, toOrder(items))\n        }\n    }\n\n    private fun trim() {\n        while (activeItems.size > maxConcurrent) {\n            if (!removeAnActiveQueueItemForTrimming()) {\n                return\n            }\n        }\n    }\n\n    private val me = User\n    private fun removeAnActiveQueueItemForTrimming(): Boolean {\n        val id = activeItems.lastOrNull() ?: return false\n        trimmedItems.add(id)\n        val result = removeActiveItem(id)\n        scope.launch { downloadEvents.stopJob(id, StoppedBy(me)) }\n        return result\n    }\n\n    private fun extend() {\n        while (maxConcurrent > activeItems.size) {\n            val queueItemStarted = downloadAQueueItemIfPossible()\n            if (!queueItemStarted) {\n                return\n            }\n        }\n    }\n\n    /**\n     * @return is any item from queue started ?\n     */\n    private fun downloadAQueueItemIfPossible(): Boolean {\n        val downloadableItemFromQueue = getAnInactiveITemFromTheQueue()\n\n        return downloadableItemFromQueue?.let {\n            addAnActiveItem(it)\n            scope.launch {\n                downloadEvents.startJob(it, ResumedBy(me))\n            }\n            true\n        } ?: false\n    }\n\n    private fun getAnInactiveITemFromTheQueue(): Long? {\n        while (true) {\n            val activeItems = activeItems\n            val item = totalItemsFlow.value\n                .firstOrNull { it !in activeItems }\n            if (item == null) {\n                // no item returning now!\n                return null\n            }\n            if (!downloadEvents.canActivateJob(item)) {\n                // finished or in status that we can't use it anymore\n                // remove it!\n                removeFromQueue(item)\n                continue\n            }\n            return item\n        }\n    }\n\n\n    fun getOrder(item: Long): Int {\n        return totalItemsFlow.value.indexOf(item)\n    }\n\n    fun getQueueItemFromOrder(order: Int): Long? {\n        return totalItemsFlow.value.getOrNull(order)\n    }\n\n    fun clearQueue() {\n        trimmedItems.clear()\n        clearActiveItems()\n        clearTotalItems()\n    }\n\n    fun removeFromQueue(ids: Set<Long>) {\n        totalItemsFlow.update {\n            it.filter { id ->\n                id !in ids\n            }\n        }\n        activeItemsFlow.update {\n            it.filter {\n                it !in ids\n            }\n        }\n        trimmedItems.removeAll(ids)\n    }\n\n    fun removeFromQueue(id: Long) {\n        return removeFromQueue(setOf(id))\n    }\n\n    fun resume(item: Long) {\n        totalItemsFlow.update {\n            it + item\n        }\n        shake(\n            // immediately shake the queue\n            delayed = false\n        )\n    }\n\n    // called by user\n    suspend fun pause(item: Long) {\n        // this should be removed here as it indicates that user manually paused it\n        // so the trimmed item list should not contain it anymore\n        trimmedItems.remove(item)\n        downloadEvents.stopJob(item, StoppedBy(me))\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/queue/QueueEvent.kt",
    "content": "package ir.amirab.downloader.queue\n\nsealed interface QueueEvent {\n    val queueId: Long\n\n    data class QueueEndTimeReached(\n        override val queueId: Long,\n        val wasActive: Boolean,\n    ) : QueueEvent\n\n    data class OnQueueBecomesEmpty(\n        override val queueId: Long,\n    ) : QueueEvent\n\n    data class OnQueueStartTimeReached(\n        override val queueId: Long,\n        val wasActive: Boolean,\n    ) : QueueEvent\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/queue/QueueManager.kt",
    "content": "package ir.amirab.downloader.queue\n\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport ir.amirab.downloader.db.IDownloadQueueDatabase\nimport ir.amirab.downloader.db.DownloadQueuePersistedDataAccess\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.util.suspendGuardedEntry\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\nobject DefaultQueueInfo {\n    const val ID = 0L\n    const val NAME = \"Main\"\n}\n\nclass QueueManager(\n    private val queueDb: IDownloadQueueDatabase,\n    private val listOfJobs: DownloadManagerMinimalControl,\n) {\n    companion object {\n\n        // we save this ids maybe later we want to add some queues\n        const val RESERVED_UNTIL_QUEUE_ID = 10L\n    }\n\n    val queues = MutableStateFlow(\n        emptyList<DownloadQueue>()\n    )\n\n    private val _queueEvent = MutableSharedFlow<QueueEvent>(\n        // send events without suspending\n        extraBufferCapacity = 64,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST,\n    )\n    val queueEvents: SharedFlow<QueueEvent> = _queueEvent.asSharedFlow()\n    private fun onQueueEvent(queueEvent: QueueEvent) {\n        _queueEvent.tryEmit(queueEvent)\n    }\n\n\n    private suspend fun addDefaultQueue() {\n        val queueModel = QueueModel(\n            id = DefaultQueueInfo.ID,\n            name = DefaultQueueInfo.NAME,\n        )\n//        println(\"creating default queue\")\n        queueDb.addQueue(queueModel)\n        val queue = createQueue(queueModel)\n        queues.update { currentList ->\n            buildList {\n                add(queue)\n                addAll(currentList)\n            }\n        }\n        queue.boot()\n    }\n\n    suspend fun addQueue(\n        name: String,\n    ) {\n        val maxId = queueDb\n            .getAllQueueIds()\n            .maxOrNull()\n            ?.coerceAtLeast(RESERVED_UNTIL_QUEUE_ID) ?: RESERVED_UNTIL_QUEUE_ID\n        // this is reserved id\n        val queueModel = QueueModel(\n            id = maxId + 1,\n            name = name,\n        )\n        queueDb.addQueue(\n            queueModel\n        )\n        val queue = createQueue(queueModel)\n        queues.update {\n            it.plus(queue)\n        }\n        queue.boot()\n    }\n\n    suspend fun deleteQueue(\n        queue: DownloadQueue\n    ) {\n        if (queue.isMainQueue()) {\n            return\n        }\n        queue.dispose()\n        queueDb.deleteQueue(queue.id)\n        queues.update {\n            it.filter {\n                it.id != queue.id\n            }\n        }\n    }\n\n    suspend fun deleteQueue(\n        id: Long,\n    ) {\n        val foundQueue = queues.value.find { it.id == id }\n        foundQueue?.let {\n            deleteQueue(foundQueue)\n        }\n    }\n\n    private var booted = suspendGuardedEntry()\n    private fun ensureBooted() {\n        require(booted.isDone()) {\n            \"please first boot QueueManager\"\n        }\n    }\n\n    suspend fun boot() {\n        booted.action {\n            val queueModels = queueDb\n                .getAllQueues()\n            val dbQueues = queueModels.map {\n                createQueue(it)\n            }\n            for (queue in dbQueues) {\n                queue.boot()\n            }\n            queues.update { dbQueues }\n            val ids = queueModels.map { it.id }\n            if (DefaultQueueInfo.ID !in ids) {\n                addDefaultQueue()\n            }\n        }\n    }\n\n    fun getMainQueue(): DownloadQueue {\n        ensureBooted()\n        return requireNotNull(\n            queues.value.find {\n                it.id == DefaultQueueInfo.ID\n            }\n        ) { \"we can't find main queue\" }\n    }\n\n    private fun createQueue(queueModel: QueueModel): DownloadQueue {\n        return DownloadQueue(\n            persistedData = QueueInfoPersistedData(\n                queueDb,\n                queueModel.id\n            ),\n            downloadEvents = listOfJobs,\n            persistedModel = queueModel,\n            onQueueEvent = ::onQueueEvent,\n        )\n    }\n\n    fun getAll(): List<DownloadQueue> {\n        return queues.value\n    }\n\n    fun getQueue(queue: Long): DownloadQueue {\n        return requireNotNull(\n            queues.value.find {\n                it.id == queue\n            }\n        )\n    }\n\n    fun canDelete(queue: Long): Boolean {\n        return queue != DefaultQueueInfo.ID\n        //        return if (queue in 0..RESERVED_UNTIL_QUEUE_ID) {\n//            false\n//        } else true\n    }\n\n    fun isItemInQueue(downloadId: Long): Boolean {\n        return findItemInQueue(downloadId) != null\n    }\n\n    fun findItemInQueue(downloadId: Long): Long? {\n        for (queue in queues.value) {\n            for (queueItem in queue.getQueueModel().queueItems) {\n                if (downloadId == queueItem) {\n                    return queue.id\n                }\n            }\n        }\n        return null\n    }\n\n    suspend fun addToQueue(\n        queueId: Long,\n        downloadId: Long,\n    ) {\n        val foundInQueue = findItemInQueue(downloadId = downloadId)\n        if (foundInQueue == queueId) {\n            //already in same queue\n            return\n        }\n        if (foundInQueue != null) {\n            getQueue(foundInQueue).removeFromQueue(downloadId)\n        }\n        getQueue(queueId).addToQueue(downloadId)\n    }\n\n    suspend fun addToQueue(queueId: Long, downloadIds: List<Long>) {\n        downloadIds.forEach {\n            addToQueue(queueId, it)\n        }\n    }\n\n    fun clearQueue(queueId: Long) {\n        val queue = getQueue(queueId)\n        queue.clearQueue()\n    }\n}\n\nprivate class QueueInfoPersistedData(\n    val db: IDownloadQueueDatabase,\n    val id: Long,\n) : DownloadQueuePersistedDataAccess {\n    var cached: QueueModel? = null\n    val lock = Mutex()\n    override suspend fun getModel(): QueueModel {\n        if (cached == null) {\n            cached = db.getQueue(id)\n        }\n//        println(\"getModel() == $cached\")\n        return cached!!\n    }\n\n    override suspend fun setModel(model: QueueModel) {\n        if (model == cached) {\n            //nothing to update\n//            println(\"Noting to update\")\n            return\n        }\n        lock.withLock {\n            db.updateQueue(model)\n//            println(\"setModel() == $model\")\n            cached = model\n        }\n    }\n}\n\nprivate fun QueueManager.getActiveOrInactiveQueues(\n    active: Boolean,\n): Flow<List<DownloadQueue>> {\n    return queues.flatMapLatest { latestQueues ->\n        if (latestQueues.isEmpty()) {\n            flowOf(emptyList())\n        } else {\n            combine(\n                latestQueues.map { it.activeFlow }\n            ) {\n                it.mapIndexedNotNull { index, value ->\n                    val shouldAdd = if (active) value else !value\n                    if (shouldAdd) {\n                        latestQueues[index]\n                    } else {\n                        null\n                    }\n                }\n            }\n        }\n    }\n}\n\nfun QueueManager.activeQueuesFlow(): Flow<List<DownloadQueue>> {\n    return getActiveOrInactiveQueues(true)\n}\n\nfun QueueManager.inactiveQueuesFlow(): Flow<List<DownloadQueue>> {\n    return getActiveOrInactiveQueues(false)\n}\n\nfun QueueManager.queueModelsFlow(): Flow<List<QueueModel>> {\n    return queues.flatMapLatest { queues ->\n        if (queues.isEmpty()) {\n            flowOf(emptyList())\n        } else {\n            combine(\n                queues.map { it.queueModel }\n            ) {\n                it.toList()\n            }\n        }\n    }\n}\n\nfun DownloadQueue.isMainQueue(): Boolean {\n    return DefaultQueueInfo.ID == id\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/queue/ScheduleTimes.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage ir.amirab.downloader.queue\n\nimport kotlinx.datetime.DateTimeUnit\nimport kotlinx.datetime.DayOfWeek\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.LocalTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.plus\nimport kotlinx.datetime.toInstant\nimport kotlinx.datetime.toLocalDateTime\nimport kotlinx.serialization.Serializable\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\n@Serializable\ndata class ScheduleTimes(\n    val daysOfWeek: Set<DayOfWeek>,\n    val startTime: LocalTime,\n    val endTime: LocalTime,\n    val enabledStartTime: Boolean,\n    val enabledEndTime: Boolean,\n) {\n    init {\n        require(daysOfWeek.isNotEmpty()) {\n            \"we have always have one day\"\n        }\n    }\n\n    companion object {\n        fun default() = ScheduleTimes(\n            daysOfWeek = DayOfWeek.entries.toSet(),\n            startTime = LocalTime(2, 30),\n            endTime = LocalTime(7, 30),\n            enabledStartTime = false,\n            enabledEndTime = false,\n        )\n    }\n\n    private fun containsThisDay(day: DayOfWeek): Boolean {\n        return day in daysOfWeek\n    }\n\n\n    private fun getNearestDayOfWork(forTime: LocalTime): Int {\n        val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())\n        var currentDay = now.dayOfWeek\n        val currentTime = now.time\n        var count = 0\n\n        while (true) {\n            if (containsThisDay(currentDay)) {\n                if (count == 0) {\n                    if (currentTime < forTime) {\n                        return count // ==0 today\n                    }\n                    //else => today's start time has been passed so we don't want today\n                    //we continue for tomorrow\n                } else {\n                    return count\n                }\n            }\n            currentDay = currentDay.plus(1)\n            count++\n            if (count > 7) {\n                error(\"there is a bug in our code stoping loop\")\n            }\n        }\n\n    }\n\n    fun getNearestTimeToStart(): Long {\n        val now = Clock.System.now()\n\n        val nextTime = now\n            .plus(\n                getNearestDayOfWork(startTime),\n                DateTimeUnit.DAY,\n                TimeZone.currentSystemDefault()\n            )\n            .toLocalDateTime(TimeZone.currentSystemDefault())\n            .let {\n                LocalDateTime(it.date, startTime)\n            }.toInstant(TimeZone.currentSystemDefault())\n        return nextTime.toEpochMilliseconds()\n    }\n\n    fun getNearestTimeToStop(): Long {\n        val stopTime = this.endTime\n        val now = Clock.System.now()\n        val nextTime = now\n            .plus(\n                getNearestDayOfWork(stopTime),\n                DateTimeUnit.DAY,\n                TimeZone.currentSystemDefault()\n            )\n            .toLocalDateTime(TimeZone.currentSystemDefault())\n            .let {\n                LocalDateTime(it.date, stopTime)\n            }.toInstant(TimeZone.currentSystemDefault())\n        return nextTime.toEpochMilliseconds()\n    }\n}\n\nprivate fun DayOfWeek.plus(days: Int): DayOfWeek {\n    val entries = DayOfWeek.entries\n    val index = (entries.indexOf(this) + days) % entries.size\n    return entries[index]\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/CallAwait.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport okhttp3.Call\nimport okhttp3.Response\nimport okhttp3.coroutines.executeAsync\n\nsuspend fun Call.await(): Response {\n    return executeAsync()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/CollectionUtils.kt",
    "content": "package ir.amirab.downloader.utils\n\ninternal fun <T>MutableList<T>.swap(\n    index: Int,toPosition: Int\n): MutableList<T> =apply {\n    val p=set(toPosition,this[index])\n    set(index,p)\n}\n\ninternal fun <T>List<T>.swapped(index:Int, toPosition: Int): List<T> {\n    val l=toMutableList()\n    l.swap(index,toPosition)\n    return l.toList()\n}\ninternal fun <T>Set<T>.swapped(a:T, b: T): Set<T> {\n    val l=toMutableList()\n    val indexA=indexOf(a)\n    val indexB=indexOf(b)\n    val tmp=l.set(indexB,l[indexA])\n    l.set(indexA,tmp)\n    return l.toList().toSet()\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/DuplicateFilter.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport java.io.File\n\ninterface DuplicateDownloadFilter {\n    fun isDuplicate(downloadItem: IDownloadItem): Boolean\n}\n\n// I moved this logic here because it used multiple times\nclass DuplicateFilterByPath(\n    private val file: File,\n) : DuplicateDownloadFilter {\n    override fun isDuplicate(downloadItem: IDownloadItem): Boolean {\n        return file == File(downloadItem.folder, downloadItem.name)\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/EmptyFileCreator.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport ir.amirab.downloader.exception.NoSpaceInStorageException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.io.RandomAccessFile\n\nclass EmptyFileCreator(\n    private val diskStat: IDiskStat,\n    private val useSparseFile: () -> Boolean\n) {\n    private fun canWeUseSparse(file: File): Boolean {\n        return useSparseFile() && SparseFile.canWeCreateSparseFile(file)\n    }\n\n    /**\n     * @param length must be -1 , or positive\n     */\n    suspend fun prepareFile(\n        file: File,\n        length: Long,\n        onProgressUpdate: (percent: Int?) -> Unit,\n    ) {\n        require(length >= -1) {\n            \"length must be -1 , or positive value but we got ${length}\"\n        }\n        withContext(Dispatchers.IO) {\n\n            val canWeUseSparse = canWeUseSparse(file)\n            onProgressUpdate(0)\n            if (length == -1L) {\n                RandomAccessFile(file, \"rw\").use {\n                    it.setLength(0)\n                }\n                onProgressUpdate(100)\n                return@withContext\n            }\n            val remainingSpace = diskStat.getRemainingSpace(file.parentFile)\n            if (file.exists()) {\n                val currentLength = file.length()\n                val requiredLength = length - currentLength\n                if (remainingSpace < requiredLength) {\n                    throw NoSpaceInStorageException(remainingSpace, requiredLength)\n                }\n\n                when {\n                    currentLength > length -> {\n                        RandomAccessFile(file, \"rw\").use {\n                            it.setLength(length)\n                        }\n                        onProgressUpdate(100)\n                        return@withContext\n                    }\n\n                    currentLength < length -> {\n                        if (canWeUseSparse) {\n                            if (!file.delete()) {\n                                throw IOException(\"can't delete file\")\n                            }\n                            if (SparseFile.createSparseFile(file)) {\n                                onProgressUpdate(null)\n                                writeAtLast(file, length)\n                            } else {\n                                file.createNewFile()\n                                fillOutput(file, length, onProgressUpdate)\n                            }\n                        } else {\n                            fillOutput(file, length, onProgressUpdate)\n                        }\n                        onProgressUpdate(100)\n                        return@withContext\n                    }\n\n                    else -> {\n                        onProgressUpdate(100)\n                        return@withContext\n                    }\n                }\n            } else {\n                if (remainingSpace < length) {\n                    throw NoSpaceInStorageException(remainingSpace, length)\n                }\n                if (canWeUseSparse && SparseFile.createSparseFile(file)) {\n                    onProgressUpdate(null)\n                    writeAtLast(file, length)\n                } else {\n                    file.createNewFile()\n                    fillOutput(file, length, onProgressUpdate)\n                }\n                onProgressUpdate(100)\n            }\n        }\n    }\n\n    /**\n     * manually write a single byte to the last of file!\n     * if the sparse is not supported for the file, at least\n     * waits for OS to create empty file for us\n     */\n    private fun writeAtLast(file: File, length: Long) {\n        RandomAccessFile(file, \"rw\").use {\n            it.seek(length - 1)\n            it.write(0)\n        }\n    }\n\n    private suspend fun fillOutput(outputFile: File, length: Long, onProgressUpdate: (percent: Int) -> Unit) {\n        val much = length - outputFile.length()\n        val remainingSpace = diskStat.getRemainingSpace(outputFile.parentFile)\n        if (remainingSpace < much) {\n            throw NoSpaceInStorageException(remainingSpace, much)\n        }\n//        println(\"how much to be appended $much\")\n        withContext(Dispatchers.IO) {\n            FileOutputStream(outputFile, true).use {\n                val buffer = ByteArray(DEFAULT_BUFFER_SIZE)\n                var written = 0L\n                while (isActive) {\n                    val writeInThisLoop = if (much - written > buffer.size) {\n                        buffer.size\n                    } else {\n                        much - written\n                    }.toInt()\n                    if (writeInThisLoop == 0) break\n//                println(writeInThisLoop)\n                    it.write(buffer, 0, writeInThisLoop)\n                    written += writeInThisLoop\n                    onProgressUpdate(calcPercent(written, much))\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/ExceptionUtils.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport ir.amirab.downloader.exception.UnSuccessfulResponseException\nimport kotlinx.coroutines.CancellationException\nimport java.io.InterruptedIOException\nimport java.net.SocketException\nimport java.net.SocketTimeoutException\nimport java.net.UnknownHostException\n\nobject ExceptionUtils {\n    fun isNormalCancellation(e: Throwable): Boolean {\n        return e is CancellationException\n    }\n\n    fun isIOInterrupted(e: Throwable): Boolean {\n        return e is InterruptedIOException\n    }\n\n    fun isNetworkError(e: Throwable): Boolean {\n        return e is UnknownHostException ||\n                e is SocketException ||\n                e is SocketTimeoutException\n    }\n\n    fun isResponseError(e: Throwable): Boolean {\n        return e is UnSuccessfulResponseException\n    }\n}\n\ninline fun <T : Throwable> T.throwIf(condition: (T) -> Boolean) {\n    if (condition(this)) {\n        throw this\n    }\n}\ninline fun <T : Throwable> T.throwIfCancelled() {\n    throwIf { ExceptionUtils.isNormalCancellation(this) }\n}\n\n\nfun Throwable.printStackIfNOtUsual() {\n    if (\n        ExceptionUtils.isNormalCancellation(this) ||\n        ExceptionUtils.isNetworkError(this) ||\n        ExceptionUtils.isIOInterrupted(this) ||\n        ExceptionUtils.isResponseError(this)\n    ) {\n        return\n    }\n    printStackTrace()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/FileNameUtil.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.isActive\nimport java.io.File\n\nobject FileNameUtil {\n    /**\n     * make sure to validate name before using this function\n     */\n    fun getExtensionOrNull(filename: String): String? {\n        return filename\n            .substringAfterLast('.', \"\")\n            .takeIf { it.isNotEmpty() }\n    }\n\n    fun numberedIfExists(filename: File): Flow<File> {\n        return flow {\n            if (!filename.exists()) {\n                emit(filename)\n            }\n            val ext = filename.extension\n                .takeIf { it.isNotEmpty() }\n                ?.let { \".$it\" }.orEmpty()\n            val name = filename.nameWithoutExtension\n            var counter = 1\n            while (currentCoroutineContext().isActive) {\n                val newFile = filename.parentFile.resolve(\n                    \"${name}_${counter}${ext}\"\n                )\n                if (!newFile.exists()) {\n                    emit(newFile)\n                }\n                counter++\n            }\n        }\n    }\n\n    fun replaceExtension(filename: String, newExtension: String, appendIfNotExists: Boolean = true): String {\n        val ext = getExtensionOrNull(filename) ?: if (appendIfNotExists) {\n            return \"$filename.$newExtension\"\n        } else {\n            return filename\n        }\n        val filenameWithoutExtension = filename.dropLast(ext.length)\n        return \"$filenameWithoutExtension$newExtension\"\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/FlowUtils.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.isActive\n\nfun intervalFlow(interval: Long) = flow {\n    while (currentCoroutineContext().isActive) {\n        emit(Unit)\n        delay(interval)\n    }\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/IDistStat.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.io.File\n\ninterface IDiskStat {\n    fun getRemainingSpace(path: File): Long\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/LockList.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.concurrent.ConcurrentHashMap\n\nclass LockList<T> {\n    private data class LockWithCounter(\n        val lock: Any = Any(),\n        var counter: Int = 1,\n    )\n\n    private val locks: ConcurrentHashMap<T, LockWithCounter> = ConcurrentHashMap()\n    fun <R> withLock(item: T, block: (T) -> R): R {\n        val itemLock = locks.compute(item) { k, existing ->\n            if (existing == null) {\n                return@compute LockWithCounter()\n            }\n            existing.counter++\n            existing\n        }!!\n        try {\n            return synchronized(itemLock.lock) {\n                block(item)\n            }\n        } finally {\n            locks.compute(item) { k, existing ->\n                if (existing == null) {\n                    return@compute null\n                }\n                if ((--existing.counter) == 0) {\n                    return@compute null\n                }\n                existing\n            }\n        }\n    }\n}\n\nclass SuspendLockList<T> {\n    private data class LockWithCounter(\n        val lock: Mutex = Mutex(),\n        var counter: Int = 1,\n    )\n\n    private val locks: ConcurrentHashMap<T, LockWithCounter> = ConcurrentHashMap()\n    suspend fun <R> withLock(item: T, block: suspend (T) -> R): R {\n        val itemLock = locks.compute(item) { key, existing ->\n            if (existing == null) {\n                return@compute LockWithCounter()\n            }\n            existing.counter++\n            existing\n        }!!\n        return try {\n            itemLock.lock.withLock {\n                block(item)\n            }\n        } finally {\n            locks.compute(item) { key, existing ->\n                if (existing == null) {\n                    return@compute null\n                }\n                if ((--existing.counter) == 0) {\n                    return@compute null\n                }\n                existing\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/Logger.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.util.logging.Logger\n\ninline fun <reified T> T.thisLogger(): Logger {\n    return Logger.getLogger(T::class.qualifiedName)\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/NumUtil.kt",
    "content": "package ir.amirab.downloader.utils\n\nfun calcPercent(proceed: Long, contentLength: Long): Int {\n    return ((proceed.toDouble() / contentLength.toDouble()) * 100).toInt()\n}\n\nfun calcPercent(proceed: Int, contentLength: Int): Int {\n    return ((proceed.toDouble() / contentLength.toDouble()) * 100).toInt()\n}\n\nfun calcPercent(proceed: Double, contentLength: Double): Int {\n    return ((proceed / contentLength) * 100).toInt()\n}\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/OnDuplicateStrategy.kt",
    "content": "package ir.amirab.downloader.utils\n\nenum class OnDuplicateStrategy {\n    AddNumbered,\n    OverrideDownload,\n    Abort,;\n\n    companion object{\n        fun default() = AddNumbered\n    }\n}\nfun OnDuplicateStrategy?.orDefault() = this?:OnDuplicateStrategy.default()"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/SparseFile.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.nio.file.OpenOption\nimport java.nio.file.StandardOpenOption\nimport kotlin.io.path.fileStore\n\ninterface ISparseFile {\n    fun createSparseFile(file: File): Boolean\n    fun canWeCreateSparseFile(file: File): Boolean\n}\n\nexpect object SparseFile : ISparseFile\n"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/SplitToRange.kt",
    "content": "\npackage ir.amirab.downloader.utils\n\nfun splitToRange(size: Long, minPartSize: Long, maxPartCount: Long): List<LongRange> {\n    require(size >= 1) {\n        \"size must be >=1 passed :$size\"\n    }\n    require(minPartSize >= 1) {\n        \"minPartSize must be >=1 passed :$minPartSize\"\n    }\n    require(maxPartCount >= 1) {\n        \"maxPartCount must be >=1 passed :$maxPartCount\"\n    }\n\n    val minParts = (size + minPartSize - 1) / minPartSize // round up division\n    val actualPartCount = minOf(maxPartCount, minParts)\n    val idealPartSize = size / actualPartCount\n    val ranges = mutableListOf<LongRange>()\n    var start = 0L\n    var end = 0L\n    for (i in 1..actualPartCount) {\n        end = start + idealPartSize - 1\n        if (i <= size % actualPartCount) {\n            end++\n        }\n        ranges.add(start..end)\n        start = end + 1\n    }\n    return ranges\n}"
  },
  {
    "path": "downloader/core/src/commonMain/kotlin/ir/amirab/downloader/utils/TimeUtils.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nobject TimeUtils {\n    fun convertLastModifiedHeaderToTimestamp(lastModified: String): Long {\n        // Define the format of the Last-Modified header\n        val dateFormat = SimpleDateFormat(\"EEE, dd MMM yyyy HH:mm:ss z\", Locale.US)\n\n        // Parse the date string into a Date object\n        val date = dateFormat.parse(lastModified)\n\n        // Convert the Date object to a timestamp (milliseconds since epoch)\n        return date?.time ?: throw IllegalArgumentException(\"Invalid Last-Modified header\")\n    }\n}"
  },
  {
    "path": "downloader/core/src/desktopMain/kotlin/ir/amirab/downloader/utils/SparseFile.desktop.kt",
    "content": "package ir.amirab.downloader.utils\n\nimport java.io.File\nimport java.nio.file.Files\nimport java.nio.file.OpenOption\nimport java.nio.file.StandardOpenOption\nimport kotlin.io.path.fileStore\n\nactual object SparseFile : ISparseFile {\n    private val fileSystemsSupportingSparseFiles = listOf(\n        // Windows\n        \"NTFS\", \"ReFS\",\n        // Linux / Unix\n        \"ext4\", \"ext3\", \"ext2\",\n        \"XFS\", \"Btrfs\", \"ZFS\", \"ReiserFS\", \"JFS\", \"F2FS\",\n        \"UFS\", \"UFS2\", \"tmpfs\", \"OverlayFS\",\n        // macOS\n        \"APFS\", \"HFS+\",\n        // Network file systems (if server supports sparse files)\n        \"SMB\", \"CIFS\", \"NFS\", \"NFSv4\",\n    )\n        .map { it.lowercase() }\n        .toSet()\n\n    override fun createSparseFile(file: File): Boolean {\n        if (!file.exists()) {\n            val options = arrayOf<OpenOption>(\n                StandardOpenOption.WRITE,\n                StandardOpenOption.CREATE_NEW,\n                StandardOpenOption.SPARSE\n            )\n            return runCatching {\n                Files.newByteChannel(\n                    file.toPath(),\n                    *options,\n                ).use {}\n                true\n            }.getOrElse { false }\n        }\n        return false\n    }\n\n    /**\n     * I assume that its parent are created before so make sure of that\n     */\n    override fun canWeCreateSparseFile(file: File): Boolean {\n        return kotlin.runCatching {\n            val nearestFileExist = file.findNearestExistingFile() ?: return false\n            val type = nearestFileExist\n                .toPath()\n                .fileStore()\n                .type()\n                .lowercase()\n            // both must be lowercase\n            fileSystemsSupportingSparseFiles.contains(type)\n        }.getOrElse { false }\n    }\n\n    private fun File.findNearestExistingFile(): File? {\n        var f: File? = this\n        while (true) {\n            if (f == null) {\n                return null\n            }\n            if (f.exists()) {\n                return f\n            } else {\n                f = f.parentFile\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Kotlin.serialization)\n    id(MyPlugins.composeBase)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(\":downloader:core\"))\n                implementation(project(\":shared:utils\"))\n                implementation(libs.kotlin.coroutines.core)\n                implementation(libs.compose.runtime)\n            }\n        }\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"ir.amirab.downloader.monitor\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/CompletedDownloadItemState.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\n@Immutable\ndata class CompletedDownloadItemState(\n    override val id: Long,\n    override val folder: String,\n    override val name: String,\n    override val downloadLink: String,\n    override val contentLength: Long,\n    override val saveLocation: String,\n    override val dateAdded: Long,\n    override val startTime: Long,\n    override val completeTime: Long,\n) : IDownloadItemState {\n    companion object {\n        fun fromDownloadItem(item: IDownloadItem): CompletedDownloadItemState {\n            return CompletedDownloadItemState(\n                id = item.id,\n                folder = item.folder,\n                name = item.name,\n                downloadLink = item.link,\n                contentLength = item.contentLength,\n                saveLocation = item.name,\n                dateAdded = item.dateAdded,\n                startTime = item.startTime ?: -1,\n                completeTime = item.completeTime ?: -1,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/DownloadItemStateFactory.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\n@Immutable\ndata class ProcessingDownloadItemFactoryInputs<\n        out TDownloadJob : DownloadJob\n        >(\n    val downloadJob: TDownloadJob,\n    val speed: Long,\n    val isWaiting: Boolean,\n)\n\ninterface DownloadItemStateFactory<\n        in TDownloadItem : IDownloadItem,\n        in TDownloadJob : DownloadJob\n        > {\n    fun createProcessingDownloadItemState(\n        props: ProcessingDownloadItemFactoryInputs<TDownloadJob>\n    ): ProcessingDownloadItemState\n\n    fun createCompletedDownloadItemState(\n        downloadItem: TDownloadItem,\n    ): CompletedDownloadItemState\n\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/DownloadMonitor.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.utils.intervalFlow\nimport ir.amirab.util.flow.saved\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.queue.ManualDownloadQueue\nimport ir.amirab.util.flow.combineStateFlows\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\n\nclass DownloadMonitor(\n    private val downloadManager: DownloadManager,\n    private val manualDownloadQueue: ManualDownloadQueue,\n    downloadItemStateFactory: Lazy<DownloadItemStateFactory<IDownloadItem, DownloadJob>>\n) : IDownloadMonitor {\n    val downloadItemStateFactory by downloadItemStateFactory\n\n    private val scope = CoroutineScope(SupervisorJob())\n\n    private var avSpeedCollectorJob: Job? = null\n    override var useAverageSpeed = false\n        set(value) {\n            if (value == field) return\n            field = value\n            //always cancel current job\n            updateUseAverageSpeedFlow(value)\n        }\n\n    private fun updateUseAverageSpeedFlow(useAverageSpeed: Boolean) {\n        avSpeedCollectorJob?.cancel()\n        avSpeedCollectorJob = if (useAverageSpeed) {\n            // enabling average speed calculator flow only if nececary\n            // it will add a subscriber count into averageSpeedFlow and causes to start working\n            scope.launch { averageDownloadSpeedFlow.collect() }\n        } else {\n            // disable average speed\n            null\n        }\n    }\n\n    override val activeDownloadListFlow = MutableStateFlow<List<ProcessingDownloadItemState>>(emptyList())\n    override val completedDownloadListFlow = MutableStateFlow<List<CompletedDownloadItemState>>(emptyList())\n    override val downloadListFlow: StateFlow<List<IDownloadItemState>> =\n        combineStateFlows(activeDownloadListFlow, completedDownloadListFlow) { a, b -> a + b }\n\n    init {\n        activeDownloadListFlow\n            .subscriptionCount\n            .map { it > 0 }\n            .distinctUntilChanged()\n            .onEach { isUsed ->\n                if (isUsed) {\n                    downloadManager.awaitBoot()\n                    startUpdateActiveDownloadList()\n                    startSpeedMeter()\n                } else {\n                    stopUpdateDownloadList()\n                    stopSpeedMeter()\n                }\n            }\n            .launchIn(scope)\n        completedDownloadListFlow\n            .subscriptionCount\n            .map { it > 0 }\n            .distinctUntilChanged()\n            .onEach { isUsed ->\n                if (isUsed) {\n                    //wait for boot to initialize part downloaders!\n                    downloadManager.awaitBoot()\n                    startUpdateCompletedList()\n                } else {\n                    stopUpdateCompletedList()\n                }\n            }.launchIn(scope)\n    }\n\n\n    private val downloadSpeedFlow = MutableStateFlow<SpeedAtTime>(SpeedAtTime.empty())\n\n    private val averageDownloadSpeedFlow = downloadSpeedFlow\n        .saved(5)\n        .map { lastSpeedHistory ->\n            val lastSpeeds = lastSpeedHistory.lastOrNull()?.speed ?: return@map SpeedAtTime.empty()\n            SpeedAtTime(\n                lastSpeeds\n                    .mapValues { (id, _) ->\n                        lastSpeedHistory\n                            .mapNotNull { it.speed.getOrElse(id) { null } }\n                            .average()\n                            .toLong()\n                    }\n            )\n        }.stateIn(scope, SharingStarted.WhileSubscribed(), SpeedAtTime.empty())\n\n    private var speedMeterJob: Job? = null\n    private fun startSpeedMeter() {\n        speedMeterJob?.cancel()\n        updateUseAverageSpeedFlow(useAverageSpeed)\n        speedMeterJob = scope.launch {\n            var lastWrites = mapOf<Long, Long>()\n            while (isActive) {\n                val newWrites = downloadManager.downloadJobs.associate {\n                    it.id to it.getDownloadedSize()\n                }\n                downloadSpeedFlow.value = SpeedAtTime(\n                    newWrites.mapValues { (id, newWrite) ->\n                        val lastWrittenData = lastWrites.getOrElse(id) { null }\n                        val newSpeed = when {\n                            lastWrittenData != null -> {\n                                if (newWrite < lastWrittenData) {\n                                    // maybe download was restarted our lastWrittenData is not valid anymore\n                                    newWrite\n                                } else {\n                                    newWrite - lastWrittenData\n                                }\n                            }\n\n                            else -> {\n                                // this item seen for the first time\n                                0\n                            }\n                        }\n                        newSpeed\n                    }\n                )\n                lastWrites = newWrites\n                delay(1_000)\n            }\n        }\n    }\n\n    private fun stopSpeedMeter() {\n        speedMeterJob?.cancel()\n        speedMeterJob = null\n        avSpeedCollectorJob?.cancel()\n        avSpeedCollectorJob = null\n    }\n\n\n    private fun getPreferedSpeedFlow(): StateFlow<SpeedAtTime> {\n        return when {\n            useAverageSpeed -> averageDownloadSpeedFlow\n            else -> downloadSpeedFlow\n        }\n\n    }\n\n    private fun getSpeedOf(id: Long): Long {\n        val speed = getPreferedSpeedFlow().value.speed.getOrElse(id) { -1 }\n//        println(\"speed of $id is $speed\")\n        return speed\n    }\n\n    private var completedDownloadListUpdaterJob: Job? = null\n    private fun startUpdateCompletedList() {\n        completedDownloadListUpdaterJob?.cancel()\n        completedDownloadListUpdaterJob = scope.launch {\n            val initialData = downloadManager.getDownloadList().filter {\n                it.status == DownloadStatus.Completed\n            }.map {\n                downloadItemStateFactory.createCompletedDownloadItemState(it)\n            }\n            completedDownloadListFlow.update { initialData }\n            downloadManager.listOfJobsEvents\n                .onEach { event ->\n\n                    when (event) {\n                        is DownloadManagerEvents.OnJobCompleted -> {\n                            val item = downloadItemStateFactory\n                                .createCompletedDownloadItemState(event.downloadItem)\n                            completedDownloadListFlow.update { current ->\n                                //replace if this id is already in the completed list\n                                // this is happened when we are creating a job from a completed download\n                                val found = current.find { it.id == item.id }\n                                if (found != null) {\n                                    current.map {\n                                        if (it.id == item.id) {\n                                            item\n                                        } else {\n                                            it\n                                        }\n                                    }\n                                } else {\n                                    current + item\n                                }\n                            }\n                        }\n\n                        is DownloadManagerEvents.OnJobRemoved -> {\n                            completedDownloadListFlow.update {\n                                it.filter {\n                                    it.id != event.downloadItem.id\n                                }\n                            }\n                        }\n\n                        is DownloadManagerEvents.OnJobChanged -> {\n                            val shouldAdd = event.downloadItem.status == DownloadStatus.Completed\n                            completedDownloadListFlow.update { current ->\n                                if (shouldAdd) {\n                                    val item =\n                                        downloadItemStateFactory.createCompletedDownloadItemState(event.downloadItem)\n                                    val exists = current.find {\n                                        it.id == item.id\n                                    } != null\n                                    if (exists) {\n                                        //replace existing\n                                        current.map {\n                                            if (it.id == item.id) {\n                                                item\n                                            } else {\n                                                it\n                                            }\n                                        }\n                                    } else {\n                                        current + item\n                                    }\n                                } else {\n                                    current.filter {\n                                        it.id != event.downloadItem.id\n                                    }\n                                }\n                            }\n                        }\n\n                        else -> {}\n                    }\n                }\n                .launchIn(this)\n        }\n\n    }\n\n    private fun stopUpdateCompletedList() {\n        completedDownloadListUpdaterJob?.cancel()\n        completedDownloadListUpdaterJob = null\n    }\n\n\n    private var downloadListUpdaterJob: Job? = null\n    private val headlessQueuePendingItemsFlow = manualDownloadQueue.pendingItems\n    private fun startUpdateActiveDownloadList() {\n        downloadListUpdaterJob?.cancel()\n        downloadListUpdaterJob = merge(\n            downloadManager.listOfJobsEvents.map { },\n            downloadSpeedFlow,\n            headlessQueuePendingItemsFlow,\n            intervalFlow(500)\n        ).onEach {\n            val newList = downloadManager.downloadJobs.filter {\n                it.status.value != DownloadJobStatus.Finished\n            }.map {\n                val status = it.status.value\n                val speed = if (status is DownloadJobStatus.IsActive) {\n                    getSpeedOf(it.id)\n                } else 0L\n                val isWaiting = headlessQueuePendingItemsFlow.value.contains(it.id)\n                downloadItemStateFactory.createProcessingDownloadItemState(\n                    ProcessingDownloadItemFactoryInputs(\n                        downloadJob = it,\n                        speed = speed,\n                        isWaiting = isWaiting,\n                    )\n                )\n            }\n            activeDownloadListFlow.update { newList }\n        }\n            .launchIn(scope)\n    }\n\n    private fun stopUpdateDownloadList() {\n        downloadListUpdaterJob?.cancel()\n//        println(\"turn off list updater\")\n        downloadListUpdaterJob = null\n    }\n\n\n    override val activeDownloadCount = downloadManager.listOfJobsEvents.map {\n        downloadManager.getActiveCount()\n    }.stateIn(\n        scope,\n        SharingStarted.Eagerly,\n        downloadManager.getActiveCount()\n    )\n\n    override suspend fun waitForDownloadToFinishOrCancel(\n        id: Long,\n    ) {\n        val event = downloadManager\n            .listOfJobsEvents\n            .filter {\n                it.downloadItem.id == id\n            }\n            .first {\n                when (it) {\n                    is DownloadManagerEvents.OnJobAdded -> false\n                    is DownloadManagerEvents.OnJobCanceled -> true\n                    is DownloadManagerEvents.OnJobChanged -> false\n                    is DownloadManagerEvents.OnJobCompleted -> true\n                    is DownloadManagerEvents.OnJobRemoved -> true\n                    is DownloadManagerEvents.OnJobStarted -> false\n                    is DownloadManagerEvents.OnJobStarting -> false\n                }\n            }\n        if (event is DownloadManagerEvents.OnJobCanceled) {\n            throw event.e\n        }\n    }\n}\n\ndata class SpeedAtTime(\n    val speed: Map<Long, Long>,\n    val time: Long = System.currentTimeMillis(),\n) {\n    companion object {\n        fun empty() = SpeedAtTime(emptyMap())\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/DownloadStateUtil.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\n\nfun IDownloadItemState.statusOrFinished(): DownloadJobStatus {\n    return (this as? ProcessingDownloadItemState)?.status ?: DownloadJobStatus.Finished\n}\n\nfun IDownloadItemState.isFinished(): Boolean {\n    return this is CompletedDownloadItemState\n}\n\nfun IDownloadItemState.isNotFinished(): Boolean {\n    return this is ProcessingDownloadItemState\n}\n\nfun IDownloadItemState.speedOrNull(): Long? {\n    return (this as? ProcessingDownloadItemState)?.speed\n}\n\nfun IDownloadItemState.remainingOrNull(): Long? {\n    return (this as? ProcessingDownloadItemState)?.remainingTime\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/IDownloadItemState.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport androidx.compose.runtime.Immutable\nimport java.io.File\n\n@Immutable\nsealed interface IDownloadItemState {\n    val id: Long\n    val folder: String\n    val name: String\n    val contentLength: Long\n    val saveLocation: String\n    val dateAdded: Long\n    val startTime: Long\n    val completeTime: Long\n    val downloadLink: String\n\n    fun getFullPath() = File(folder, name)\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/IDownloadMonitor.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\ninterface IDownloadMonitor {\n    var useAverageSpeed: Boolean\n    val activeDownloadListFlow: StateFlow<List<ProcessingDownloadItemState>>\n    val completedDownloadListFlow: StateFlow<List<CompletedDownloadItemState>>\n    val downloadListFlow: StateFlow<List<IDownloadItemState>>\n    val activeDownloadCount: StateFlow<Int>\n\n    suspend fun waitForDownloadToFinishOrCancel(\n        id: Long,\n    )\n}\n\nfun IDownloadMonitor.isDownloadActiveFlow(\n    downloadId: Long,\n): StateFlow<Boolean> {\n    return activeDownloadListFlow.mapStateFlow { activeDownloadList ->\n        activeDownloadList.find {\n            downloadId == it.id\n        }?.canBePaused() ?: false\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/ProcessingDownloadItemState.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.part.PartDownloadStatus\nimport ir.amirab.downloader.utils.calcPercent\n\nsealed interface ProcessingDownloadItemState : IDownloadItemState {\n    val status: DownloadJobStatus\n    val speed: Long\n    val supportResume: Boolean?\n    val parts: List<UiPart>\n\n    val gotAnyProgress: Boolean\n    val progress: Long\n    val hasProgress: Boolean\n    val percent: Int? // 0..100\n\n    //remaining time in seconds\n    val remainingTime: Long?\n\n    val isWaiting: Boolean\n\n    fun canBePaused() = isWaiting || status is DownloadJobStatus.IsActive\n    fun canBeResumed() = status is DownloadJobStatus.CanBeResumed && !isWaiting\n}\n\n@Immutable\ndata class RangeBasedProcessingDownloadItemState(\n    override val id: Long,\n    override val folder: String,\n    override val name: String,\n    override val downloadLink: String,\n    override val contentLength: Long,\n    override val saveLocation: String,\n    override val dateAdded: Long,\n    override val startTime: Long,\n    override val completeTime: Long,\n    override val status: DownloadJobStatus,\n    override val speed: Long,\n    override val parts: List<UiPart>,\n    override val supportResume: Boolean?,\n    override val isWaiting: Boolean,\n) : ProcessingDownloadItemState {\n    override val progress = parts.sumOf {\n        it.howMuchProceed\n    }\n    override val hasProgress get() = progress > 0\n    override val gotAnyProgress = progress > 0L\n    override val percent: Int? = if (contentLength == IDownloadItem.LENGTH_UNKNOWN) {\n        null\n    } else {\n        calcPercent(progress, contentLength)\n    }\n\n    //remaining time in seconds\n    override val remainingTime: Long? = kotlin.run {\n        when {\n            contentLength <= 0 || speed <= 0 -> null\n            else -> (contentLength - progress) / speed\n        }\n    }\n\n    companion object\n}\n\n@Immutable\ndata class DurationBasedProcessingDownloadItemState(\n    override val id: Long,\n    override val folder: String,\n    override val name: String,\n    override val downloadLink: String,\n    override val contentLength: Long,\n    override val saveLocation: String,\n    override val dateAdded: Long,\n    override val startTime: Long,\n    override val completeTime: Long,\n    override val status: DownloadJobStatus,\n    override val speed: Long,\n    override val parts: List<UiPart>,\n    override val supportResume: Boolean?,\n    val optimisticLength: Long,\n    val duration: Double?,\n    override val progress: Long,\n    override val percent: Int,\n    override val isWaiting: Boolean,\n\n) : ProcessingDownloadItemState {\n\n    override val hasProgress get() = progress > 0\n    override val gotAnyProgress = progress > 0L\n//    override val percent: Int? = run {\n//        val length = getLengthOrOptimistic(contentLength, optimisticLength)\n//        if (length == IDownloadItem.LENGTH_UNKNOWN) {\n//            val partsSize = parts.size\n//            if (partsSize > 0) {\n//                calcPercent(finishedPartsCount, partsSize)\n//            } else {\n//                null\n//            }\n//        } else {\n//            calcPercent(progress, length)\n//        }\n//    }\n\n    override val remainingTime: Long? = kotlin.run {\n        val length = getLengthOrOptimistic(contentLength, optimisticLength)\n        when {\n            length <= 0 || speed <= 0 -> null\n            else -> (length - progress) / speed\n        }\n    }\n\n    companion object {\n        private fun getLengthOrOptimistic(\n            exactLength: Long, optimisticLength: Long\n        ): Long {\n            return when {\n                exactLength > 0 -> exactLength\n                optimisticLength > 0 -> optimisticLength\n                else -> -1\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "downloader/monitor/src/commonMain/kotlin/ir/amirab/downloader/monitor/UiPart.kt",
    "content": "package ir.amirab.downloader.monitor\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.downloader.part.MediaSegment\nimport ir.amirab.downloader.part.RangedPart\nimport ir.amirab.downloader.part.PartDownloadStatus\n\n@Immutable\nsealed interface UiPart {\n    val id: Long\n    val status: PartDownloadStatus\n    val howMuchProceed: Long\n    val percent: Int?\n    val length: Long?\n    val partSpace: Float\n}\n\n@Immutable\ndata class UiRangedPart(\n    override val id: Long,\n    override val status: PartDownloadStatus,\n    override val howMuchProceed: Long,\n    override val percent: Int?,\n    override val length: Long?,\n    override val partSpace: Float\n) : UiPart {\n    companion object {\n        fun fromPart(\n            part: RangedPart,\n            totalLength: Long,\n        ): UiRangedPart {\n            val length = part.partLength\n            return UiRangedPart(\n                id = part.getID(),\n                status = part.status,\n                howMuchProceed = part.howMuchProceed(),\n                percent = part.percent,\n                length = length,\n                partSpace = when {\n                    totalLength <= 0 || length == null || length <= 0L -> 0f\n                    else -> (length.toDouble() / totalLength.toDouble()).toFloat()\n                },\n            )\n        }\n    }\n}\n\n@Immutable\ndata class UiDurationBasedPart(\n    override val id: Long,\n    override val status: PartDownloadStatus,\n    override val howMuchProceed: Long,\n    override val percent: Int?,\n    override val length: Long?,\n    override val partSpace: Float,\n//    val duration: Double,\n) : UiPart {\n    companion object {\n        fun fromPart(\n            part: MediaSegment,\n            totalPartsCount: Int,\n        ): UiDurationBasedPart {\n            val index = part.segmentIndex\n            return UiDurationBasedPart(\n                id = index,\n                status = part.status,\n                howMuchProceed = part.howMuchProceed(),\n                percent = part.percent,\n                length = part.length,\n                partSpace = if (totalPartsCount == 0) {\n                    0f\n                } else {\n                    1f / totalPartsCount\n                }\n//                duration = part.duration,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nfastscrollerCore = \"0.3.2\"\nkotlin = \"2.3.10\"\nksp = \"2.3.3\"\ncompose = \"1.10.1\"\nkotlin-serialization = \"1.10.0\"\nokhttp = \"5.3.2\"\nokio = \"3.16.4\"\ncoroutines = \"1.10.2\"\nkoin = \"4.1.1\"\nkoinAnnotation = \"2.3.1\"\ndecompose = \"3.4.0\"\nessenty = \"2.5.0\"\nlogback = \"1.5.22\"\nbuildConfig = \"6.0.7\"\nchangelog = \"2.5.0\"\nhttp4k = \"6.28.1.0\"\narrow = \"2.2.1.1\"\ndatastore = \"1.2.0\"\naboutLibraries = \"13.2.1\"\njna = \"5.18.1\"\ndatetime = \"0.7.1\"\nfileKit = \"0.8.8\"\nreorderable = \"3.0.0\"\nsemver = \"3.0.0\"\njgit = \"7.3.0.202506031305-r\"\nosThemeDetector = \"3.9.1\"\nkotlinFileWatcher = \"1.4.0\"\nmarkdownRenderer = \"0.39.2\"\nproxyVole = \"2.0.0\"\njbrApi = \"1.10.1\"\ngradleVersions = \"0.53.0\"\nhandlebars = \"4.5.0\"\ncomposeNativeTray = \"1.1.0\"\n\nagp = \"8.12.3\"\nandroidx-core = \"1.17.0\"\nandroidx-activity-compose = \"1.12.4\"\n\n[libraries]\n\nkotlin-stdlib = { module = \"org.jetbrains.kotlin:kotlin-stdlib\", version.ref = \"kotlin\" }\nkotlin-reflect = { module = \"org.jetbrains.kotlin:kotlin-reflect\", version.ref = \"kotlin\" }\n\nkotlin-serialization-core = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-core\", version.ref = \"kotlin-serialization\" }\nkotlin-serialization-json = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"kotlin-serialization\" }\n\nokhttp-loggingInterceptor = { module = \"com.squareup.okhttp3:logging-interceptor\", version.ref = \"okhttp\" }\nokhttp-okhttp = { module = \"com.squareup.okhttp3:okhttp\", version.ref = \"okhttp\" }\nokhttp-coroutines = { module = \"com.squareup.okhttp3:okhttp-coroutines\", version.ref = \"okhttp\" }\nokio-okio = { module = \"com.squareup.okio:okio\", version.ref = \"okio\" }\n\nkoin-core = { module = \"io.insert-koin:koin-core\", version.ref = \"koin\" }\nkoin-compose = { module = \"io.insert-koin:koin-compose\", version = \"1.0.1\" }\nkoin-compiler = { module = \"io.insert-koin:koin-ksp-compiler\", version.ref = \"koinAnnotation\" }\nkoin-annotations = { module = \"io.insert-koin:koin-annotations\", version.ref = \"koinAnnotation\" }\n\ndecompose = { module = \"com.arkivanov.decompose:decompose\", version.ref = \"decompose\" }\ndecompose-jbCompose = { module = \"com.arkivanov.decompose:extensions-compose\", version.ref = \"decompose\" }\n\nessenty-lifecycleCoroutines = { module = \"com.arkivanov.essenty:lifecycle-coroutines\", version.ref = \"essenty\" }\n\nkotlin-coroutines-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"coroutines\" }\nkotlin-coroutines-debug = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-debug\", version.ref = \"coroutines\" }\nkotlin-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"coroutines\" }\nkotlin-coroutines-swing = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-swing\", version.ref = \"coroutines\" }\n\nkotlin-datetime = { module = \"org.jetbrains.kotlinx:kotlinx-datetime\", version.ref = \"datetime\" }\ncomposeFileKit = { module = \"io.github.vinceglb:filekit-compose\", version.ref = \"fileKit\" }\n\nfastscroller-core = { module = \"io.github.oikvpqya.compose.fastscroller:fastscroller-core\", version.ref = \"fastscrollerCore\" }\n\nloggers-logback-core = { module = \"ch.qos.logback:logback-core\", version.ref = \"logback\" }\nloggers-logback-classic = { module = \"ch.qos.logback:logback-classic\", version.ref = \"logback\" }\n\nnanoHttpd-core = \"org.nanohttpd:nanohttpd:2.3.1\"\nhttp4k-core = { module = \"org.http4k:http4k-core\", version.ref = \"http4k\" }\nhttp4k-server-jetty = { module = \"org.http4k:http4k-server-jetty\", version.ref = \"http4k\" }\nhttp4k-client-okhttp = { module = \"org.http4k:http4k-client-okhttp\", version.ref = \"http4k\" }\n\ncompose-material-rippleEffect = { module = \"org.jetbrains.compose.material:material-ripple\", version.ref = \"compose\" }\ncompose-reorderable = { module = \"sh.calvin.reorderable:reorderable\", version.ref = \"reorderable\" }\n\ncompose-runtime = { module = \"org.jetbrains.compose.runtime:runtime\", version.ref = \"compose\" }\ncompose-foundation = { module = \"org.jetbrains.compose.foundation:foundation\", version.ref = \"compose\" }\ncompose-ui = { module = \"org.jetbrains.compose.ui:ui\", version.ref = \"compose\" }\n\nsemver = { module = \"io.github.z4kn4fein:semver\", version.ref = \"semver\" }\njgit = { module = \"org.eclipse.jgit:org.eclipse.jgit\", version.ref = \"jgit\" }\n\naboutLibraries-core = { module = \"com.mikepenz:aboutlibraries-core\", version.ref = \"aboutLibraries\" }\naboutLibraries-compose = { module = \"com.mikepenz:aboutlibraries-compose\", version.ref = \"aboutLibraries\" }\n\njna-core = { module = \"net.java.dev.jna:jna\", version.ref = \"jna\" }\njna-platform = { module = \"net.java.dev.jna:jna-platform\", version.ref = \"jna\" }\npluginAndroidGradle = { module = \"com.android.tools.build:gradle\", version.ref = \"agp\" }\npluginComposeMultiplatform = { module = \"org.jetbrains.compose:compose-gradle-plugin\", version.ref = \"compose\" }\npluginComposeCompiler = { module = \"org.jetbrains.kotlin:compose-compiler-gradle-plugin\", version.ref = \"kotlin\" }\npluginKotlin = { module = \"org.jetbrains.kotlin:kotlin-gradle-plugin\", version.ref = \"kotlin\" }\npluginSerialization = { module = \"org.jetbrains.kotlin:kotlin-serialization\", version.ref = \"kotlin\" }\npluginChangeLog = { module = \"org.jetbrains.intellij.plugins:gradle-changelog-plugin\", version.ref = \"changelog\" }\npluginBuildConfig = { module = \"com.github.gmazzo.buildconfig:plugin\", version.ref = \"buildConfig\" }\npluginKsp = { module = \"com.google.devtools.ksp:symbol-processing-gradle-plugin\", version.ref = \"ksp\" }\npluginAboutLibraries = { module = \"com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin\", version.ref = \"aboutLibraries\" }\npluginGradleVersions = { module = \"com.github.ben-manes:gradle-versions-plugin\", version.ref = \"gradleVersions\" }\n\narrow-core = { module = \"io.arrow-kt:arrow-core\", version.ref = \"arrow\" }\narrow-optics = { module = \"io.arrow-kt:arrow-optics\", version.ref = \"arrow\" }\narrow-opticKsp = { module = \"io.arrow-kt:arrow-optics-ksp-plugin\", version.ref = \"arrow\" }\n\nandroidx-core-ktx = { module = \"androidx.core:core-ktx\", version.ref = \"androidx-core\" }\nandroidx-datastore = { module = \"androidx.datastore:datastore\", version.ref = \"datastore\" }\nandroidx-activity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"androidx-activity-compose\" }\n\nosThemeDetector = { module = \"com.github.Dansoftowner:jSystemThemeDetector\", version.ref = \"osThemeDetector\" }\nmarkdownRenderer-core = { module = \"com.mikepenz:multiplatform-markdown-renderer\", version.ref = \"markdownRenderer\" }\nhandlebarsJava = { module = \"com.github.jknack:handlebars\", version.ref = \"handlebars\" }\n\nkotlinFileWatcher = { module = \"io.github.irgaly.kfswatch:kfswatch\", version.ref = \"kotlinFileWatcher\" }\n\ncomposeNativeTray = { module = \"io.github.kdroidfilter:composenativetray\", version.ref = \"composeNativeTray\" }\nproxyVole = { module = \"org.bidib.com.github.markusbernhardt:proxy-vole\", version.ref = \"proxyVole\" }\njbrApi = { module = \"org.jetbrains.runtime:jbr-api\", version.ref = \"jbrApi\" }\n[plugins]\nkotlin-jvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\ncompose = { id = \"org.jetbrains.compose\", version.ref = \"compose\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\n\nbuildConfig = { id = \"com.github.gmazzo.buildconfig\", version.ref = \"buildConfig\" }\nchangeLog = { id = \"org.jetbrains.changelog\", version.ref = \"changelog\" }\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. For more details, visit\n# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects\norg.gradle.parallel=true\n#org.gradle.configuration-cache=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n# Enables namespacing of each library's R class so that its R class includes only the\n# resources declared in the library itself and none from the library's dependencies,\n# thereby reducing the size of the R class for that library\nandroid.nonTransitiveRClass=true\n"
  },
  {
    "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\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=\r\n\r\n\r\n@rem Execute Gradle\r\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\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "integration/server/build.gradle.kts",
    "content": "plugins{\n    id(MyPlugins.kotlin)\n    id(Plugins.Kotlin.serialization)\n}\n\ndependencies {\n    implementation(libs.kotlin.coroutines.core)\n    implementation(libs.kotlin.serialization.json)\n    implementation(project(\":shared:utils\"))\n    implementation(project(\":shared:nanohttp4k\"))\n}"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/ApiQueueModel.kt",
    "content": "package com.abdownloadmanager.integration\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ApiQueueModel(\n        val id: Long,\n        val name: String,\n)\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/DownloadCredentialsFromIntegration.kt",
    "content": "package com.abdownloadmanager.integration\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.SerializationException\nimport kotlinx.serialization.json.Json\n\n@Serializable\nsealed interface IDownloadCredentialsFromIntegration {\n    val link: String\n    val downloadPage: String?\n\n    val suggestedName: String?\n}\n\n@SerialName(\"http\")\n@Serializable\ndata class HttpDownloadCredentialsFromIntegration(\n    override val link: String,\n    val headers: Map<String, String>? = null,\n    override val downloadPage: String? = null,\n    override val suggestedName: String? = null,\n) : IDownloadCredentialsFromIntegration\n\n@SerialName(\"hls\")\n@Serializable\ndata class HLSDownloadCredentialsFromIntegration(\n    override val link: String,\n    val headers: Map<String, String>? = null,\n    override val downloadPage: String? = null,\n    override val suggestedName: String? = null,\n) : IDownloadCredentialsFromIntegration\n\n@Serializable\ndata class AddDownloadOptionsFromIntegration(\n    val silentAdd: Boolean = false,\n    val silentStart: Boolean = false,\n)\n\n@Serializable\ndata class AddDownloadsFromIntegration(\n    val items: List<IDownloadCredentialsFromIntegration>,\n    val options: AddDownloadOptionsFromIntegration = AddDownloadOptionsFromIntegration()\n) {\n    companion object {\n        fun createFromRequest(json: Json, jsonData: String): AddDownloadsFromIntegration {\n            return try {\n                json.decodeFromString<AddDownloadsFromIntegration>(jsonData)\n            } catch (_: SerializationException) {\n                // TODO Remove this after a while\n                AddDownloadsFromIntegration(\n                    items = json.decodeFromString<List<HttpDownloadCredentialsFromIntegration>>(jsonData),\n                    AddDownloadOptionsFromIntegration(\n                        silentAdd = false,\n                        silentStart = false,\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/Integration.kt",
    "content": "package com.abdownloadmanager.integration\n\nimport com.abdownloadmanager.integration.http4k.MyHttp4KServer\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.builtins.ListSerializer\n\nsealed interface IntegrationResult {\n    data object Inactive : IntegrationResult\n    data class Fail(val throwable: Throwable) : IntegrationResult\n    data class Success(val port: Int) : IntegrationResult\n}\n\nclass Integration(\n    val integrationHandler: IntegrationHandler,\n    val scope: CoroutineScope,\n    private val json: Json,\n    val debugMode: Boolean,\n) {\n\n    private val portFlow = MutableStateFlow<Int?>(null)\n    val integrationStatus = MutableStateFlow<IntegrationResult>(IntegrationResult.Inactive)\n\n    fun enable(port: Int) {\n        portFlow.update { port }\n    }\n\n    fun disable() {\n        portFlow.update { null }\n    }\n\n    fun boot() {\n        scope.launch {\n            kotlin.runCatching {\n                portFlow.collect { port ->\n                    runCatching {\n                        if (port != null) {\n                            startServer(port)\n                            integrationStatus.update { IntegrationResult.Success(port) }\n                        } else {\n                            stopServer()\n                            integrationStatus.update { IntegrationResult.Inactive }\n                        }\n                    }.onFailure { throwable ->\n                        integrationStatus.update {\n                            IntegrationResult.Fail(throwable)\n                        }\n                        kotlin.runCatching {\n                            disable()\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @Volatile\n    private var server: MyServer? = null\n    private suspend fun startServer(port: Int) {\n        stopServer()\n        val server = createServer(port)\n        this.server = server\n        withContext(Dispatchers.IO) {\n//            println(\"start server\")\n            server.startMyServer()\n        }\n    }\n\n    private suspend fun stopServer() {\n        server?.let {\n//            println(\"stop server\")\n            withContext(Dispatchers.IO) {\n                it.stopMyServer()\n            }\n        }\n        server = null\n    }\n\n\n    private fun createServer(port: Int): MyServer {\n        val handlers = HandlerMap().apply {\n            post(\"/add\") {\n                runBlocking {\n                    val itemsToAdd = kotlin.runCatching {\n                        val message = it.getBody().orEmpty()\n                        AddDownloadsFromIntegration.createFromRequest(\n                            json = json,\n                            jsonData = message\n                        )\n                    }\n                    itemsToAdd.onFailure { it.printStackTrace() }\n                    itemsToAdd.getOrThrow().let { newImportRequest ->\n                        integrationHandler.addDownload(\n                            newImportRequest.items,\n                            newImportRequest.options,\n                        )\n                    }\n                }\n                MyResponse.Text(\"OK\")\n            }\n            get(\"/queues\") {\n                runBlocking {\n                    val queues = integrationHandler.listQueues()\n                    val jsonResponse = json.encodeToString(ListSerializer(ApiQueueModel.serializer()), queues)\n                    MyResponse.Text(jsonResponse)\n                }\n            }\n            post(\"/start-headless-download\") {\n                runBlocking {\n                    val itemsToAdd = kotlin.runCatching {\n                        val message = it.getBody().orEmpty()\n                        json.decodeFromString<NewDownloadTask>(message)\n                    }\n                    itemsToAdd.onFailure { it.printStackTrace() }\n                    integrationHandler.addDownloadTask(itemsToAdd.getOrThrow())\n                }\n                MyResponse.Text(\"OK\")\n            }\n            post(\"/ping\") {\n                MyResponse.Text(\"pong\")\n            }\n        }\n        return MyHttp4KServer(port, handlers, debugMode)\n    }\n}\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/IntegrationHandler.kt",
    "content": "package com.abdownloadmanager.integration\n\ninterface IntegrationHandler{\n    suspend fun addDownload(\n        list: List<IDownloadCredentialsFromIntegration>,\n        options: AddDownloadOptionsFromIntegration,\n    )\n    fun listQueues(): List<ApiQueueModel>\n    suspend fun addDownloadTask(task: NewDownloadTask)\n}\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/MyRequestAndResponse.kt",
    "content": "package com.abdownloadmanager.integration\n\ntypealias Header = Map<String, String>\n\ndata class MyRequest(\n    val uri:String,\n    val method:String,//: GET | POST\n    val getBody:()->String?\n)\n\n\nsealed class MyResponse(\n    val statusCode: Int,\n    val headers: Header,\n) {\n    abstract fun getContent(): String\n\n    class Json(val jsonData: String, headers: Header = emptyMap()) : MyResponse(statusCode = 200, headers = headers) {\n        override fun getContent() = jsonData\n    }\n\n    class Text(\n        val text: String,\n        headers: Header = emptyMap(),\n        statusCode: Int=200,\n    ) : MyResponse(statusCode = statusCode, headers = headers) {\n        override fun getContent() = text\n    }\n\n    class BadRequest(\n        val errorText: String,\n        headers: Header = emptyMap(),\n        statusCode: Int=400,\n    ) : MyResponse(\n        statusCode = statusCode,\n        headers = headers,\n    ) {\n        override fun getContent() = errorText\n    }\n}\n\n\ntypealias Handler = (MyRequest) -> MyResponse\n\n\n\nclass HandlerMap {\n    private val handlers = mutableListOf<Pair<String, Handler>>()\n    private fun add(uri: String, method: String, handler: Handler) {\n        handlers.add(Pair(uri, handler))\n    }\n    fun get(uri: String,handler: Handler){\n        add(uri,\"GET\",handler)\n    }\n    fun post(uri: String,handler: Handler){\n        add(uri,\"POST\",handler)\n    }\n\n\n    fun findMatch(session: MyRequest): Handler? {\n        val handler = handlers.find {\n            session.uri == it.first\n        }?.second\n        return handler\n    }\n}\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/MyServer.kt",
    "content": "package com.abdownloadmanager.integration\n\ninterface MyServer{\n    fun stopMyServer()\n    fun startMyServer()\n}"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/MySunHttpServer.kt",
    "content": "package com.abdownloadmanager.integration\n\nimport com.sun.net.httpserver.HttpExchange\nimport com.sun.net.httpserver.HttpHandler\nimport com.sun.net.httpserver.HttpServer\nimport java.net.InetSocketAddress\nimport java.util.concurrent.ExecutorService\nimport java.util.concurrent.Executors\n\nclass MySunHttpServer(\n    val port: Int,\n    val handlerMap: HandlerMap,\n    val isDebugMode: Boolean,\n) : MyServer {\n    private var server: HttpServer? = null\n\n    private fun createServer(): HttpServer {\n        val httpServer = HttpServer.create(\n            InetSocketAddress(\"localhost\", port),\n            1000,\n        )\n        httpServer.createContext(\n            /* path = */ \"/\",\n            /* handler = */ HttpHandlerImpl(\n                handlerMap = handlerMap,\n                isDebugMode = isDebugMode\n            )\n        )\n        httpServer.executor = Executors.newWorkStealingPool()\n        return httpServer\n    }\n\n    override fun stopMyServer() {\n        server?.run {\n            (executor as ExecutorService).shutdownNow()\n            stop(0)\n        }\n        server = null\n    }\n\n    override fun startMyServer() {\n        stopMyServer()\n        server = createServer().also {\n            it.start()\n        }\n    }\n}\n\nprivate class HttpHandlerImpl(\n    val handlerMap: HandlerMap,\n    val isDebugMode: Boolean,\n) : HttpHandler {\n    fun createMyRequest(exchange: HttpExchange): MyRequest {\n        return MyRequest(\n            uri = exchange.requestURI.toString(),\n            method = exchange.requestMethod,\n            getBody = {\n                exchange.requestBody.reader().readText()\n            },\n        )\n    }\n\n    fun fillExchangeWithResponse(exchange: HttpExchange, response: MyResponse) {\n        response.headers.forEach { (key, value) ->\n            exchange.responseHeaders.add(key, value)\n        }\n        exchange.sendResponseHeaders(response.statusCode, response.getContent().length.toLong())\n        exchange.responseBody.writer().use {\n            it.write(response.getContent())\n        }\n    }\n\n    override fun handle(exchange: HttpExchange) {\n        val request = createMyRequest(exchange)\n        val handler = handlerMap.findMatch(request)\n        try {\n            val response = handler?.invoke(request) ?: MyResponse.BadRequest(\n                errorText = \"Not Found\",\n                statusCode = 404,\n            )\n            fillExchangeWithResponse(exchange, response)\n        } catch (e: Exception) {\n            val internalServerErrorResponse = MyResponse.Text(\n                if (isDebugMode) \"Error ${e.localizedMessage}\"\n                else \"Error\",\n                statusCode = 500,\n            )\n            fillExchangeWithResponse(exchange, internalServerErrorResponse)\n            return\n        } finally {\n            exchange.close()\n        }\n    }\n}\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/NewDownloadTask.kt",
    "content": "package com.abdownloadmanager.integration\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class NewDownloadTask(\n    val downloadSource: IDownloadCredentialsFromIntegration,\n    var folder: String? = null,\n    var name: String? = null,\n    var queueId: Long? = null,\n)\n"
  },
  {
    "path": "integration/server/src/main/kotlin/com/abdownloadmanager/integration/http4k/MyHttp4KServer.kt",
    "content": "package com.abdownloadmanager.integration.http4k\n\nimport ir.amirab.util.http4k.NanoHttp\nimport com.abdownloadmanager.integration.HandlerMap\nimport com.abdownloadmanager.integration.MyRequest\nimport com.abdownloadmanager.integration.MyResponse\nimport com.abdownloadmanager.integration.MyServer\nimport org.http4k.core.*\nimport org.http4k.server.Http4kServer\nimport org.http4k.server.asServer\n\n\nclass MyHttp4KServer(\n    val port: Int,\n    val handlerMap: HandlerMap,\n    val isDebugMode: Boolean,\n) : MyServer {\n    private fun toMyRequest(request: Request): MyRequest {\n        return MyRequest(\n            uri = request.uri.toString(),\n            method = request.method.toString(),\n            getBody = {\n                if (request.body == Body.EMPTY) null\n                else request.bodyString()\n            },\n        )\n    }\n\n    private fun toHttp4kResponse(response: MyResponse): Response {\n        val status = Status.serverValues.find {\n            it.code == response.statusCode\n        }!!\n        return Response(status)\n            .headers(response.headers.map { it.key to it.value })\n            .body(response.getContent())\n    }\n\n    private var server: Http4kServer? = null\n\n    private fun createServer(): Http4kServer {\n//        val logAll = Filter { next ->\n//            {\n//                println(\"req: $it\")\n//                next(it).also {\n//                    println(\"res: $it\")\n//                }\n//            }\n//        }\n        val appRoute = { req: Request ->\n            val myRequest = toMyRequest(req)\n            val handler = handlerMap.findMatch(myRequest)\n            if (handler != null) {\n                toHttp4kResponse(handler(myRequest))\n            } else {\n                Response(Status.NOT_FOUND)\n                    .body(\"Not Found\")\n            }\n        }\n        return Filter.NoOp\n//            .then(logAll)\n            .then(appRoute)\n            .asServer(NanoHttp(\"localhost\",port))\n    }\n\n    override fun stopMyServer() {\n        server?.stop()\n        server = null\n    }\n\n    override fun startMyServer() {\n        stopMyServer()\n        server = createServer().also {\n            it.start()\n        }\n    }\n}"
  },
  {
    "path": "integration/server/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} MDC=%X{user} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"error\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>"
  },
  {
    "path": "integration/server/src/main/resources/rules.pro",
    "content": ""
  },
  {
    "path": "scripts/install.sh",
    "content": "#!/usr/bin/env bash\n\n# Downloads the latest tarball from https://github.com/amir1376/ab-download-manager/releases and unpacks it into ~/.local/.\n# Creates a .desktop entry for the app in ~/.local/share/applications based on FreeDesktop specifications.\n\nset -euo pipefail\n\nDEPENDENCIES=(curl tar)\nLOG_FILE=\"/tmp/ab-dm-installer.log\"\n\n# --- Custom Logger\nlogger() {\n  timestamp=$(date +\"%Y/%m/%d %H:%M:%S\")\n\n  if [[ \"$1\" == \"error\" ]]; then\n    # Red color for errors\n    echo -e \"${timestamp} -- ABDM-Installer [Error]: \\033[0;31m$@\\033[0m\" | tee -a ${LOG_FILE}\n  else\n    # Default color for non-error messages\n    echo -e \"${timestamp} -- ABDM-Installer [Info]: $@\" | tee -a ${LOG_FILE}\n  fi\n}\n\nremove_if_exists() {\n    local target=\"$1\"\n\n    if [ -z \"$target\" ]; then\n        logger \"No target specified in remove_if_exists function\"\n        return 1\n    fi\n\n    if [ -e \"$target\" ]; then\n        logger \"File \\\"$target\\\" Removed\"\n        rm -rf \"$target\"\n    else\n        logger \"File \\\"$target\\\" does not exist\"\n    fi\n}\n\n# --- Detect OS and The Package Manager to use\ndetect_package_manager() {\n    if [ -f /etc/os-release ]; then\n        source /etc/os-release\n        local OS=${NAME}\n    elif type lsb_release >/dev/null 2>&1; then\n        local OS=$(lsb_release -si)\n    elif [ -f /etc/lsb-release ]; then\n        source /etc/lsb-release\n        local OS=\"${DISTRIB_ID}\"\n    elif [ -f /etc/debian_version ]; then\n        local OS=Debian\n    else\n        logger error \"Your Linux Distro is not Supperted.\"\n        logger error \"Please install ${DEPENDENCIES[@]} Manually.\"\n        exit 1\n    fi\n\n    if `grep -E 'Debian|Ubuntu' <<< $OS > /dev/null` ; then\n        systemPackage=\"apt\"\n    elif `grep -E 'Fedora|CentOS|Red Hat|AlmaLinux' <<< $OS > /dev/null`; then\n        systemPackage=\"dnf\"\n    fi\n}\n\ndetect_package_manager\n\n# --- Install dependencies\ninstall_dependencies() {\n\n    local answer\n    read -p \"Do you want to install $1? [Y/n]: \" -r answer\n    answer=${answer:-Y}  # Set default to 'Y' if no input is given\n\n    case $answer in\n        [Yy]* )\n            sudo ${systemPackage} update -y\n            logger \"installing $1 package ...\"\n            sudo ${systemPackage} install -y $1\n            ;;\n        [Nn]* )\n            logger \"Skipping the installation of $1.\"\n            ;;\n        * )\n            logger error \"Please answer yes or no.\"\n            install_dependencies \"$1\"  # re-prompt for the same package\n            ;;\n    esac\n}\n\n# Check dependencies and install if missing\ncheck_dependencies() {\n    for pkg in \"${DEPENDENCIES[@]}\"; do\n        if ! command -v \"$pkg\" >/dev/null 2>&1; then\n            logger \"$pkg is not installed. Installing...\"\n            install_dependencies \"$pkg\"\n        else\n            logger \"$pkg is already installed.\"\n        fi\n    done\n}\n\nget_arch() {\n  case \"$(uname -m)\" in\n    x86_64 | amd64)\n      echo \"x64\"\n      ;;\n    aarch64 | arm64)\n      echo \"arm64\"\n      ;;\n    *)\n      logger error \"Unsupported architecture: $(uname -m)\"\n      return 1\n      ;;\n  esac\n}\n\nAPP_NAME=\"ABDownloadManager\"\nPLATFORM=\"linux\"\nARCH=\"$(get_arch)\" || exit 1\nEXT=\"tar.gz\"\n\nRELEASE_URL=\"https://api.github.com/repos/amir1376/ab-download-manager/releases/latest\"\nGITHUB_RELEASE_DOWNLOAD=\"https://github.com/amir1376/ab-download-manager/releases/download\"\n\nLATEST_VERSION=$(curl -fSs \"${RELEASE_URL}\" | grep '\"tag_name\":' | sed -E 's/.*\"tag_name\": ?\"([^\"]+)\".*/\\1/')\n\nASSET_NAME=\"${APP_NAME}_${LATEST_VERSION:1}_${PLATFORM}_${ARCH}.${EXT}\"\nDOWNLOAD_URL=\"$GITHUB_RELEASE_DOWNLOAD/${LATEST_VERSION}/$ASSET_NAME\"\n\nAPP_PATH=\"$HOME/.local/$APP_NAME\"\nBINARY_PATH=\"$APP_PATH/bin/$APP_NAME\"\nICON_PATH=\"$APP_PATH/lib/$APP_NAME.png\"\n\n\n# --- Delete the old version Application if exists\ndelete_old_version() {\n    # Find the PID(s) of the application\n    PIDS=$(pidof \"$APP_NAME\") || true\n\n    if [ -n \"$PIDS\" ]; then\n        echo \"Found $APP_NAME with PID(s): $PIDS. Attempting to kill...\"\n\n        # Attempt to terminate the process gracefully\n        kill $PIDS 2>/dev/null || echo \"Graceful kill failed...\"\n\n        # Wait for a short period to allow graceful shutdown\n        sleep 2\n\n        # Check if the process is still running\n        PIDS=$(pidof \"$APP_NAME\") || true\n        if [ -n \"$PIDS\" ]; then\n            echo \"Process still running. Force killing...\"\n            kill -9 $PIDS 2>/dev/null || echo \"Force kill failed...\"\n        else\n            echo \"$APP_NAME terminated successfully.\"\n        fi\n    else\n        echo \"$APP_NAME is not running.\"\n    fi\n\n    # Remove old version directories\n    # First Remove link to \"$HOME/.local/$APP_NAME\"\n    remove_if_exists \"$HOME/.local/bin/$APP_NAME\"\n    # then Remove the main binary files directory\n    remove_if_exists \"$HOME/.local/$APP_NAME\"\n\n\n    # Log the removal action\n    logger \"Removed old version of $APP_NAME\"\n}\n\n# --- Generate a .desktop file for the app\ngenerate_desktop_file() {\n    cat <<EOF > \"$HOME/.local/share/applications/com.abdownloadmanager.desktop\"\n[Desktop Entry]\nName=AB Download Manager\nComment=Manage and organize your download files better than before\nGenericName=Downloader\nCategories=Utility;Network;\nExec=\"$BINARY_PATH\"\nIcon=$ICON_PATH\nTerminal=false\nType=Application\nStartupWMClass=com-abdownloadmanager-desktop-AppKt\nEOF\n}\n\n# --- Download the latest version of the app\ndownload_zip() {\n    # Remove the app tarball if it exists in /tmp\n    remove_if_exists \"/tmp/$ASSET_NAME\"\n\n    logger \"downloading AB Download Manager ...\"\n    # Perform the download with curl\n    if curl --progress-bar -fSL -o \"/tmp/$ASSET_NAME\" \"${DOWNLOAD_URL}\"; then\n        logger \"download finished successfully\"\n    else\n        logger error \"Download failed! Something Went Wrong\"\n        logger error \"Check Your Internet Connectivity\"\n        # Optionally remove the partially downloaded file\n        remove_if_exists \"/tmp/$ASSET_NAME\"\n    fi\n}\n\n\n# --- Install the app\ninstall_app() {\n\n    logger \"Installing AB Download Manager ...\"\n    # --- Setup ~/.local directories\n    mkdir -p \"$HOME/.local/bin\" \"$HOME/.local/share/applications\"\n    tar -xzf \"/tmp/$ASSET_NAME\" -C \"$HOME/.local\"\n\n    # --- remove tarball after installation\n    remove_if_exists \"/tmp/$ASSET_NAME\"\n\n    # Link the binary to ~/.local/bin\n    ln -sf \"$BINARY_PATH\" \"$HOME/.local/bin/$APP_NAME\"\n\n    # Create a .desktop file in ~/.local/share/applications\n    generate_desktop_file\n\n    logger \"AB Download Manager installed successfully\"\n    logger \"it can be found in Applications menu or run '$APP_NAME' in terminal\"\n    logger \"Make sure $HOME/.local/bin exists in PATH\"\n    logger \"installation logs saved in: ${LOG_FILE}\"\n    \n}\n\n# --- Check if the app is installed\ncheck_if_installed() {\n    local installed_version\n    installed_version=$($APP_NAME --version 2>/dev/null)\n    if [ -n \"$installed_version\" ]; then\n        echo \"$installed_version\"\n    else\n        echo \"\"\n    fi\n}\n\n# --- Update the app\nupdate_app() {\n    logger \"checking update\"\n    if [ \"$1\" != \"${LATEST_VERSION:1}\" ]; then\n        logger \"new version is available: v${LATEST_VERSION:1}. Updating...\"\n        download_zip\n        delete_old_version\n        install_app\n    else\n        logger \"You have the latest version installed.\"\n        exit 0\n    fi\n}\n\nmain() {\n    echo \"\" > \"$LOG_FILE\"\n    local installed_version\n    check_dependencies\n    installed_version=$(check_if_installed)\n    if [ -n \"$installed_version\" ]; then\n        logger \"AB Download Manager v$installed_version is currently installed.\"\n        update_app \"$installed_version\"\n    else\n        download_zip\n        install_app\n    fi\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "scripts/uninstall.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n# set -x\n\n\nAPP_NAME=\"ABDownloadManager\"\n\nLOG_FILE=\"/tmp/ab-dm-uninstaller.log\"\n\n# --- Custom Logger\nlogger() {\n  timestamp=$(date +\"%Y/%m/%d %H:%M:%S\")\n\n  if [[ \"$1\" == \"error\" ]]; then\n    # Red color for errors\n    echo -e \"${timestamp} -- ABDM-Uninstaller [Error]: \\033[0;31m$@\\033[0m\" | tee -a ${LOG_FILE}\n  else\n    # Default color for non-error messages\n    echo -e \"${timestamp} -- ABDM-Uninstaller [Info]: $@\" | tee -a ${LOG_FILE}\n  fi\n}\n\nremove_if_exists() {\n    local target=\"$1\"\n\n    if [ -z \"$target\" ]; then\n        logger \"No target specified in remove_if_exists function\"\n        return 1\n    fi\n\n    if [ -e \"$target\" ]; then\n        logger \"File \\\"$target\\\" Removed\"\n        rm -rf \"$target\"\n    else\n        logger \"File \\\"$target\\\" does not exist\"\n    fi\n}\n\ndelete_app_config_dir() {\n\n    local answer\n    read -p \"Do you want to continue? [Y/n]: \" -r answer\n    answer=${answer:-Y}  # Set default to 'Y' if no input is given\n\n    case $answer in\n        [Yy]* )\n            remove_if_exists \"$HOME/.abdm\"\n            ;;\n        [Nn]* )\n            logger \"Remove The $HOME/.abdm directory manually.\"\n            ;;\n        * )\n            logger error \"Please answer yes or no.\"\n            delete_app_config_dir\n            ;;\n    esac\n}\n\ndelete_app() {\n\n    # Find the PID(s) of the application\n    PIDS=$(pidof \"$APP_NAME\") || true\n\n    if [ -n \"$PIDS\" ]; then\n        echo \"Found $APP_NAME with PID(s): $PIDS. Attempting to kill...\"\n\n        # Attempt to terminate the process gracefully\n        kill $PIDS 2>/dev/null || echo \"Graceful kill failed...\"\n\n        # Wait for a short period to allow graceful shutdown\n        sleep 2\n\n        # Check if the process is still running\n        PIDS=$(pidof \"$APP_NAME\") || true\n        if [ -n \"$PIDS\" ]; then\n            echo \"Process still running. Force killing...\"\n            kill -9 $PIDS 2>/dev/null || echo \"Force kill failed...\"\n        else\n            echo \"$APP_NAME terminated successfully.\"\n        fi\n    else\n        echo \"$APP_NAME is not running.\"\n    fi\n\n    logger \"removing $APP_NAME desktop file ...\"\n    # --- Remove the .desktop file in ~/.local/share/applications\n    remove_if_exists \"$HOME/.local/share/applications/com.abdownloadmanager.desktop\"\n\n    logger \"removing $APP_NAME link ...\"\n    remove_if_exists \"$HOME/.local/bin/$APP_NAME\"\n\n    logger \"removing $APP_NAME binary ...\"\n    remove_if_exists \"$HOME/.local/$APP_NAME\"\n\n    logger \"removing $APP_NAME autostart at boot file ...\"\n    remove_if_exists \"$HOME/.config/autostart/com.abdownloadmanager.desktop\"\n\n    if [ -e \"$HOME/.abdm\" ]; then\n        logger \"removing $APP_NAME settings and download lists $HOME/.abdm\"\n        delete_app_config_dir\n    fi\n\n    logger \"AB Download Manager completely removed\"\n}\n\nmain() {\n  delete_app\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n}\n\nplugins{\n//    id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"0.8.0\"\n}\n\ndependencyResolutionManagement {\n    repositories {\n        mavenCentral()\n        google()\n        maven(\"https://maven.pkg.jetbrains.space/public/p/compose/dev\")\n    }\n}\n\nrootProject.name = \"ABDownloadManager\"\n\ninclude(\"android:app\")\ninclude(\"desktop:app\")\ninclude(\"desktop:app-utils\")\ninclude(\"desktop:shared\")\ninclude(\"desktop:mac_utils\")\ninclude(\"downloader:core\")\ninclude(\"downloader:monitor\")\ninclude(\"integration:server\")\ninclude(\"shared:utils\")\ninclude(\"shared:app\")\ninclude(\"shared:compose-utils\")\ninclude(\"shared:resources\")\ninclude(\"shared:resources:contracts\")\ninclude(\"shared:config\")\ninclude(\"shared:updater\")\ninclude(\"shared:auto-start\")\ninclude(\"shared:nanohttp4k\")\nincludeBuild(\"./compositeBuilds/shared\"){\n    name=\"build-shared\"\n}\nincludeBuild(\"./compositeBuilds/plugins\")\n"
  },
  {
    "path": "shared/app/build.gradle.kts",
    "content": "import buildlogic.versioning.getAppDataDirName\nimport buildlogic.versioning.getAppName\nimport buildlogic.versioning.getAppVersionString\nimport buildlogic.versioning.getApplicationPackageName\nimport buildlogic.versioning.getPrettifiedAppName\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(MyPlugins.composeBase)\n    id(Plugins.Kotlin.serialization)\n    id(Plugins.Android.library)\n    id(Plugins.buildConfig)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain.dependencies {\n            api(libs.compose.runtime)\n            api(libs.compose.foundation)\n            api(libs.compose.ui)\n\n            api(project(\":downloader:core\"))\n            api(project(\":downloader:monitor\"))\n\n            api(project(\":shared:config\"))\n            api(project(\":shared:utils\"))\n            api(project(\":shared:compose-utils\"))\n            api(project(\":shared:resources\"))\n            api(project(\":shared:auto-start\"))\n            api(project(\":shared:updater\"))\n\n            api(libs.kotlin.coroutines.core)\n            api(libs.kotlin.serialization.json)\n\n            api(libs.decompose)\n            api(libs.essenty.lifecycleCoroutines)\n            api(libs.koin.core)\n\n            api(libs.androidx.datastore)\n\n            implementation(libs.kotlinFileWatcher)\n\n            //because we don't have material design, but we use ripple effect\n            implementation(libs.compose.material.rippleEffect)\n\n            // multiplatform scrollbars\n            api(libs.fastscroller.core)\n            api(libs.markdownRenderer.core)\n            api(libs.compose.reorderable)\n        }\n        androidMain.dependencies {\n            api(libs.androidx.core.ktx)\n            api(libs.androidx.activity.compose)\n        }\n        val desktopMain by getting\n        desktopMain.dependencies {\n            implementation(libs.osThemeDetector.get().toString()) {\n                exclude(group = \"net.java.dev.jna\")\n            }\n        }\n    }\n}\n\nandroid {\n    compileSdk = 36\n    namespace = \"com.abdownloadmanager.shared\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n// generate a file with these constants\nbuildConfig {\n    packageName = \"com.abdownloadmanager.shared\"\n    buildConfigField(\n        \"PACKAGE_NAME\",\n        provider {\n            getApplicationPackageName()\n        }\n    )\n    buildConfigField(\n        \"APP_DISPLAY_NAME\",\n        provider { getPrettifiedAppName() }\n    )\n    buildConfigField(\n        \"DATA_DIR_NAME\",\n        provider { getAppDataDirName() }\n    )\n    buildConfigField(\n        \"APP_VERSION\",\n        provider { getAppVersionString() }\n    )\n    buildConfigField(\n        \"APP_NAME\",\n        provider { getAppName() }\n    )\n    buildConfigField(\n        \"PROJECT_WEBSITE\",\n        provider {\n            \"https://abdownloadmanager.com\"\n        }\n    )\n    buildConfigField(\n        \"PROJECT_SOURCE_CODE\",\n        provider {\n            \"https://github.com/amir1376/ab-download-manager\"\n        }\n    )\n    buildConfigField(\n        \"DONATE_LINK\",\n        provider {\n            \"https://github.com/amir1376/ab-download-manager/blob/master/DONATE.md\"\n        }\n    )\n    buildConfigField(\n        \"PROJECT_GITHUB_OWNER\",\n        provider {\n            \"amir1376\"\n        }\n    )\n    buildConfigField(\n        \"PROJECT_GITHUB_REPO\",\n        provider {\n            \"ab-download-manager\"\n        }\n    )\n    buildConfigField(\n        \"PROJECT_TRANSLATIONS\",\n        provider {\n            \"https://crowdin.com/project/ab-download-manager\"\n        }\n    )\n    buildConfigField(\n        \"INTEGRATION_CHROME_LINK\",\n        provider {\n            \"https://chromewebstore.google.com/detail/ab-download-manager-brows/bbobopahenonfdgjgaleledndnnfhooj\"\n        }\n    )\n    buildConfigField(\n        \"INTEGRATION_FIREFOX_LINK\",\n        provider {\n            \"https://addons.mozilla.org/en-US/firefox/addon/ab-download-manager/\"\n        }\n    )\n    buildConfigField(\n        \"TELEGRAM_GROUP\",\n        provider {\n            \"https://t.me/abdownloadmanager_discussion\"\n        }\n    )\n    buildConfigField(\n        \"TELEGRAM_CHANNEL\",\n        provider {\n            \"https://t.me/abdownloadmanager\"\n        }\n    )\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/ui/modifier/PointerHoverIcon.android.kt",
    "content": "package com.abdownloadmanager.shared.ui.modifier\n\nimport androidx.compose.ui.Modifier\n\nactual fun Modifier.myPointerHoverIcon(pointerHoverIcon: MyPointerHoverIcon, overrideDescendants: Boolean): Modifier {\n    // No-op\n    return this\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/ui/theme/PlatformThemeDefinitions.android.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.MyShapes\nimport com.abdownloadmanager.shared.util.ui.theme.MySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.TextSizes\nimport io.github.oikvpqya.compose.fastscroller.ScrollbarStyle\nimport io.github.oikvpqya.compose.fastscroller.ThumbStyle\nimport io.github.oikvpqya.compose.fastscroller.TrackStyle\n\n@Composable\nactual fun PlatformDependentProviders(content: @Composable (() -> Unit)) {\n    CompositionLocalProvider(\n        // no providers yet,\n        content = content,\n    )\n}\n\n@Composable\nactual fun myPlatformScrollbarStyle(): ScrollbarStyle {\n    val shape = RoundedCornerShape(4.dp)\n    return ScrollbarStyle(\n        minimalHeight = 16.dp,\n        thickness = 6.dp,\n        thumbStyle = ThumbStyle(\n            shape = shape,\n            unhoverColor = myColors.onBackground / 10,\n            hoverColor = myColors.onBackground / 30,\n        ),\n        trackStyle = TrackStyle(\n            unhoverColor = Color.Transparent,\n            hoverColor = Color.Transparent,\n            shape = RectangleShape,\n        ),\n        hoverDurationMillis = 300,\n    )\n}\n\n\nprivate val androidTextSizes = TextSizes(\n    xs = 10.sp,\n    sm = 12.sp,\n    base = 14.sp,\n    lg = 16.sp,\n    xl = 18.sp,\n    x2l = 20.sp,\n    x3l = 22.sp,\n    x4l = 24.sp,\n    x5l = 26.sp,\n)\n\nprivate val androidShapes = MyShapes(\n    defaultRounded = RoundedCornerShape(12.dp),\n)\n\n@Composable\nactual fun myPlatformTextSizes(): TextSizes {\n    return androidTextSizes\n}\n\n@Composable\nactual fun myPlatformShapes(): MyShapes {\n    return androidShapes\n}\nprivate val androidSpacings = MySpacings(\n    thumbSize = 48.dp,\n    iconSize = 24.dp,\n    smallSpace = 4.dp,\n    mediumSpace = 8.dp,\n    largeSpace = 16.dp,\n)\n\n@Composable\nactual fun myPlatformSpacing(): MySpacings {\n    return androidSpacings\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/ui/widget/Tooltip.android.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.awaitLongPressOrCancellation\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.pointerInput\n\nactual fun Modifier.detectTooltip(\n    state: MutableState<Boolean>,\n): Modifier = pointerInput(Unit) {\n    awaitEachGesture {\n        val down = awaitFirstDown(\n            requireUnconsumed = false,\n            pass = PointerEventPass.Main\n        )\n\n        val longPress = awaitLongPressOrCancellation(down.id)\n\n        if (longPress != null) {\n            state.value = true\n\n            down.consume()\n\n            // Consume everything until release\n            while (true) {\n                val event = awaitPointerEvent(PointerEventPass.Initial)\n                event.changes.forEach { it.consume() }\n\n                if (event.changes.all { !it.pressed }) {\n                    break\n                }\n            }\n\n            state.value = false\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/Option.android.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\nimport ir.amirab.util.compose.action.MenuItem\n\n/**\n * TODO (KMP) implement it based on design\n * it's our context menu!\n */\n@Composable\nactual fun ShowOptionsInPopup(\n    menu: MenuItem.SubMenu,\n    onDismissRequest: () -> Unit\n) {\n    Popup(\n        popupPositionProvider = rememberMyComponentRectPositionProvider(\n            alignment = Alignment.BottomEnd\n        ),\n        onDismissRequest = onDismissRequest\n    ) {\n        RenderOptions(menu, onDismissRequest)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/WithContextMenu.android.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\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.geometry.Offset\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.shared.ui.widget.rememberMyPopupPositionProviderAtPosition\nimport ir.amirab.util.compose.action.MenuItem\n\n@Composable\nactual fun WithContextMenu(\n    menuProvider: () -> List<MenuItem>,\n    modifier: Modifier,\n    content: @Composable (() -> Unit)\n) {\n    val menu = remember(menuProvider) {\n        mutableStateOf(emptyList<MenuItem>())\n    }\n    val onDismissRequest = {\n        menu.value = emptyList()\n    }\n    var lastClickPosition: Offset? by remember {\n        mutableStateOf(null)\n    }\n    Box(\n        modifier.onLongPress {\n            menu.value = menuProvider()\n            lastClickPosition = it\n        }\n    ) {\n        content()\n        if (menu.value.isNotEmpty()) {\n            Popup(\n                popupPositionProvider = rememberMyPopupPositionProviderAtPosition(\n                    positionPx = lastClickPosition\n                    // shouldn't happen! just to not use !!\n                        ?: Offset.Zero,\n                    alignment = Alignment.BottomEnd\n                ),\n                onDismissRequest = onDismissRequest\n            ) {\n                SubMenu(\n                    subMenu = menu.value,\n                    onRequestClose = onDismissRequest,\n                )\n            }\n        }\n    }\n}\n\nprivate fun Modifier.onLongPress(\n    onLongPress: (Offset) -> Unit\n): Modifier = pointerInput(Unit) {\n    detectTapGestures(\n        onLongPress = { onLongPress(it) }\n    )\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/util/ClipboardUtil.android.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport androidx.core.content.getSystemService\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\n\nactual object ClipboardUtil : KoinComponent {\n    private val context: Context by inject()\n    private val clipboardManager get() = context.getSystemService<ClipboardManager>()\n    actual fun copy(text: String) {\n        runCatching {\n            clipboardManager?.let {\n                val clip = ClipData.newPlainText(\"Copied Text\", text)\n                it.setPrimaryClip(clip)\n            }\n        }\n    }\n\n    actual fun read(): String? {\n        return runCatching {\n            clipboardManager?.let {\n                val clip: ClipData? = it.primaryClip\n                if (clip != null && clip.itemCount > 0) {\n                    return clip.getItemAt(0).coerceToText(context)\n                        .toString()\n                }\n                return null\n            }\n        }.getOrNull()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/util/DesktopDiskStat.android.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.downloader.utils.IDiskStat\nimport java.io.File\n\nactual typealias PlatformDiskStat = AndroidDiskStat\n\nclass AndroidDiskStat : IDiskStat {\n    override fun getRemainingSpace(path: File): Long {\n        return path.freeSpace\n    }\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/util/PlatformThemeDetector.android.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\nimport kotlinx.coroutines.channels.awaitClose\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.callbackFlow\n\nactual typealias PlatformThemeDetector = AndroidSystemThemeDetector\n\nclass AndroidSystemThemeDetector(\n    private val context: Context,\n) : ISystemThemeDetector {\n    override val isSupported: Boolean = true\n    override fun isDark(): Boolean {\n        return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\n    }\n\n    override val systemThemeFlow: Flow<Boolean> = callbackFlow {\n        trySend(isDark())\n        val callback = GlobalActivityLifecycleCallbacks {\n            trySend(isDark())\n        }\n        val application = context.applicationContext as? Application\n        application?.registerActivityLifecycleCallbacks(callback)\n        awaitClose {\n            application?.unregisterActivityLifecycleCallbacks(callback)\n        }\n    }\n}\n\nprivate class GlobalActivityLifecycleCallbacks(\n    private val recheck: () -> Unit,\n) : Application.ActivityLifecycleCallbacks {\n    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {\n        recheck()\n    }\n\n    override fun onActivityDestroyed(activity: Activity) {\n    }\n\n    override fun onActivityPaused(activity: Activity) {\n    }\n\n    override fun onActivityResumed(activity: Activity) {\n    }\n\n    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {\n    }\n\n    override fun onActivityStarted(activity: Activity) {}\n\n    override fun onActivityStopped(activity: Activity) {}\n\n}\n\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/PlatformDownloadLocationProvider.android.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport android.os.Environment\nimport com.abdownloadmanager.shared.util.SystemDownloadLocationProvider\nimport java.io.File\n\nclass AndroidDownloadLocationProvider : SystemDownloadLocationProvider() {\n    override fun getCommonDownloadLocation(): File {\n        return Environment\n            .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)\n            .absoluteFile\n    }\n\n    override fun getCurrentDownloadLocation(): File {\n        return getCommonDownloadLocation()\n    }\n\n}\n\nactual fun getPlatformDownloadLocationProvider(): SystemDownloadLocationProvider {\n    return AndroidDownloadLocationProvider()\n}\n"
  },
  {
    "path": "shared/app/src/androidMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/MPBackHandler.android.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.runtime.Composable\n\n@Composable\nactual fun MPBackHandler(isEnabled: Boolean, onBack: () -> Unit) {\n    BackHandler(enabled = isEnabled, onBack = onBack)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/action/CommonActionFactories.kt",
    "content": "package com.abdownloadmanager.shared.action\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.AboutPageManager\nimport com.abdownloadmanager.shared.pagemanager.AddDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.BatchDownloadPageManager\nimport com.abdownloadmanager.shared.pagemanager.EnterNewURLDialogManager\nimport com.abdownloadmanager.shared.pagemanager.ExitApplicationRequestManager\nimport com.abdownloadmanager.shared.pagemanager.NewQueuePageManager\nimport com.abdownloadmanager.shared.pagemanager.OpenSourceLibrariesPageManager\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pagemanager.SettingsPageManager\nimport com.abdownloadmanager.shared.pagemanager.TranslatorsPageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.updater.UpdateComponent\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.SharedConstants\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.DownloadCredentialFromStringExtractor\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.DownloadCredentialsFromCurl\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.queue.inactiveQueuesFlow\nimport ir.amirab.util.URLOpener\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.combineStateFlows\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.launch\n\n\nfun createNewDownloadAction(\n    enterNewURLDialogManager: EnterNewURLDialogManager,\n): AnAction {\n    return simpleAction(\n        Res.string.new_download.asStringSource(),\n        MyIcons.add,\n    ) {\n        enterNewURLDialogManager.openEnterNewURLWindow()\n    }\n}\nfun createDownloadFromClipboardAction(\n    addDownloadDialogManager: AddDownloadDialogManager,\n): AnAction {\n    return simpleAction(\n        Res.string.import_from_clipboard.asStringSource(),\n        MyIcons.paste,\n    ) {\n        val contentsInClipboard = ClipboardUtil.read()\n        if (contentsInClipboard.isNullOrEmpty()) {\n            return@simpleAction\n        }\n        val curlItems = DownloadCredentialsFromCurl.extract(contentsInClipboard)\n        if (curlItems.isNotEmpty()) {\n            addDownloadDialogManager.openAddDownloadDialog(\n                curlItems.map {\n                    AddDownloadCredentialsInUiProps(it)\n                }\n            )\n            return@simpleAction\n        }\n        val items: List<IDownloadCredentials> = DownloadCredentialFromStringExtractor\n            .extract(contentsInClipboard)\n            .distinctBy { it.link }\n        if (items.isEmpty()) {\n            return@simpleAction\n        }\n        addDownloadDialogManager.openAddDownloadDialog(items.map {\n            AddDownloadCredentialsInUiProps(it)\n        })\n    }\n}\n\nfun createOpenBatchDownloadAction(\n    batchDownloadPageManager: BatchDownloadPageManager\n): AnAction {\n    return simpleAction(\n        title = Res.string.batch_download.asStringSource(),\n        icon = MyIcons.download\n    ) {\n        batchDownloadPageManager.openBatchDownloadPage()\n    }\n}\n\n\nfun createStopQueueGroupAction(\n    scope: CoroutineScope,\n    activeQueuesFlow: StateFlow<List<DownloadQueue>>\n): MenuItem.SubMenu {\n    return MenuItem.SubMenu(\n        icon = MyIcons.queueStop,\n        title = Res.string.stop_queue.asStringSource(),\n        items = emptyList()\n    ).apply {\n        activeQueuesFlow\n            .onEach {\n                setItems(it.map {\n                    createStopQueueAction(scope, it)\n                })\n            }.launchIn(scope)\n    }\n}\nfun createStartQueueGroupAction(\n    scope: CoroutineScope,\n    queueManager: QueueManager,\n): MenuItem.SubMenu {\n    return MenuItem.SubMenu(\n        icon = MyIcons.queueStart,\n        title = Res.string.start_queue.asStringSource(),\n        items = emptyList()\n    ).apply {\n        queueManager\n            .inactiveQueuesFlow()\n            .onEach {\n                setItems(it.map {\n                    createStartQueueAction(scope, it)\n                })\n            }.launchIn(scope)\n    }\n}\n\n// ui exit\nfun createRequestExitAction(\n    scope: CoroutineScope,\n    exitAppRequestManager: ExitApplicationRequestManager\n): AnAction {\n    return simpleAction(\n        Res.string.exit.asStringSource(),\n        MyIcons.exit,\n    ) {\n        scope.launch { exitAppRequestManager.requestExitApp() }\n    }\n}\n\n\nfun createPerHostSettingsPage(\n    perHostSettingsPageManager: PerHostSettingsPageManager\n): AnAction {\n    return simpleAction(\n        Res.string.settings_per_host_settings.asStringSource(),\n        MyIcons.earth,\n    ) {\n        perHostSettingsPageManager.openPerHostSettings(null)\n    }\n}\n\n\nfun createOpenSettingsAction(\n    settingsPageManager: SettingsPageManager\n): AnAction {\n    return simpleAction(\n        Res.string.settings.asStringSource(),\n        MyIcons.settings,\n    ) {\n        settingsPageManager.openSettings()\n    }\n}\n\nfun createCheckForUpdateAction(\n    updaterComponent: UpdateComponent\n): AnAction {\n    return simpleAction(\n        title = Res.string.update_check_for_update.asStringSource(),\n        icon = MyIcons.refresh,\n        checkEnable = MutableStateFlow(\n            updaterComponent.isUpdateSupported()\n        )\n    ) {\n        updaterComponent.requestCheckForUpdate()\n    }\n}\n\n\nfun createOpenAboutPage(aboutPageManager: AboutPageManager): AnAction {\n    return simpleAction(\n        title = Res.string.about.asStringSource(),\n        icon = MyIcons.info,\n    ) {\n        aboutPageManager.openAboutPage()\n    }\n}\n\nfun createOpenOpenSourceThirdPartyLibrariesPage(\n    openSourceLibrariesPageManager: OpenSourceLibrariesPageManager,\n): AnAction {\n    return simpleAction(\n        title = Res.string.view_the_open_source_licenses.asStringSource(),\n        icon = MyIcons.openSource,\n    ) {\n        openSourceLibrariesPageManager.openOpenSourceLibrariesPage()\n    }\n}\n\nfun createOpenTranslatorsPageAction(\n    opeTranslatorsPageManager: TranslatorsPageManager,\n): AnAction {\n    return simpleAction(\n        title = Res.string.meet_the_translators.asStringSource(),\n        icon = MyIcons.language,\n    ) {\n        opeTranslatorsPageManager.openTranslatorsPage()\n    }\n}\n\nval donate = simpleAction(\n    title = Res.string.donate.asStringSource(),\n    icon = MyIcons.hearth,\n) {\n    URLOpener.openUrl(SharedConstants.donateLink)\n}\n\nval supportActionGroup = MenuItem.SubMenu(\n    title = Res.string.support_and_community.asStringSource(),\n    icon = MyIcons.group,\n    items = buildMenu {\n        item(Res.string.website.asStringSource(), MyIcons.appIcon) {\n            URLOpener.openUrl(SharedConstants.projectWebsite)\n        }\n        item(Res.string.source_code.asStringSource(), MyIcons.openSource) {\n            URLOpener.openUrl(SharedConstants.projectSourceCode)\n        }\n        subMenu(Res.string.telegram.asStringSource(), MyIcons.telegram) {\n            item(Res.string.channel.asStringSource(), MyIcons.speaker) {\n                URLOpener.openUrl(SharedConstants.telegramChannelUrl)\n            }\n            item(Res.string.group.asStringSource(), MyIcons.group) {\n                URLOpener.openUrl(SharedConstants.telegramGroupUrl)\n            }\n        }\n    }\n)\n\nfun createOpenQueuesAction(\n    queuePageManager: QueuePageManager\n): AnAction {\n    return simpleAction(\n        title = Res.string.queues.asStringSource(),\n        icon = MyIcons.queue\n    ) {\n        queuePageManager.openQueues()\n    }\n}\n\n\nfun createMoveToQueueAction(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    queue: DownloadQueue,\n    itemId: List<Long>,\n): AnAction {\n    return simpleAction(queue.getQueueModel().name.asStringSource()) {\n        scope.launch {\n            downloadSystem\n                .queueManager\n                .addToQueue(\n                    queueId = queue.id,\n                    downloadIds = itemId,\n                )\n        }\n    }\n}\n\nfun createMoveToCategoryAction(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    category: Category,\n    itemIds: List<Long>,\n): AnAction {\n    return simpleAction(category.name.asStringSource()) {\n        scope.launch {\n            downloadSystem\n                .categoryManager\n                .addItemsToCategory(\n                    categoryId = category.id,\n                    itemIds = itemIds,\n                )\n        }\n    }\n}\n\nfun createStopQueueAction(\n    scope: CoroutineScope,\n    queue: DownloadQueue,\n): AnAction {\n    return simpleAction(queue.getQueueModel().name.asStringSource()) {\n        scope.launch {\n            queue.stop()\n        }\n    }\n}\n\nfun createStartQueueAction(\n    scope: CoroutineScope,\n    queue: DownloadQueue,\n): AnAction {\n    return simpleAction(queue.getQueueModel().name.asStringSource()) {\n        scope.launch {\n            queue.start()\n        }\n    }\n}\n\nfun createNewQueueAction(\n    scope: CoroutineScope,\n    queuePageManager: NewQueuePageManager,\n): AnAction {\n    return simpleAction(Res.string.add_new_queue.asStringSource()) {\n        scope.launch {\n            queuePageManager.openNewQueueDialog()\n        }\n    }\n}\nfun createStopAllAction(\n    scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    extraJobs: () -> Unit,\n    activeQueuesFlow: StateFlow<List<DownloadQueue>>\n): AnAction {\n    return simpleAction(\n        Res.string.stop_all.asStringSource(),\n        MyIcons.stop,\n        checkEnable = combineStateFlows(\n            downloadSystem.downloadMonitor.activeDownloadCount,\n            activeQueuesFlow\n        ) { downloadCount, activeQueues ->\n            downloadCount > 0 || activeQueues.isNotEmpty()\n        }\n    ) {\n        scope.launch {\n            downloadSystem.stopAnything()\n            extraJobs()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/action/DevActionFactories.kt",
    "content": "package com.abdownloadmanager.shared.action\n\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.action.AnAction\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\n\n\nfun createDummyExceptionAction(): AnAction {\n    return simpleAction(\n        \"Dummy Exception\".asStringSource(),\n        MyIcons.info\n    ) {\n        error(\"This is a dummy exception that is thrown by developer\")\n    }\n}\n\nfun createDummyMessageAction(\n    notificationSender: NotificationSender,\n): MenuItem.SubMenu {\n    return MenuItem.SubMenu(\n        title = \"Show Dialog Message\".asStringSource(),\n        icon = MyIcons.info,\n        items = listOf(\n            MessageDialogType.Info,\n            MessageDialogType.Error,\n            MessageDialogType.Warning,\n            MessageDialogType.Success,\n        ).map {\n            createDummyMessage(it, notificationSender)\n        }\n    )\n}\n\nprivate fun createDummyMessage(\n    type: MessageDialogType,\n    notificationSender: NotificationSender,\n): AnAction {\n    return simpleAction(\n        \"$type Message\".asStringSource(),\n        MyIcons.info,\n    ) {\n        notificationSender.sendDialogNotification(\n            type = type,\n            title = \"Dummy Message\".asStringSource(),\n            description = \"This is a test message\".asStringSource()\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/BasicDownloadItem.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\ndata class BasicDownloadItem(\n    var folder: String,\n    var name: String,\n    var contentLength: Long = -1,\n    var preferredConnectionCount: Int? = null,\n    var speedLimit: Long = 0,\n    var fileChecksum: String? = null\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/CredentialAndItemMapper.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\n/**\n * the resulting value is a copied object so implementations doesn't have side effects\n */\ninterface CredentialAndItemMapper<TCredentials : IDownloadCredentials, TDownloadItem : IDownloadItem> {\n    fun itemToCredentials(item: TDownloadItem): TCredentials\n    fun appliedCredentialsToItem(item: TDownloadItem, credentials: TCredentials): TDownloadItem\n\n    // I believe that these two are redundant it needs to be improved\n    fun itemWithEditedName(item: TDownloadItem, name: String): TDownloadItem\n    fun credentialsWithEditedLink(credentials: TCredentials, link: String): TCredentials\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/DownloadSize.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.abdownloadmanager.shared.util.LocalSizeUnit\nimport com.abdownloadmanager.shared.util.convertDurationToHumanReadable\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.datasize.ConvertSizeConfig\n\nsealed class DownloadSize : Comparable<DownloadSize> {\n    abstract fun comparePriority(): Int\n    abstract fun plus(other: DownloadSize): DownloadSize\n\n    data class Bytes(\n        val bytes: Long,\n    ) : DownloadSize() {\n        fun asStringSource(\n            sizeUnit: ConvertSizeConfig\n        ): StringSource {\n            return convertPositiveSizeToHumanReadable(bytes, sizeUnit)\n        }\n\n        override fun comparePriority() = 2\n        override fun plus(other: DownloadSize): DownloadSize {\n            return if (other !is Bytes || other.bytes == 0L) {\n                this\n            } else {\n                return Bytes(bytes + other.bytes)\n            }\n        }\n\n        override fun compareTo(other: DownloadSize): Int {\n            if (other is Bytes) {\n                return bytes.compareTo(other.bytes)\n            }\n            return super.compareTo(other)\n        }\n        companion object{\n            val Zero = Bytes(0)\n        }\n    }\n\n    data class Duration(val duration: Double) : DownloadSize() {\n        fun asStringSource(): StringSource {\n            return convertDurationToHumanReadable(duration)\n        }\n\n        override fun comparePriority() = 1\n        override fun plus(other: DownloadSize): DownloadSize {\n            return if (other !is Duration || other.duration == 0.0) {\n                this\n            } else {\n                Duration(duration + other.duration)\n            }\n        }\n\n        override fun compareTo(other: DownloadSize): Int {\n            if (other is Duration) {\n                return duration.compareTo(other.duration)\n            }\n            return super.compareTo(other)\n        }\n        companion object{\n            val Zero = Duration(0.0)\n        }\n    }\n\n    override fun compareTo(other: DownloadSize): Int {\n        return comparePriority().compareTo(other.comparePriority())\n    }\n}\n\n@Composable\nfun DownloadSize.rememberString(): String {\n    return when (this) {\n        is DownloadSize.Bytes -> {\n            val sizeUnit = LocalSizeUnit.current\n            remember(this, sizeUnit) {\n                asStringSource(sizeUnit)\n            }\n        }\n\n        is DownloadSize.Duration -> {\n            remember(this) {\n                asStringSource()\n            }\n        }\n    }.rememberString()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/DownloadUiChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport com.abdownloadmanager.shared.downloaderinui.add.AddDownloadChecker\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.flow.onEachLatest\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.*\n\nabstract class DownloadUiChecker<\n        TCredentials : IDownloadCredentials,\n        TResponseInfoType : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfoType, TDownloadSize>,\n        >(\n    initialCredentials: TCredentials,\n    linkCheckerFactory: LinkCheckerFactory<TCredentials, TResponseInfoType, TDownloadSize, TLinkChecker>,\n    initialFolder: String,\n    initialName: String = \"\",\n    downloadSystem: DownloadSystem,\n    scope: CoroutineScope,\n) {\n    val credentials = MutableStateFlow(initialCredentials)\n    val name = MutableStateFlow(initialName)\n    val folder = MutableStateFlow(initialFolder)\n\n    protected val linkChecker = linkCheckerFactory.createLinkChecker(credentials.value)\n    protected val addDownloadChecker = AddDownloadChecker(\n        linkChecker = linkChecker,\n        initialName = name.value,\n        initialFolder = folder.value,\n        downloadSystem = downloadSystem,\n        parentScope = scope,\n    )\n    val downloadSize: StateFlow<TDownloadSize?> = linkChecker.downloadSize\n\n    val gettingResponseInfo = linkChecker.isLoading\n    val responseInfo = linkChecker.responseInfo\n\n    val canAddToDownloadResult = addDownloadChecker.canAddResult\n    val canAdd = addDownloadChecker.canAdd\n    val isDuplicate = addDownloadChecker.isDuplicate\n\n    private val refreshResponseInfoImmediately = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n    private val scheduleRefreshResponseInfo = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n    private val scheduleRecheckAddToDownloadIsPossible = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n\n    fun refresh() {\n        refreshResponseInfoImmediately.tryEmit(Unit)\n    }\n\n    private fun scheduleRefresh(\n        alsoRecheckLink: Boolean\n    ) {\n        if (alsoRecheckLink) {\n            scheduleRefreshResponseInfo.tryEmit(Unit)\n        }\n        scheduleRecheckAddToDownloadIsPossible.tryEmit(Unit)\n    }\n\n    init {\n        merge(\n            scheduleRefreshResponseInfo.debounce(500),\n            refreshResponseInfoImmediately\n        ).onEachLatest {\n            linkChecker.check()\n        }.launchIn(scope)\n        merge(\n            scheduleRecheckAddToDownloadIsPossible,\n//            ...\n        ).onEachLatest {\n            addDownloadChecker.check()\n        }.launchIn(scope)\n\n\n\n        linkChecker.suggestedName\n            .onEach {\n                it?.let { name ->\n                    this.name.update { name }\n                }\n            }.launchIn(scope)\n\n\n        credentials.onEach { credentials ->\n            linkChecker.credentials.update { credentials }\n            scheduleRefresh(alsoRecheckLink = true)\n        }.launchIn(scope)\n        name.onEach { name ->\n            if (addDownloadChecker.name.value != name) {\n                addDownloadChecker.name.update { name }\n                scheduleRefresh(alsoRecheckLink = false)\n            }\n        }.launchIn(scope)\n        folder.onEach { folder ->\n            if (addDownloadChecker.folder.value != folder) {\n                addDownloadChecker.folder.update { folder }\n                scheduleRefresh(alsoRecheckLink = false)\n            }\n        }.launchIn(scope)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/DownloaderInUi.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputsFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadCheckerFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputsFactory\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.Downloader\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.DownloadItemStateFactory\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemFactoryInputs\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.CoroutineScope\n\n/**\n * This is a class that represent a downloader implementation details tight to the Application not just the downloader logic\n * including ui, component factories and every thing that app need work with\n */\nabstract class DownloaderInUi<\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>,\n        TDownloadItem : IDownloadItem,\n        TNewDownloadInputs : NewDownloadInputs<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>,\n        TEditDownloadInputs : EditDownloadInputs<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker, TCredentialAndItemMapper>,\n        TCredentialAndItemMapper : CredentialAndItemMapper<TCredentials, TDownloadItem>,\n        TDownloadJob : DownloadJob,\n        TDownloader : Downloader<TDownloadItem, TDownloadJob, TCredentials>\n        >(\n    val downloader: TDownloader\n) :\n    LinkCheckerFactory<TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>,\n    EditDownloadCheckerFactory<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>,\n    NewDownloadInputsFactory<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker, TNewDownloadInputs>,\n    EditDownloadInputsFactory<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker, TCredentialAndItemMapper, TEditDownloadInputs>,\n    DownloadItemStateFactory<TDownloadItem, TDownloadJob> {\n    abstract fun newDownloadUiChecker(\n        initialCredentials: TCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope,\n    ): DownloadUiChecker<TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>\n\n\n    abstract fun acceptDownloadCredentials(item: IDownloadCredentials): Boolean\n    abstract fun supportsThisLink(link: String): Boolean\n    abstract fun createMinimumCredentials(link: String): TCredentials\n    abstract fun createBareDownloadItem(\n        credentials: TCredentials,\n        basicDownloadItem: BasicDownloadItem\n    ): TDownloadItem\n\n    abstract override fun createProcessingDownloadItemState(\n        props: ProcessingDownloadItemFactoryInputs<TDownloadJob>,\n    ): ProcessingDownloadItemState\n\n    override fun createCompletedDownloadItemState(\n        downloadItem: TDownloadItem,\n    ): CompletedDownloadItemState {\n        return CompletedDownloadItemState.fromDownloadItem(downloadItem)\n    }\n\n    abstract val name: StringSource\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/DownloaderInUiRegistry.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport ir.amirab.downloader.Downloader\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.DownloadItemStateFactory\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemFactoryInputs\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport kotlin.reflect.KClass\n\ntypealias TADownloaderInUI = DownloaderInUi<\n        IDownloadCredentials,\n        IResponseInfo,\n        DownloadSize,\n        LinkChecker<IDownloadCredentials, IResponseInfo, DownloadSize>,\n        IDownloadItem,\n        NewDownloadInputs<IDownloadItem, IDownloadCredentials, IResponseInfo, DownloadSize, LinkChecker<IDownloadCredentials, IResponseInfo, DownloadSize>>,\n        EditDownloadInputs<IDownloadItem, IDownloadCredentials, IResponseInfo, DownloadSize, LinkChecker<IDownloadCredentials, IResponseInfo, DownloadSize>, CredentialAndItemMapper<IDownloadCredentials, IDownloadItem>>,\n        CredentialAndItemMapper<IDownloadCredentials, IDownloadItem>,\n        DownloadJob,\n        Downloader<IDownloadItem, DownloadJob, IDownloadCredentials>>\n\nclass DownloaderInUiRegistry\n    : DownloadItemStateFactory<IDownloadItem, DownloadJob> {\n    private val list = mutableListOf<TADownloaderInUI>()\n    private val componentHashes = hashMapOf<Any, TADownloaderInUI>()\n    fun add(downloaderInUi: DownloaderInUi<*, *, *, *, *, *, *, *, *, *>) {\n        // the compiler gave me error when I add these two generics (TDownloadJob, TDownloader) into the DownloaderInUi\n        val element = downloaderInUi as TADownloaderInUI\n        @Suppress(\"UNCHECKED_CAST\")\n        list.add(element)\n        getComponentsOf(element).forEach {\n            componentHashes[it] = element\n        }\n    }\n\n    fun remove(downloaderInUi: DownloaderInUi<*, *, *, *, *, *, *, *, *, *>) {\n        list.remove(downloaderInUi)\n        @Suppress(\"UNCHECKED_CAST\")\n        getComponentsOf(\n            downloaderInUi as TADownloaderInUI\n        ).forEach {\n            componentHashes.remove(it)\n        }\n    }\n\n    fun getDownloaderOf(downloadItem: IDownloadCredentials): TADownloaderInUI? {\n        componentHashes.get(downloadItem::class)?.let {\n            // fast path without iterating the list\n            return it\n        }\n        // IDownloadCredentials is an intermediate interface so we should iterate the list instead\n        return list.firstOrNull {\n            it.acceptDownloadCredentials(downloadItem)\n        }\n    }\n\n    private fun getDownloaderOf(downloadJob: DownloadJob): TADownloaderInUI? {\n        return getDownloaderOf(downloadJob.downloadItem)\n    }\n\n    fun bestMatchForThisLink(link: String): TADownloaderInUI? {\n        return list.firstOrNull {\n            it.supportsThisLink(link)\n        }\n    }\n\n    fun getAll(): List<TADownloaderInUI> {\n        return list.toList()\n    }\n\n\n    override fun createProcessingDownloadItemState(\n        props: ProcessingDownloadItemFactoryInputs<DownloadJob>,\n    ): ProcessingDownloadItemState {\n        val downloadJob = props.downloadJob\n        return requireNotNull(getDownloaderOf(downloadJob)) {\n            \"there is no downloader in UI registered for this download job: ${downloadJob::class.qualifiedName}\"\n        }.createProcessingDownloadItemState(props)\n    }\n\n    override fun createCompletedDownloadItemState(downloadItem: IDownloadItem): CompletedDownloadItemState {\n        return requireNotNull(getDownloaderOf(downloadItem)) {\n            \"there is no downloader in UI registered for this download item: ${downloadItem::class.qualifiedName}\"\n        }.createCompletedDownloadItemState(downloadItem)\n    }\n\n    companion object {\n        private fun getComponentsOf(element: TADownloaderInUI): List<KClass<out Any>> {\n            return listOf(\n                element.downloader.downloadJobClass,\n                element.downloader.downloadItemClass,\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/LinkChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nabstract class LinkChecker<\n        Credentials : IDownloadCredentials,\n        ResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        >(\n    initialCredentials: Credentials\n) {\n    //input\n    val credentials = MutableStateFlow(initialCredentials)\n\n    abstract val suggestedName: StateFlow<String?>\n    abstract val downloadSize: StateFlow<TDownloadSize?>\n\n    private val _isLoading = MutableStateFlow(false)\n    val isLoading = _isLoading.asStateFlow()\n    private val _responseInfo = MutableStateFlow<ResponseInfo?>(null)\n    val responseInfo = _responseInfo.asStateFlow()\n\n    private val _isValid = MutableStateFlow(false)\n    private val isValid = _isValid.asStateFlow()\n\n    abstract fun infoUpdated(responseInfo: ResponseInfo?)\n    abstract suspend fun actualCheck(credentials: Credentials): ResponseInfo\n    fun isValidCredentials(credentials: Credentials): Boolean {\n        return runCatching {\n            credentials.validateCredentials()\n            true\n        }.getOrElse { false }\n    }\n\n    private fun setInfo(responseInfo: ResponseInfo?) {\n        _responseInfo.update { responseInfo }\n        infoUpdated(responseInfo)\n        validate()\n    }\n\n    private fun validate() {\n        val isValid = isValidCredentials(this.credentials.value)\n        _isValid.update { isValid }\n    }\n\n    suspend fun check() {\n        val downloadCredentials = credentials.value\n        val link = downloadCredentials.link\n        val isValidUrl = HttpUrlUtils.isValidUrl(link)\n        setInfo(null)\n        if (link.isBlank() || !isValidUrl) {\n            return\n        }\n        _isLoading.update { true }\n        val info = runCatching {\n            actualCheck(downloadCredentials)\n        }.getOrNull()\n        _isLoading.update { false }\n        setInfo(info)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/LinkCheckerFactory.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui\n\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\n\ninterface LinkCheckerFactory<\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>,\n        > {\n    fun createLinkChecker(\n        initialCredentials: TCredentials\n    ): TLinkChecker\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/add/AddDownloadChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.add\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.utils.DuplicateFilterByPath\nimport ir.amirab.util.FileNameValidator\nimport ir.amirab.util.osfileutil.FileUtils\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\nimport java.io.File\n\nclass AddDownloadChecker<\n        Credentials : IDownloadCredentials,\n        ResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        >(\n    initialName: String,\n    initialFolder: String,\n    private val linkChecker: LinkChecker<Credentials, ResponseInfo, TDownloadSize>,\n    private val downloadSystem: DownloadSystem,\n    private val parentScope: CoroutineScope,\n) {\n\n    val canAddResult = MutableStateFlow(null as CanAddResult?)\n    val canAdd = canAddResult.mapStateFlow() {\n        it is CanAddResult.CanAdd\n    }\n    val isDuplicate = canAddResult.mapStateFlow() {\n        it is CanAddResult.DownloadAlreadyExists\n    }\n\n    val name = MutableStateFlow(initialName)\n    val folder = MutableStateFlow(initialFolder)\n\n    init {\n        combine(\n            this.name,\n            this.folder,\n            transform = { _ ->\n                canAddResult.update { null }\n            }\n        ).launchIn(parentScope)\n    }\n\n    suspend fun check() {\n        canAddResult.update { null }\n        val newResult = validate()\n        canAddResult.update { newResult }\n    }\n\n    private suspend fun validate(): CanAddResult {\n        if (!linkChecker.isValidCredentials(linkChecker.credentials.value)) {\n            return CanAddResult.InvalidUrl\n        }\n        if (!fileNameValid()) {\n            return CanAddResult.InvalidFileName\n        }\n        val name = name.value\n        val folder = folder.value\n        val file = File(folder, name)\n        val duplicateFilterByPath = DuplicateFilterByPath(file)\n        val items = downloadSystem\n            .getDownloadItemsBy(duplicateFilterByPath::isDuplicate)\n\n//        val fileExists = File(folder, name).exists()\n\n        if (items.isNotEmpty()) {\n            return CanAddResult.DownloadAlreadyExists(items.first().id)\n        }\n        if (!FileUtils.canWriteInThisFolder(folder)) {\n            return CanAddResult.CantWriteInThisFolder\n        }\n//        if((length?:0)>File(folder).length()){\n//            return  CanAddResult.NotEnoughMemory\n//        }\n        return CanAddResult.CanAdd\n    }\n\n    private fun fileNameValid(): Boolean {\n        return FileNameValidator.isValidFileName(name.value)\n    }\n\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/add/CanAddResult.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.add\n\nsealed interface CanAddResult {\n    data class DownloadAlreadyExists(val itemId: Long) : CanAddResult\n    data object InvalidFileName : CanAddResult\n    data object CantWriteInThisFolder : CanAddResult\n\n    data object InvalidUrl : CanAddResult\n    data object CanAdd : CanAddResult\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/add/NewDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.add\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.DownloadUiChecker\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\n\nabstract class NewDownloadInputs<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfoType : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfoType, TDownloadSize>,\n        >(\n    val downloadUiChecker: DownloadUiChecker<TCredentials, TResponseInfoType, TDownloadSize, TLinkChecker>\n) {\n    val openedTime = System.currentTimeMillis()\n\n    val name = downloadUiChecker.name\n    val folder = downloadUiChecker.folder\n    val credentials = downloadUiChecker.credentials\n    val downloadSize = downloadUiChecker.downloadSize\n    abstract val downloadItem: StateFlow<TDownloadItem>\n    abstract val downloadJobConfig: StateFlow<DownloadJobExtraConfig?>\n    abstract val configurableList: List<Configurable<*>>\n\n    abstract fun applyHostSettingsToExtraConfig(extraConfig: PerHostSettingsItem)\n\n    fun setCredentials(credentials: TCredentials) {\n        downloadUiChecker.credentials.update { credentials }\n    }\n\n    abstract fun downloadSizeToStringSource(downloadSize: TDownloadSize): StringSource?\n\n    val lengthStringFlow: StateFlow<StringSource> = downloadSize.mapStateFlow {\n        it\n            ?.let(::downloadSizeToStringSource)\n            ?: Res.string.unknown.asStringSource()\n    }\n\n    fun getLengthString(): StringSource {\n        return lengthStringFlow.value\n    }\n\n    fun getUniqueId(): NewDownloadInputsUniqueIdType = hashCode()\n}\ntypealias TANewDownloadInputs = NewDownloadInputs<IDownloadItem, IDownloadCredentials, IResponseInfo, DownloadSize, LinkChecker<IDownloadCredentials, IResponseInfo, DownloadSize>>\ntypealias NewDownloadInputsUniqueIdType = Int\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/add/NewDownloadInputsFactory.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.add\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.CoroutineScope\n\ninterface NewDownloadInputsFactory<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfoType : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfoType, TDownloadSize>,\n        TNewDownloadInputs : NewDownloadInputs<\n                TDownloadItem,\n                TCredentials,\n                TResponseInfoType,\n                TDownloadSize,\n                TLinkChecker,\n                >\n        > {\n    fun createNewDownloadInputs(\n        initialCredentials: TCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope\n    ): TNewDownloadInputs\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/CanEditDownloadResult.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nsealed interface CanEditDownloadResult {\n    data object FileNameAlreadyExists : CanEditDownloadResult\n    data object InvalidURL : CanEditDownloadResult\n    data object InvalidFileName : CanEditDownloadResult\n    data object NothingChanged : CanEditDownloadResult\n    data object Waiting : CanEditDownloadResult\n    data class CanEdit(\n        val warnings: List<CanEditWarnings>,\n    ) : CanEditDownloadResult\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/CanEditWarnings.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.convertDurationToHumanReadable\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\n\nsealed interface CanEditWarnings {\n    fun asStringSource(): StringSource\n    data class FileSizeNotMatch(\n        val currentSize: Long,\n        val newSize: Long,\n    ) : CanEditWarnings {\n        override fun asStringSource(): StringSource {\n            return Res.string.edit_download_saved_download_item_size_not_match\n                .asStringSourceWithARgs(\n                    Res.string.edit_download_saved_download_item_size_not_match_createArgs(\n                        currentSize = \"$currentSize\",\n                        newSize = \"$newSize\",\n                    )\n                )\n        }\n\n    }\n    data class DurationNotMatch(\n        val currentDuration: Double?,\n        val newDuration: Double?,\n    ) : CanEditWarnings {\n        val notAvailableString = Res.string.unknown.asStringSource()\n        override fun asStringSource(): StringSource {\n            val currentDurationString = currentDuration?.let {\n                convertDurationToHumanReadable(it)\n            } ?: notAvailableString\n            val newDurationString = newDuration?.let {\n                convertDurationToHumanReadable(it)\n            } ?: notAvailableString\n            return Res.string.edit_download_saved_download_item_size_not_match\n                .asStringSourceWithARgs(\n                    Res.string.edit_download_saved_download_item_size_not_match_createArgs(\n                        currentSize = currentDurationString.getString(),\n                        newSize = newDurationString.getString(),\n                    )\n                )\n        }\n\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/DownloadConflictDetector.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\ninterface IDownloadConflictDetector<in TDownloadItem : IDownloadItem> {\n    fun checkAlreadyExists(\n        current: TDownloadItem,\n        edited: TDownloadItem,\n    ): Boolean\n}\n\nclass DownloadConflictDetector(\n    private val downloadSystem: DownloadSystem\n) : IDownloadConflictDetector<IDownloadItem> {\n    override fun checkAlreadyExists(current: IDownloadItem, edited: IDownloadItem): Boolean {\n        val editedDownloadFile = downloadSystem.getDownloadFile(edited)\n        val alreadyExists = editedDownloadFile.exists()\n        if (alreadyExists) {\n            return true\n        }\n        return downloadSystem\n            .getAllRegisteredDownloadFiles()\n            .contains(editedDownloadFile)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/EditDownloadCheckerFactory.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.downloaderinui.http.edit.EditDownloadChecker\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface EditDownloadCheckerFactory<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>> {\n    fun createEditDownloadChecker(\n        currentDownloadItem: MutableStateFlow<TDownloadItem>,\n        editedDownloadItem: MutableStateFlow<TDownloadItem>,\n        linkChecker: TLinkChecker,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope,\n    ): EditDownloadChecker<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/EditDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.CredentialAndItemMapper\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.downloaderinui.LinkCheckerFactory\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport ir.amirab.util.flow.onEachLatest\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.merge\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.update\n\ntypealias TAEditDownloadInputs = EditDownloadInputs<*, *, *, *, *, *>\n\nabstract class EditDownloadInputs<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>,\n        TCredentialAndItemMapper : CredentialAndItemMapper<TCredentials, TDownloadItem>\n        >(\n    val currentDownloadItem: MutableStateFlow<TDownloadItem>,\n    val editedDownloadItem: MutableStateFlow<TDownloadItem>,\n    val mapper: TCredentialAndItemMapper,\n    val scope: CoroutineScope,\n    val conflictDetector: DownloadConflictDetector,\n    linkCheckerFactory: LinkCheckerFactory<TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>,\n    editDownloadCheckerFactory: EditDownloadCheckerFactory<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker>,\n) {\n    private val _showMoreSettings = MutableStateFlow(false)\n    val showMoreSettings = _showMoreSettings.asStateFlow()\n    fun setShowMoreSettings(showMoreSettings: Boolean) {\n        _showMoreSettings.value = showMoreSettings\n    }\n\n    val credentials: MutableStateFlow<TCredentials> = editedDownloadItem.mapTwoWayStateFlow(\n        map = {\n            mapper.itemToCredentials(it)\n        },\n        unMap = {\n            mapper.appliedCredentialsToItem(this, it)\n        }\n    )\n    val name = editedDownloadItem.mapTwoWayStateFlow(\n        map = {\n            it.name\n        },\n        unMap = {\n            mapper.itemWithEditedName(this, it)\n        }\n    )\n    abstract val downloadJobConfig: StateFlow<DownloadJobExtraConfig?>\n    abstract val configurableList: List<Configurable<*>>\n    abstract fun applyEditedItemTo(item: TDownloadItem)\n    fun setName(name: String) {\n        this.name.value = name\n    }\n\n    val link = credentials.mapTwoWayStateFlow(\n        map = { it.link },\n        unMap = {\n            mapper.credentialsWithEditedLink(this, it)\n        }\n    )\n\n    fun setLink(link: String) {\n        credentials.update {\n            mapper.credentialsWithEditedLink(it, link)\n        }\n    }\n\n    fun importCredentials(importedCredentials: TCredentials) {\n        this.credentials.update {\n            importedCredentials\n        }\n    }\n\n    protected val linkChecker = linkCheckerFactory.createLinkChecker(credentials.value)\n    private val httpEditDownloadChecker = editDownloadCheckerFactory.createEditDownloadChecker(\n        currentDownloadItem = currentDownloadItem,\n        editedDownloadItem = editedDownloadItem,\n        linkChecker = linkChecker,\n        conflictDetector = conflictDetector,\n        scope = scope,\n    )\n\n    val isLinkLoading = linkChecker.isLoading\n\n    val gettingResponseInfo = linkChecker.isLoading\n    val responseInfo = linkChecker.responseInfo\n\n\n    val canEditDownloadResult = httpEditDownloadChecker.canEditResult\n    val canEdit = httpEditDownloadChecker.canEdit\n\n    private val refreshResponseInfoImmediately = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n    private val scheduleRefreshResponseInfo = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n    private val scheduleRecheckEditDownloadIsPossible = MutableSharedFlow<Unit>(\n        replay = 1,\n        onBufferOverflow = BufferOverflow.DROP_LATEST\n    )\n\n    fun refresh() {\n        refreshResponseInfoImmediately.tryEmit(Unit)\n    }\n\n    protected fun scheduleRefresh(\n        alsoRecheckLink: Boolean,\n    ) {\n        if (alsoRecheckLink) {\n            scheduleRefreshResponseInfo.tryEmit(Unit)\n        }\n        scheduleRecheckEditDownloadIsPossible.tryEmit(Unit)\n    }\n\n    init {\n        merge(\n            scheduleRefreshResponseInfo.debounce(500),\n            refreshResponseInfoImmediately\n        ).onEachLatest {\n            linkChecker.check()\n        }.launchIn(scope)\n        merge(\n            scheduleRecheckEditDownloadIsPossible.debounce(500),\n//            ...\n        ).onEachLatest {\n            httpEditDownloadChecker.check()\n        }.launchIn(scope)\n\n        credentials.onEach { credentials ->\n            linkChecker.credentials.update { credentials }\n            scheduleRefresh(alsoRecheckLink = true)\n        }.launchIn(scope)\n        editedDownloadItem.onEach {\n            scheduleRefresh(alsoRecheckLink = false)\n        }.launchIn(scope)\n    }\n\n    abstract fun downloadSizeToStringSource(downloadSize: TDownloadSize): StringSource\n    val lengthStringFlow: StateFlow<StringSource> = linkChecker.downloadSize.mapStateFlow {\n        it\n            ?.let(::downloadSizeToStringSource)\n            ?: Res.string.unknown.asStringSource()\n    }\n\n    fun getLengthString(): StringSource {\n        return lengthStringFlow.value\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/edit/EditDownloadInputsFactory.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.edit\n\nimport com.abdownloadmanager.shared.downloaderinui.CredentialAndItemMapper\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface EditDownloadInputsFactory<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>,\n        TCredentialsToItemMapper : CredentialAndItemMapper<TCredentials, TDownloadItem>,\n        TEditDownloadInputs : EditDownloadInputs<TDownloadItem, TCredentials, TResponseInfo, TDownloadSize, TLinkChecker, TCredentialsToItemMapper>\n        > {\n    fun createEditDownloadInputs(\n        currentDownloadItem: MutableStateFlow<TDownloadItem>,\n        editedDownloadItem: MutableStateFlow<TDownloadItem>,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope,\n    ): TEditDownloadInputs\n}\n\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/HLSDownloaderInUi.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls\n\nimport com.abdownloadmanager.shared.downloaderinui.BasicDownloadItem\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUi\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.hls.add.HLSDownloadUIChecker\nimport com.abdownloadmanager.shared.downloaderinui.hls.add.HLSNewDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.hls.edit.HLSEditDownloadChecker\nimport com.abdownloadmanager.shared.downloaderinui.hls.edit.HLSEditDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.http.edit.EditDownloadChecker\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadItem\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadJob\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloader\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport ir.amirab.downloader.downloaditem.hls.IHLSCredentials\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemFactoryInputs\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass HLSDownloaderInUi(\n    downloader: HLSDownloader,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n) : DownloaderInUi<\n        HLSDownloadCredentials,\n        HLSResponseInfo,\n        DownloadSize.Duration,\n        HLSLinkChecker,\n        HLSDownloadItem,\n        HLSNewDownloadInputs,\n        HLSEditDownloadInputs,\n        HlsItemToCredentialMapper,\n        HLSDownloadJob,\n        HLSDownloader\n        >(downloader) {\n    override fun newDownloadUiChecker(\n        initialCredentials: HLSDownloadCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope\n    ): HLSDownloadUIChecker {\n        return HLSDownloadUIChecker(\n            initCredentials = initialCredentials,\n            linkCheckerFactory = this,\n            initialFolder = initialFolder,\n            initialName = initialName,\n            downloadSystem = downloadSystem,\n            scope = scope,\n        )\n    }\n\n    override fun acceptDownloadCredentials(item: IDownloadCredentials): Boolean {\n        return item is IHLSCredentials\n    }\n\n    override fun supportsThisLink(link: String): Boolean {\n        return HttpUrlUtils.isValidUrl(link)\n    }\n\n    override fun createMinimumCredentials(link: String): HLSDownloadCredentials {\n        return HLSDownloadCredentials(link = link)\n    }\n\n    override fun createBareDownloadItem(\n        credentials: HLSDownloadCredentials,\n        basicDownloadItem: BasicDownloadItem\n    ): HLSDownloadItem {\n        return HLSDownloadItem.createWithCredentials(\n            id = -1,\n            credentials = credentials,\n            folder = basicDownloadItem.folder,\n            name = basicDownloadItem.name,\n            contentLength = basicDownloadItem.contentLength,\n            preferredConnectionCount = basicDownloadItem.preferredConnectionCount,\n            speedLimit = basicDownloadItem.speedLimit,\n            fileChecksum = basicDownloadItem.fileChecksum,\n        )\n    }\n\n    override fun createProcessingDownloadItemState(\n        props: ProcessingDownloadItemFactoryInputs<HLSDownloadJob>\n    ): ProcessingDownloadItemState {\n        return UiProcessingItemForHSLFactory.create(\n            props,\n        )\n    }\n\n    override val name: StringSource = \"HLS\".asStringSource()\n\n    override fun createLinkChecker(initialCredentials: HLSDownloadCredentials): HLSLinkChecker {\n        return HLSLinkChecker(\n            credentials = initialCredentials,\n            client = downloader.client\n        )\n    }\n\n    override fun createEditDownloadChecker(\n        currentDownloadItem: MutableStateFlow<HLSDownloadItem>,\n        editedDownloadItem: MutableStateFlow<HLSDownloadItem>,\n        linkChecker: HLSLinkChecker,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope\n    ): EditDownloadChecker<HLSDownloadItem, HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker> {\n        return HLSEditDownloadChecker(\n            currentDownloadItem = currentDownloadItem,\n            editedDownloadItem = editedDownloadItem,\n            linkChecker = linkChecker,\n            conflictDetector = conflictDetector,\n            scope = scope,\n        )\n    }\n\n    override fun createNewDownloadInputs(\n        initialCredentials: HLSDownloadCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope\n    ): HLSNewDownloadInputs {\n        return HLSNewDownloadInputs(\n            newDownloadUiChecker(\n                initialCredentials = initialCredentials,\n                initialFolder = initialFolder,\n                initialName = initialName,\n                downloadSystem = downloadSystem,\n                scope = scope,\n            ),\n            sizeAndSpeedUnitProvider,\n            scope,\n        )\n    }\n\n    override fun createEditDownloadInputs(\n        currentDownloadItem: MutableStateFlow<HLSDownloadItem>,\n        editedDownloadItem: MutableStateFlow<HLSDownloadItem>,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope\n    ): HLSEditDownloadInputs {\n        return HLSEditDownloadInputs(\n            currentDownloadItem = currentDownloadItem,\n            editedDownloadItem = editedDownloadItem,\n            mapper = HlsItemToCredentialMapper(),\n            conflictDetector = conflictDetector,\n            scope = scope,\n            linkCheckerFactory = this,\n            editDownloadCheckerFactory = this,\n            sizeAndSpeedUnitProvider = sizeAndSpeedUnitProvider,\n        )\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/HLSLinkChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nclass HLSLinkChecker(\n    credentials: HLSDownloadCredentials,\n    private val client: HttpDownloaderClient,\n) : LinkChecker<HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration>(\n    initialCredentials = credentials\n) {\n    private val _suggestedName: MutableStateFlow<String?> = MutableStateFlow(null)\n    override val suggestedName: StateFlow<String?> = MutableStateFlow(null)\n    private val _duration: MutableStateFlow<Double?> = MutableStateFlow(null)\n    val duration: StateFlow<Double?> = _duration.asStateFlow()\n    override val downloadSize: StateFlow<DownloadSize.Duration?> = _duration.mapStateFlow {\n        it?.let(DownloadSize::Duration)\n    }\n    override fun infoUpdated(responseInfo: HLSResponseInfo?) {\n        _suggestedName.value = responseInfo?.name\n        _duration.value = responseInfo?.duration\n    }\n\n    override suspend fun actualCheck(credentials: HLSDownloadCredentials): HLSResponseInfo {\n        return client.connect(credentials, null, null)\n            .use { HLSResponseInfo.fromConnection(it) }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/HlsItemToCredentialMapper.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls\n\nimport com.abdownloadmanager.shared.downloaderinui.CredentialAndItemMapper\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadItem\n\nclass HlsItemToCredentialMapper : CredentialAndItemMapper<\n        HLSDownloadCredentials, HLSDownloadItem> {\n    override fun itemToCredentials(item: HLSDownloadItem): HLSDownloadCredentials {\n        return HLSDownloadCredentials(\n            link = item.link,\n            downloadPage = item.downloadPage,\n            headers = item.headers,\n            username = item.username,\n            password = item.password,\n            userAgent = item.userAgent,\n        )\n    }\n\n    override fun appliedCredentialsToItem(\n        item: HLSDownloadItem,\n        credentials: HLSDownloadCredentials\n    ): HLSDownloadItem {\n        return item.copy().withCredentials(credentials)\n    }\n\n    override fun itemWithEditedName(\n        item: HLSDownloadItem,\n        name: String\n    ): HLSDownloadItem {\n        return item.copy(name = name)\n    }\n\n    override fun credentialsWithEditedLink(\n        credentials: HLSDownloadCredentials,\n        link: String\n    ): HLSDownloadCredentials {\n        return credentials.copy(link = link)\n    }\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/UiProcessingItemForHSLFactory.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls\n\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadJob\nimport ir.amirab.downloader.monitor.DurationBasedProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemFactoryInputs\nimport ir.amirab.downloader.monitor.UiDurationBasedPart\nimport ir.amirab.downloader.part.PartDownloadStatus\n\nobject UiProcessingItemForHSLFactory {\n    fun create(\n        props: ProcessingDownloadItemFactoryInputs<HLSDownloadJob>\n    ): DurationBasedProcessingDownloadItemState {\n        val downloadJob = props.downloadJob\n        val item = downloadJob.downloadItem\n        val downloadParts = downloadJob.getParts()\n        val totalPartsCount = downloadParts.size\n        val uiParts = downloadParts.map {\n            UiDurationBasedPart.fromPart(\n                part = it,\n                totalPartsCount = totalPartsCount,\n            )\n        }\n        var finishedPartsCount = 0\n        var downloadedBytes = 0L\n        // only those who has length\n        var activeCount = 0\n        var activeCountTotalLength = 0L\n        var activeCountProgress = 0L\n        // ----------\n        for (part in uiParts) {\n            val status = part.status\n            val howMuchProceed = part.howMuchProceed\n            if (status is PartDownloadStatus.Completed) {\n                finishedPartsCount++\n            } else if (status is PartDownloadStatus.IsActive) {\n                val length = part.length\n                if (length != null) {\n                    activeCount++\n                    activeCountProgress += howMuchProceed\n                    activeCountTotalLength += length\n                }\n            }\n            downloadedBytes += howMuchProceed\n        }\n        val percentFraction = getPercentFraction(\n            finishedPartsCount = finishedPartsCount,\n            totalPartsCount = totalPartsCount,\n            activePartsCount = activeCount,\n            activeCountsTotalDownloaded = activeCountProgress,\n            activeCountsTotalLength = activeCountTotalLength,\n        )\n        val length = (downloadedBytes / percentFraction).toLong()\n        return DurationBasedProcessingDownloadItemState(\n            id = downloadJob.id,\n            name = item.name,\n            folder = item.folder,\n            status = downloadJob.status.value,\n            speed = props.speed,\n            supportResume = true,\n            contentLength = item.contentLength,\n            parts = uiParts,\n            downloadLink = item.link,\n            saveLocation = item.name,\n            dateAdded = item.dateAdded,\n            startTime = item.startTime ?: 0,\n            completeTime = item.completeTime ?: 0,\n            duration = item.duration,\n            progress = downloadedBytes,\n            percent = (percentFraction * 100).toInt(),\n            optimisticLength = length,\n            isWaiting = props.isWaiting,\n        )\n    }\n\n    private fun getPercentFraction(\n        finishedPartsCount: Int,\n        totalPartsCount: Int,\n        activePartsCount: Int,\n        activeCountsTotalDownloaded: Long,\n        activeCountsTotalLength: Long,\n    ): Double {\n        // how much all of active parts have progress (0.0-1.0)\n        val activePartCountProgress = if (activeCountsTotalLength == 0L) {\n            0.0\n        } else {\n            (activeCountsTotalDownloaded / activeCountsTotalLength.toDouble()) * activePartsCount\n        }\n        return (finishedPartsCount + activePartCountProgress) / totalPartsCount\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/add/HLSDownloadUIChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls.add\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.DownloadUiChecker\nimport com.abdownloadmanager.shared.downloaderinui.LinkCheckerFactory\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSLinkChecker\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport kotlinx.coroutines.CoroutineScope\n\nclass HLSDownloadUIChecker(\n    initCredentials: HLSDownloadCredentials,\n    linkCheckerFactory: LinkCheckerFactory<HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker>,\n    initialFolder: String,\n    initialName: String,\n    downloadSystem: DownloadSystem,\n    scope: CoroutineScope,\n) : DownloadUiChecker<HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker>(\n    initialCredentials = initCredentials,\n    linkCheckerFactory = linkCheckerFactory,\n    initialFolder = initialFolder,\n    initialName = initialName,\n    downloadSystem = downloadSystem,\n    scope = scope,\n) {\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/add/HLSNewDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls.add\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputs\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSLinkChecker\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport com.abdownloadmanager.shared.downloaderinui.http.applyToHttpDownload\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadItem\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadJobExtraConfig\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass HLSNewDownloadInputs(\n    downloadUiChecker: HLSDownloadUIChecker,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n    private val scope: CoroutineScope,\n) : NewDownloadInputs<\n        HLSDownloadItem,\n        HLSDownloadCredentials,\n        HLSResponseInfo,\n        DownloadSize.Duration,\n        HLSLinkChecker,\n        >(\n    downloadUiChecker = downloadUiChecker\n) {\n\n    //extra settings\n    private var threadCount = MutableStateFlow(null as Int?)\n    private var speedLimit = MutableStateFlow(0L)\n    private var fileChecksum = MutableStateFlow(null as FileChecksum?)\n    override val downloadItem: StateFlow<HLSDownloadItem> = combineStateFlows(\n        this.credentials,\n        this.folder,\n        this.name,\n        this.downloadSize,\n        this.speedLimit,\n        this.threadCount,\n        this.fileChecksum,\n    ) { credentials,\n        folder,\n        name,\n        duration,\n        speedLimit,\n        threadCount,\n        fileChecksum\n        ->\n        HLSDownloadItem(\n            id = -1,\n            folder = folder,\n            name = name,\n            link = credentials.link,\n            dateAdded = openedTime,\n            startTime = null,\n            completeTime = null,\n            status = DownloadStatus.Added,\n            preferredConnectionCount = threadCount,\n            speedLimit = speedLimit,\n            fileChecksum = fileChecksum?.toString(),\n            duration = duration?.duration,\n        ).withCredentials(credentials)\n    }\n    override val downloadJobConfig: StateFlow<DownloadJobExtraConfig?> = downloadUiChecker.responseInfo.mapStateFlow {\n        it?.let {\n            HLSDownloadJobExtraConfig(\n                hlsManifest = it.hlsManifest\n            )\n        }\n    }\n\n    override fun applyHostSettingsToExtraConfig(extraConfig: PerHostSettingsItem) {\n        extraConfig.applyToHttpDownload(\n            setUsername = { setCredentials(credentials.value.copy(username = it)) },\n            setPassword = { setCredentials(credentials.value.copy(password = it)) },\n            setUserAgent = { setCredentials(credentials.value.copy(userAgent = it)) },\n            setThreadCount = { threadCount.value = it },\n            setSpeedLimit = { speedLimit.value = it }\n        )\n    }\n\n    override val configurableList = listOf(\n        SpeedLimitConfigurable(\n            Res.string.download_item_settings_speed_limit.asStringSource(),\n            Res.string.download_item_settings_speed_limit_description.asStringSource(),\n            backedBy = speedLimit,\n            describe = {\n                if (it == 0L) Res.string.unlimited.asStringSource()\n                else convertPositiveSpeedToHumanReadable(\n                    it, sizeAndSpeedUnitProvider.speedUnit.value\n                ).asStringSource()\n            }\n        ),\n        FileChecksumConfigurable(\n            Res.string.download_item_settings_file_checksum.asStringSource(),\n            Res.string.download_item_settings_file_checksum_description.asStringSource(),\n            backedBy = fileChecksum,\n            describe = { \"\".asStringSource() }\n        ),\n        IntConfigurable(\n            Res.string.settings_download_thread_count.asStringSource(),\n            Res.string.settings_download_thread_count_description.asStringSource(),\n            backedBy = threadCount.mapTwoWayStateFlow(\n                map = {\n                    it ?: 0\n                },\n                unMap = {\n                    it.takeIf { it >= 1 }\n                }\n            ),\n            range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n            describe = {\n                if (it == 0) Res.string.use_global_settings.asStringSource()\n                else Res.string.download_item_settings_thread_count_describe\n                    .asStringSourceWithARgs(\n                        Res.string.download_item_settings_thread_count_describe_createArgs(\n                            count = it.toString()\n                        )\n                    )\n            }\n        ),\n        StringConfigurable(\n            Res.string.username.asStringSource(),\n            Res.string.download_item_settings_username_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = credentials.mapStateFlow {\n                    it.username.orEmpty()\n                },\n                updater = {\n                    setCredentials(credentials.value.copy(username = it.takeIf { it.isNotBlank() }))\n                }, scope\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.password.asStringSource(),\n            Res.string.download_item_settings_password_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = credentials.mapStateFlow {\n                    it.password.orEmpty()\n                },\n                updater = {\n                    setCredentials(credentials.value.copy(password = it.takeIf { it.isNotBlank() }))\n                }, scope\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_user_agent.asStringSource(),\n            Res.string.download_item_settings_user_agent_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.userAgent.orEmpty()\n                },\n                unMap = {\n                    copy(userAgent = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_download_page.asStringSource(),\n            Res.string.download_item_settings_download_page_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.downloadPage.orEmpty()\n                },\n                unMap = {\n                    copy(downloadPage = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        )\n    )\n\n    override fun downloadSizeToStringSource(downloadSize: DownloadSize.Duration): StringSource {\n        return downloadSize.asStringSource()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/edit/HLSEditDownloadChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls.edit\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditDownloadResult\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditWarnings\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSLinkChecker\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport com.abdownloadmanager.shared.downloaderinui.http.edit.EditDownloadChecker\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadItem\nimport ir.amirab.util.FileNameValidator\nimport ir.amirab.util.HttpUrlUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass HLSEditDownloadChecker(\n    currentDownloadItem: MutableStateFlow<HLSDownloadItem>,\n    editedDownloadItem: MutableStateFlow<HLSDownloadItem>,\n    linkChecker: HLSLinkChecker,\n    conflictDetector: DownloadConflictDetector,\n    scope: CoroutineScope\n) : EditDownloadChecker<HLSDownloadItem, HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker>(\n    currentDownloadItem = currentDownloadItem,\n    editedDownloadItem = editedDownloadItem,\n    linkChecker = linkChecker,\n    conflictDetector = conflictDetector,\n    scope = scope,\n) {\n    init {\n        editedDownloadItem\n            .onEach {\n                _canEditResult.value = CanEditDownloadResult.Waiting\n            }.launchIn(scope)\n    }\n\n    override fun check() {\n        _canEditResult.value = CanEditDownloadResult.Waiting\n        _canEditResult.value = check(\n            current = currentDownloadItem.value,\n            edited = editedDownloadItem.value,\n            newDuration = linkChecker.duration.value,\n        )\n    }\n\n    private fun check(\n        current: HLSDownloadItem,\n        edited: HLSDownloadItem,\n        newDuration: Double?,\n    ): CanEditDownloadResult {\n        if (current == edited) {\n            return CanEditDownloadResult.NothingChanged\n        }\n        if (!HttpUrlUtils.isValidUrl(edited.link)) {\n            return CanEditDownloadResult.InvalidURL\n        }\n        if (edited.name != current.name) {\n            if (!FileNameValidator.isValidFileName(edited.name)) {\n                return CanEditDownloadResult.InvalidFileName\n            }\n            if (conflictDetector.checkAlreadyExists(current, edited)) {\n                return CanEditDownloadResult.FileNameAlreadyExists\n            }\n        }\n        val warnings = mutableListOf<CanEditWarnings>()\n        if (current.duration != newDuration) {\n            warnings.add(\n                CanEditWarnings.DurationNotMatch(\n                    current.duration,\n                    newDuration,\n                )\n            )\n        }\n        return CanEditDownloadResult.CanEdit(warnings)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/hls/edit/HLSEditDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.hls.edit\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkCheckerFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadCheckerFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadCredentials\nimport com.abdownloadmanager.shared.downloaderinui.hls.HLSLinkChecker\nimport ir.amirab.downloader.downloaditem.hls.HLSResponseInfo\nimport com.abdownloadmanager.shared.downloaderinui.hls.HlsItemToCredentialMapper\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadItem\nimport ir.amirab.downloader.downloaditem.hls.HLSDownloadJobExtraConfig\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass HLSEditDownloadInputs(\n    currentDownloadItem: MutableStateFlow<HLSDownloadItem>,\n    editedDownloadItem: MutableStateFlow<HLSDownloadItem>,\n    mapper: HlsItemToCredentialMapper,\n    conflictDetector: DownloadConflictDetector,\n    linkCheckerFactory: LinkCheckerFactory<HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker>,\n    editDownloadCheckerFactory: EditDownloadCheckerFactory<HLSDownloadItem, HLSDownloadCredentials, HLSResponseInfo, DownloadSize.Duration, HLSLinkChecker>,\n    scope: CoroutineScope,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n) : EditDownloadInputs<\n        HLSDownloadItem,\n        HLSDownloadCredentials,\n        HLSResponseInfo,\n        DownloadSize.Duration,\n        HLSLinkChecker,\n        HlsItemToCredentialMapper\n        >(\n    currentDownloadItem,\n    editedDownloadItem,\n    mapper = mapper,\n    scope = scope,\n    conflictDetector = conflictDetector,\n    linkCheckerFactory = linkCheckerFactory,\n    editDownloadCheckerFactory = editDownloadCheckerFactory,\n) {\n\n    override val configurableList = listOf(\n        SpeedLimitConfigurable(\n            Res.string.download_item_settings_speed_limit.asStringSource(),\n            Res.string.download_item_settings_speed_limit_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.speedLimit\n                },\n                unMap = {\n                    copy(speedLimit = it)\n                }\n            ),\n            describe = {\n                if (it == 0L) Res.string.unlimited.asStringSource()\n                else convertPositiveSpeedToHumanReadable(it, sizeAndSpeedUnitProvider.speedUnit.value).asStringSource()\n            }\n        ),\n        FileChecksumConfigurable(\n            Res.string.download_item_settings_file_checksum.asStringSource(),\n            Res.string.download_item_settings_file_checksum_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.fileChecksum?.let {\n                        runCatching {\n                            FileChecksum.Companion.fromString(it)\n                        }.onFailure {\n                            println(it.printStackTrace())\n                        }.getOrNull()\n                    }\n                },\n                unMap = {\n                    copy(fileChecksum = it?.toString())\n                }\n            ),\n            describe = { \"\".asStringSource() }\n        ),\n        IntConfigurable(\n            Res.string.settings_download_thread_count.asStringSource(),\n            Res.string.settings_download_thread_count_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.preferredConnectionCount ?: 0\n                },\n                unMap = {\n                    copy(\n                        preferredConnectionCount = it.takeIf { it >= 1 }\n                    )\n                }\n            ),\n            range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n            describe = {\n                if (it == 0) Res.string.use_global_settings.asStringSource()\n                else Res.string.download_item_settings_thread_count_describe\n                    .asStringSourceWithARgs(\n                        Res.string.download_item_settings_thread_count_describe_createArgs(\n                            count = it.toString()\n                        )\n                    )\n            }\n        ),\n        StringConfigurable(\n            Res.string.username.asStringSource(),\n            Res.string.download_item_settings_username_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.username.orEmpty()\n                },\n                unMap = {\n                    copy(username = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.password.asStringSource(),\n            Res.string.download_item_settings_password_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.password.orEmpty()\n                },\n                unMap = {\n                    copy(password = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_user_agent.asStringSource(),\n            Res.string.download_item_settings_user_agent_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.userAgent.orEmpty()\n                },\n                unMap = {\n                    copy(userAgent = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_download_page.asStringSource(),\n            Res.string.download_item_settings_download_page_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.downloadPage.orEmpty()\n                },\n                unMap = {\n                    copy(downloadPage = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n    )\n    val duration = linkChecker.duration\n    override val downloadJobConfig: StateFlow<DownloadJobExtraConfig?> = linkChecker.responseInfo.mapStateFlow {\n        it?.let {\n            HLSDownloadJobExtraConfig(it.hlsManifest)\n        }\n    }\n\n    private fun HLSDownloadItem.applyOurChanges(edited: HLSDownloadItem) {\n        // we don't change some of these properties, so I commented them\n\n        link = edited.link\n        headers = edited.headers\n        username = edited.username\n        password = edited.password\n        downloadPage = edited.downloadPage\n        userAgent = edited.userAgent\n\n//        id = edited.id\n        folder = edited.folder\n        name = edited.name\n\n        contentLength = edited.contentLength\n\n//        dateAdded = edited.dateAdded\n//        startTime = edited.startTime\n//        completeTime = edited.completeTime\n//        status = edited.status\n        preferredConnectionCount = edited.preferredConnectionCount\n        speedLimit = edited.speedLimit\n\n        fileChecksum = edited.fileChecksum\n        duration = edited.duration\n    }\n\n    override fun applyEditedItemTo(item: HLSDownloadItem) {\n        val edited = editedDownloadItem.value\n        item.applyOurChanges(edited)\n    }\n\n    init {\n        duration.onEach {\n            scheduleRefresh(alsoRecheckLink = false)\n        }.launchIn(scope)\n    }\n\n    override fun downloadSizeToStringSource(downloadSize: DownloadSize.Duration): StringSource {\n        return downloadSize.asStringSource()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/HttpCredentialsToItemMapper.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http\n\nimport com.abdownloadmanager.shared.downloaderinui.CredentialAndItemMapper\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.downloaditem.http.withHttpCredentials\n\nobject HttpCredentialsToItemMapper : CredentialAndItemMapper<HttpDownloadCredentials, HttpDownloadItem> {\n    override fun itemToCredentials(item: HttpDownloadItem): HttpDownloadCredentials {\n        return HttpDownloadCredentials.from(item)\n    }\n\n    override fun appliedCredentialsToItem(\n        item: HttpDownloadItem,\n        credentials: HttpDownloadCredentials\n    ): HttpDownloadItem {\n        return item.copy().withHttpCredentials(credentials)\n    }\n\n    override fun itemWithEditedName(item: HttpDownloadItem, name: String): HttpDownloadItem {\n        return item.copy(name = name)\n    }\n\n    override fun credentialsWithEditedLink(\n        credentials: HttpDownloadCredentials,\n        link: String\n    ): HttpDownloadCredentials {\n        return credentials.copy(link = link)\n    }\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/HttpDownloaderInUi.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http\n\nimport com.abdownloadmanager.shared.downloaderinui.BasicDownloadItem\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUi\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.http.add.HttpDownloadUiChecker\nimport com.abdownloadmanager.shared.downloaderinui.http.add.HttpLinkChecker\nimport com.abdownloadmanager.shared.downloaderinui.http.add.HttpNewDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.http.edit.HttpEditDownloadChecker\nimport com.abdownloadmanager.shared.downloaderinui.http.edit.HttpEditDownloadInputs\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJob\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadJob\nimport ir.amirab.downloader.downloaditem.http.HttpDownloader\nimport ir.amirab.downloader.downloaditem.http.IHttpDownloadCredentials\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemFactoryInputs\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.RangeBasedProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.UiRangedPart\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass HttpDownloaderInUi(\n    httpDownloader: HttpDownloader,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n) : DownloaderInUi<HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker, HttpDownloadItem, HttpNewDownloadInputs, HttpEditDownloadInputs, HttpCredentialsToItemMapper, HttpDownloadJob, HttpDownloader>(\n    downloader = httpDownloader\n) {\n    override fun createLinkChecker(initialCredentials: HttpDownloadCredentials): HttpLinkChecker {\n        return HttpLinkChecker(\n            initialCredentials,\n            downloader.httpDownloaderClient,\n        )\n    }\n\n    override fun newDownloadUiChecker(\n        initialCredentials: HttpDownloadCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope,\n    ): HttpDownloadUiChecker {\n        return HttpDownloadUiChecker(\n            initialCredentials = initialCredentials,\n            linkCheckerFactory = this,\n            initialFolder = initialFolder,\n            initialName = initialName,\n            downloadSystem = downloadSystem,\n            scope = scope,\n        )\n    }\n\n    override fun createNewDownloadInputs(\n        initialCredentials: HttpDownloadCredentials,\n        initialFolder: String,\n        initialName: String,\n        downloadSystem: DownloadSystem,\n        scope: CoroutineScope\n    ): HttpNewDownloadInputs {\n        val downloadUiChecker = newDownloadUiChecker(\n            initialCredentials,\n            initialFolder,\n            initialName,\n            downloadSystem,\n            scope,\n        )\n        return HttpNewDownloadInputs(\n            downloadUiChecker = downloadUiChecker,\n            scope = scope,\n            sizeAndSpeedUnitProvider = sizeAndSpeedUnitProvider\n        )\n    }\n\n    override fun createEditDownloadInputs(\n        currentDownloadItem: MutableStateFlow<HttpDownloadItem>,\n        editedDownloadItem: MutableStateFlow<HttpDownloadItem>,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope\n    ): HttpEditDownloadInputs {\n        return HttpEditDownloadInputs(\n            currentDownloadItem = currentDownloadItem,\n            editedDownloadItem = editedDownloadItem,\n            sizeAndSpeedUnitProvider = sizeAndSpeedUnitProvider,\n            mapper = HttpCredentialsToItemMapper,\n            conflictDetector = conflictDetector,\n            scope = scope,\n            linkCheckerFactory = this,\n            editDownloadCheckerFactory = this,\n        )\n    }\n\n    override fun acceptDownloadCredentials(item: IDownloadCredentials): Boolean {\n        return item is IHttpDownloadCredentials\n    }\n\n    override fun supportsThisLink(link: String): Boolean {\n        return HttpUrlUtils.isValidUrl(link)\n    }\n\n    override fun createMinimumCredentials(link: String): HttpDownloadCredentials {\n        return HttpDownloadCredentials(link = link)\n    }\n\n    override fun createProcessingDownloadItemState(\n        props: ProcessingDownloadItemFactoryInputs<HttpDownloadJob>\n    ): ProcessingDownloadItemState {\n        val downloadJob = props.downloadJob\n        val downloadItem = downloadJob.downloadItem\n        val downloadJobStatus = downloadJob.status.value\n        val parts = downloadJob.getParts()\n        val contentLength = downloadItem.contentLength\n        return RangeBasedProcessingDownloadItemState(\n            id = downloadItem.id,\n            folder = downloadItem.folder,\n            name = downloadItem.name,\n            contentLength = contentLength,\n            dateAdded = downloadItem.dateAdded,\n            startTime = downloadItem.startTime ?: -1,\n            completeTime = downloadItem.completeTime ?: -1,\n            status = downloadJobStatus,\n            saveLocation = downloadItem.name,\n            parts = parts.map {\n                UiRangedPart.fromPart(\n                    part = it,\n                    totalLength = contentLength,\n                )\n            },\n            speed = props.speed,\n            supportResume = downloadJob.supportsConcurrent,\n            downloadLink = downloadItem.link,\n            isWaiting = props.isWaiting,\n        )\n    }\n\n    override fun createBareDownloadItem(\n        credentials: HttpDownloadCredentials,\n        basicDownloadItem: BasicDownloadItem\n    ): HttpDownloadItem {\n        return HttpDownloadItem.createWithCredentials(\n            id = -1,\n            credentials = credentials,\n            folder = basicDownloadItem.folder,\n            name = basicDownloadItem.name,\n            contentLength = basicDownloadItem.contentLength,\n            preferredConnectionCount = basicDownloadItem.preferredConnectionCount,\n            speedLimit = basicDownloadItem.speedLimit,\n            fileChecksum = basicDownloadItem.fileChecksum,\n        )\n    }\n\n    override val name: StringSource = \"HTTP\".asStringSource()\n    override fun createEditDownloadChecker(\n        currentDownloadItem: MutableStateFlow<HttpDownloadItem>,\n        editedDownloadItem: MutableStateFlow<HttpDownloadItem>,\n        linkChecker: HttpLinkChecker,\n        conflictDetector: DownloadConflictDetector,\n        scope: CoroutineScope\n    ): HttpEditDownloadChecker {\n        return HttpEditDownloadChecker(\n            currentDownloadItem = currentDownloadItem,\n            editedDownloadItem = editedDownloadItem,\n            linkChecker = linkChecker,\n            conflictDetector = conflictDetector,\n            scope = scope,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/add/HttpDownloadUiChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http.add\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.downloaderinui.DownloadUiChecker\nimport com.abdownloadmanager.shared.downloaderinui.LinkCheckerFactory\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport kotlinx.coroutines.CoroutineScope\n\nclass HttpDownloadUiChecker(\n    initialCredentials: HttpDownloadCredentials = HttpDownloadCredentials.Companion.empty(),\n    linkCheckerFactory: LinkCheckerFactory<HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker>,\n    initialFolder: String,\n    initialName: String = \"\",\n    downloadSystem: DownloadSystem,\n    scope: CoroutineScope,\n) : DownloadUiChecker<HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker>(\n    initialCredentials, linkCheckerFactory, initialFolder, initialName, downloadSystem, scope\n) {\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/add/HttpLinkChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http.add\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.util.FilenameFixer\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport ir.amirab.downloader.connection.HttpDownloaderClient\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass HttpLinkChecker(\n    initialCredentials: HttpDownloadCredentials = HttpDownloadCredentials.Companion.empty(),\n    private val client: HttpDownloaderClient,\n) : LinkChecker<HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes>(initialCredentials) {\n    private val _suggestedName = MutableStateFlow(null as String?)\n    override val suggestedName = _suggestedName.asStateFlow()\n\n    private val _length = MutableStateFlow(null as Long?)\n    override val downloadSize = _length.mapStateFlow {\n        it?.let(DownloadSize::Bytes)\n    }\n\n    override fun infoUpdated(responseInfo: HttpResponseInfo?) {\n        updateNameAndLength(responseInfo)\n    }\n\n    override suspend fun actualCheck(credentials: HttpDownloadCredentials): HttpResponseInfo {\n        return client.test(credentials)\n    }\n\n    private fun updateNameAndLength(responseInfo: HttpResponseInfo?) {\n        val suggestedName = responseInfo\n            ?.fileName\n            ?.let(FilenameFixer::fix)\n        val length = responseInfo?.run {\n            totalLength.takeIf { isSuccessFul }\n        }\n        _suggestedName.update { suggestedName }\n        _length.update { length }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/add/HttpNewDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http.add\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputs\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport com.abdownloadmanager.shared.downloaderinui.http.applyToHttpDownload\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass HttpNewDownloadInputs(\n    downloadUiChecker: HttpDownloadUiChecker,\n    scope: CoroutineScope,\n    private val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider\n) : NewDownloadInputs<\n        HttpDownloadItem,\n        HttpDownloadCredentials,\n        HttpResponseInfo,\n        DownloadSize.Bytes,\n        HttpLinkChecker,\n        >(\n    downloadUiChecker\n) {\n    //extra settings\n    private var threadCount = MutableStateFlow(null as Int?)\n    private var speedLimit = MutableStateFlow(0L)\n    private var fileChecksum = MutableStateFlow(null as FileChecksum?)\n    override val downloadItem: StateFlow<HttpDownloadItem> = combineStateFlows(\n        this.credentials,\n        this.folder,\n        this.name,\n        this.downloadSize,\n        this.speedLimit,\n        this.threadCount,\n        this.fileChecksum,\n    ) {\n            credentials,\n            folder,\n            name,\n            length,\n            speedLimit,\n            threadCount,\n            fileChecksum,\n        ->\n        HttpDownloadItem(\n            id = -1,\n            folder = folder,\n            name = name,\n            link = credentials.link,\n            contentLength = length?.bytes ?: IDownloadItem.LENGTH_UNKNOWN,\n            dateAdded = openedTime,\n            startTime = null,\n            completeTime = null,\n            status = DownloadStatus.Added,\n            preferredConnectionCount = threadCount,\n            speedLimit = speedLimit,\n            fileChecksum = fileChecksum?.toString()\n        ).withCredentials(credentials)\n    }\n\n    override val downloadJobConfig: StateFlow<DownloadJobExtraConfig?> = MutableStateFlow(null)\n\n    override fun applyHostSettingsToExtraConfig(extraConfig: PerHostSettingsItem) {\n        extraConfig.applyToHttpDownload(\n            setUsername = { setCredentials(credentials.value.copy(username = it)) },\n            setPassword = { setCredentials(credentials.value.copy(password = it)) },\n            setUserAgent = { setCredentials(credentials.value.copy(userAgent = it)) },\n            setThreadCount = { threadCount.value = it },\n            setSpeedLimit = { speedLimit.value = it }\n        )\n    }\n\n    override val configurableList = listOf(\n        SpeedLimitConfigurable(\n            Res.string.download_item_settings_speed_limit.asStringSource(),\n            Res.string.download_item_settings_speed_limit_description.asStringSource(),\n            backedBy = speedLimit,\n            describe = {\n                if (it == 0L) Res.string.unlimited.asStringSource()\n                else convertPositiveSpeedToHumanReadable(\n                    it, sizeAndSpeedUnitProvider.speedUnit.value\n                ).asStringSource()\n            }\n        ),\n        FileChecksumConfigurable(\n            Res.string.download_item_settings_file_checksum.asStringSource(),\n            Res.string.download_item_settings_file_checksum_description.asStringSource(),\n            backedBy = fileChecksum,\n            describe = { \"\".asStringSource() }\n        ),\n        IntConfigurable(\n            Res.string.settings_download_thread_count.asStringSource(),\n            Res.string.settings_download_thread_count_description.asStringSource(),\n            backedBy = threadCount.mapTwoWayStateFlow(\n                map = {\n                    it ?: 0\n                },\n                unMap = {\n                    it.takeIf { it >= 1 }\n                }\n            ),\n            range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n            describe = {\n                if (it == 0) Res.string.use_global_settings.asStringSource()\n                else Res.string.download_item_settings_thread_count_describe\n                    .asStringSourceWithARgs(\n                        Res.string.download_item_settings_thread_count_describe_createArgs(\n                            count = it.toString()\n                        )\n                    )\n            }\n        ),\n        StringConfigurable(\n            Res.string.username.asStringSource(),\n            Res.string.download_item_settings_username_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = credentials.mapStateFlow {\n                    it.username.orEmpty()\n                },\n                updater = {\n                    setCredentials(credentials.value.copy(username = it.takeIf { it.isNotBlank() }))\n                }, scope\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.password.asStringSource(),\n            Res.string.download_item_settings_password_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = credentials.mapStateFlow {\n                    it.password.orEmpty()\n                },\n                updater = {\n                    setCredentials(credentials.value.copy(password = it.takeIf { it.isNotBlank() }))\n                }, scope\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_user_agent.asStringSource(),\n            Res.string.download_item_settings_user_agent_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.userAgent.orEmpty()\n                },\n                unMap = {\n                    copy(userAgent = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_download_page.asStringSource(),\n            Res.string.download_item_settings_download_page_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.downloadPage.orEmpty()\n                },\n                unMap = {\n                    copy(downloadPage = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        )\n    )\n\n    override fun downloadSizeToStringSource(downloadSize: DownloadSize.Bytes): StringSource {\n        return downloadSize.asStringSource(sizeAndSpeedUnitProvider.sizeUnit.value)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/applyHostSettingsToExtraConfig.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http\n\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\n\nfun PerHostSettingsItem.applyToHttpDownload(\n    downloadCredentials: HttpDownloadCredentials\n): HttpDownloadCredentials {\n    var out = downloadCredentials\n    applyToHttpDownload(\n        setUsername = {\n            out = out.copy(username = it)\n        },\n        setPassword = {\n            out = out.copy(password = it)\n        },\n        setUserAgent = {\n            out = out.copy(userAgent = it)\n        },\n        setThreadCount = {},\n        setSpeedLimit = {}\n    )\n    return out\n}\n\nfun PerHostSettingsItem.applyToHttpDownload(\n    downloadCredentials: HttpDownloadItem\n): HttpDownloadItem {\n    var out = downloadCredentials\n    applyToHttpDownload(\n        setUsername = {\n            out = out.copy(username = it)\n        },\n        setPassword = {\n            out = out.copy(password = it)\n        },\n        setUserAgent = {\n            out = out.copy(userAgent = it)\n        },\n        setThreadCount = {\n            out = out.copy(preferredConnectionCount = it)\n        },\n        setSpeedLimit = {\n            out = out.copy(speedLimit = it)\n        }\n    )\n    return out\n}\n\nfun PerHostSettingsItem.applyToHttpDownload(\n    setUsername: (String) -> Unit,\n    setPassword: (String) -> Unit,\n    setUserAgent: (String) -> Unit,\n    setThreadCount: (Int) -> Unit,\n    setSpeedLimit: (Long) -> Unit,\n) {\n    username?.let(setUsername)\n    password?.let(setPassword)\n    userAgent?.let(setUserAgent)\n    threadCount?.let(setThreadCount)\n    speedLimit?.let(setSpeedLimit)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/edit/HttpEditDownloadChecker.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http.edit\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkChecker\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditDownloadResult\nimport com.abdownloadmanager.shared.downloaderinui.edit.CanEditWarnings\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.http.add.HttpLinkChecker\nimport ir.amirab.downloader.connection.IResponseInfo\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.util.FileNameValidator\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nabstract class EditDownloadChecker<\n        TDownloadItem : IDownloadItem,\n        TCredentials : IDownloadCredentials,\n        TResponseInfo : IResponseInfo,\n        TDownloadSize : DownloadSize,\n        TLinkChecker : LinkChecker<TCredentials, TResponseInfo, TDownloadSize>\n        >(\n    val currentDownloadItem: MutableStateFlow<TDownloadItem>,\n    val editedDownloadItem: MutableStateFlow<TDownloadItem>,\n    val linkChecker: TLinkChecker,\n    val conflictDetector: DownloadConflictDetector,\n    val scope: CoroutineScope,\n) {\n    abstract fun check()\n\n    protected val _canEditResult = MutableStateFlow<CanEditDownloadResult>(CanEditDownloadResult.NothingChanged)\n    val canEditResult = _canEditResult.asStateFlow()\n    val canEdit = canEditResult.mapStateFlow {\n        it is CanEditDownloadResult.CanEdit\n    }\n}\n\nclass HttpEditDownloadChecker(\n    currentDownloadItem: MutableStateFlow<HttpDownloadItem>,\n    editedDownloadItem: MutableStateFlow<HttpDownloadItem>,\n    conflictDetector: DownloadConflictDetector,\n    scope: CoroutineScope,\n    linkChecker: HttpLinkChecker,\n) : EditDownloadChecker<HttpDownloadItem, HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker>(\n    currentDownloadItem = currentDownloadItem,\n    editedDownloadItem = editedDownloadItem,\n    conflictDetector = conflictDetector,\n    scope = scope,\n    linkChecker = linkChecker\n) {\n    init {\n        editedDownloadItem\n            .onEach {\n                _canEditResult.value = CanEditDownloadResult.Waiting\n            }.launchIn(scope)\n    }\n\n    override fun check() {\n        _canEditResult.value = CanEditDownloadResult.Waiting\n        _canEditResult.value = check(\n            current = currentDownloadItem.value,\n            edited = editedDownloadItem.value,\n            newLength = linkChecker.downloadSize.value?.bytes,\n        )\n    }\n\n    private fun check(\n        current: HttpDownloadItem,\n        edited: HttpDownloadItem,\n        newLength: Long?,\n    ): CanEditDownloadResult {\n        if (current == edited) {\n            return CanEditDownloadResult.NothingChanged\n        }\n        if (!HttpUrlUtils.isValidUrl(edited.link)) {\n            return CanEditDownloadResult.InvalidURL\n        }\n        if (edited.name != current.name) {\n            if (!FileNameValidator.isValidFileName(edited.name)) {\n                return CanEditDownloadResult.InvalidFileName\n            }\n            if (conflictDetector.checkAlreadyExists(current, edited)) {\n                return CanEditDownloadResult.FileNameAlreadyExists\n            }\n        }\n        val warnings = mutableListOf<CanEditWarnings>()\n        if (current.contentLength != newLength) {\n            warnings.add(\n                CanEditWarnings.FileSizeNotMatch(\n                    currentSize = current.contentLength,\n                    newSize = newLength ?: IDownloadItem.Companion.LENGTH_UNKNOWN,\n                )\n            )\n        }\n        return CanEditDownloadResult.CanEdit(warnings)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/downloaderinui/http/edit/HttpEditDownloadInputs.kt",
    "content": "package com.abdownloadmanager.shared.downloaderinui.http.edit\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.LinkCheckerFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadCheckerFactory\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.http.HttpCredentialsToItemMapper\nimport com.abdownloadmanager.shared.downloaderinui.http.add.HttpLinkChecker\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport ir.amirab.downloader.connection.response.HttpResponseInfo\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass HttpEditDownloadInputs(\n    currentDownloadItem: MutableStateFlow<HttpDownloadItem>,\n    editedDownloadItem: MutableStateFlow<HttpDownloadItem>,\n    val sizeAndSpeedUnitProvider: SizeAndSpeedUnitProvider,\n    mapper: HttpCredentialsToItemMapper,\n    conflictDetector: DownloadConflictDetector,\n    scope: CoroutineScope,\n    linkCheckerFactory: LinkCheckerFactory<HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker>,\n    editDownloadCheckerFactory: EditDownloadCheckerFactory<HttpDownloadItem, HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker>\n) : EditDownloadInputs<HttpDownloadItem, HttpDownloadCredentials, HttpResponseInfo, DownloadSize.Bytes, HttpLinkChecker, HttpCredentialsToItemMapper>(\n    currentDownloadItem = currentDownloadItem,\n    editedDownloadItem = editedDownloadItem,\n    mapper = mapper,\n    scope = scope,\n    conflictDetector = conflictDetector,\n    linkCheckerFactory = linkCheckerFactory,\n    editDownloadCheckerFactory = editDownloadCheckerFactory,\n) {\n\n    override val configurableList = listOf(\n        SpeedLimitConfigurable(\n            Res.string.download_item_settings_speed_limit.asStringSource(),\n            Res.string.download_item_settings_speed_limit_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.speedLimit\n                },\n                unMap = {\n                    copy(speedLimit = it)\n                }\n            ),\n            describe = {\n                if (it == 0L) Res.string.unlimited.asStringSource()\n                else convertPositiveSpeedToHumanReadable(it, sizeAndSpeedUnitProvider.speedUnit.value).asStringSource()\n            }\n        ),\n        FileChecksumConfigurable(\n            Res.string.download_item_settings_file_checksum.asStringSource(),\n            Res.string.download_item_settings_file_checksum_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.fileChecksum?.let {\n                        runCatching {\n                            FileChecksum.Companion.fromString(it)\n                        }.onFailure {\n                            println(it.printStackTrace())\n                        }.getOrNull()\n                    }\n                },\n                unMap = {\n                    copy(fileChecksum = it?.toString())\n                }\n            ),\n            describe = { \"\".asStringSource() }\n        ),\n        IntConfigurable(\n            Res.string.settings_download_thread_count.asStringSource(),\n            Res.string.settings_download_thread_count_description.asStringSource(),\n            backedBy = editedDownloadItem.mapTwoWayStateFlow(\n                map = {\n                    it.preferredConnectionCount ?: 0\n                },\n                unMap = {\n                    copy(\n                        preferredConnectionCount = it.takeIf { it >= 1 }\n                    )\n                }\n            ),\n            range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n            describe = {\n                if (it == 0) Res.string.use_global_settings.asStringSource()\n                else Res.string.download_item_settings_thread_count_describe\n                    .asStringSourceWithARgs(\n                        Res.string.download_item_settings_thread_count_describe_createArgs(\n                            count = it.toString()\n                        )\n                    )\n            }\n        ),\n        StringConfigurable(\n            Res.string.username.asStringSource(),\n            Res.string.download_item_settings_username_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.username.orEmpty()\n                },\n                unMap = {\n                    copy(username = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.password.asStringSource(),\n            Res.string.download_item_settings_password_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.password.orEmpty()\n                },\n                unMap = {\n                    copy(password = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_user_agent.asStringSource(),\n            Res.string.download_item_settings_user_agent_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.userAgent.orEmpty()\n                },\n                unMap = {\n                    copy(userAgent = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n        StringConfigurable(\n            Res.string.download_item_settings_download_page.asStringSource(),\n            Res.string.download_item_settings_download_page_description.asStringSource(),\n            backedBy = credentials.mapTwoWayStateFlow(\n                map = {\n                    it.downloadPage.orEmpty()\n                },\n                unMap = {\n                    copy(downloadPage = it.takeIf { it.isNotEmpty() })\n                }\n            ),\n            describe = {\n                \"\".asStringSource()\n            }\n        ),\n    )\n    val length = linkChecker.downloadSize\n    override val downloadJobConfig: MutableStateFlow<DownloadJobExtraConfig?> = MutableStateFlow(null)\n\n    private fun HttpDownloadItem.applyOurChanges(edited: HttpDownloadItem) {\n        // we don't change some of these properties, so I commented them\n\n        link = edited.link\n        headers = edited.headers\n        username = edited.username\n        password = edited.password\n        downloadPage = edited.downloadPage\n        userAgent = edited.userAgent\n\n//        id = edited.id\n        folder = edited.folder\n        name = edited.name\n\n        contentLength = edited.contentLength\n        serverETag = edited.serverETag\n\n//        dateAdded = edited.dateAdded\n//        startTime = edited.startTime\n//        completeTime = edited.completeTime\n//        status = edited.status\n        preferredConnectionCount = edited.preferredConnectionCount\n        speedLimit = edited.speedLimit\n\n        fileChecksum = edited.fileChecksum\n    }\n\n    override fun applyEditedItemTo(item: HttpDownloadItem) {\n        val edited = editedDownloadItem.value\n        item.applyOurChanges(edited)\n    }\n\n    init {\n        length.onEach {\n            scheduleRefresh(alsoRecheckLink = false)\n        }.launchIn(scope)\n    }\n\n    override fun downloadSizeToStringSource(downloadSize: DownloadSize.Bytes): StringSource {\n        return downloadSize.asStringSource(sizeAndSpeedUnitProvider.sizeUnit.value)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/AboutPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface AboutPageManager {\n    fun openAboutPage()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/AddDownloadDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\n\ninterface AddDownloadDialogManager {\n    fun closeAddDownloadDialog()\n    fun openAddDownloadDialog(\n        links: List<AddDownloadCredentialsInUiProps>,\n        importOptions: ImportOptions = ImportOptions(),\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/BatchDownloadPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface BatchDownloadPageManager {\n    fun openBatchDownloadPage()\n    fun closeBatchDownload()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/CategoryDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface CategoryDialogManager {\n    fun openCategoryDialog(categoryId: Long)\n    fun closeCategoryDialog()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/DownloadDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface DownloadDialogManager {\n    fun openDownloadDialog(id: Long)\n    fun closeDownloadDialog()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/EditDownloadDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface EditDownloadDialogManager {\n    fun openEditDownloadDialog(id: Long)\n    fun closeEditDownloadDialog()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/EnterNewURLDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface EnterNewURLDialogManager {\n    fun openEnterNewURLWindow()\n    fun closeEnterNewURLWindow()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/ExitApplicationRequestManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface ExitApplicationRequestManager {\n    suspend fun requestExitApp()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/FileChecksumDialogManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface FileChecksumDialogManager {\n    fun openFileChecksumPage(ids: List<Long>)\n\n    fun closeFileChecksumPage(dialogId: String)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/NotificationSender.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport ir.amirab.util.compose.StringSource\n\ninterface NotificationSender {\n    fun sendDialogNotification(title: StringSource, description: StringSource, type: MessageDialogType)\n    fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/OpenSourceLibrariesPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface OpenSourceLibrariesPageManager {\n    fun openOpenSourceLibrariesPage()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/PerHostSettingsPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface PerHostSettingsPageManager {\n    fun openPerHostSettings(openedHost: String?)\n    fun closePerHostSettings()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/QueuePageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface QueuePageManager : QueueItemPageManager, NewQueuePageManager\ninterface QueueItemPageManager {\n    fun openQueues(openQueueId: Long? = null)\n    fun closeQueues()\n}\n\ninterface NewQueuePageManager {\n    fun openNewQueueDialog()\n    fun closeNewQueueDialog()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/SettingsPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface SettingsPageManager {\n    fun openSettings()\n    fun closeSettings()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pagemanager/TranslatorsPageManager.kt",
    "content": "package com.abdownloadmanager.shared.pagemanager\n\ninterface TranslatorsPageManager {\n    fun openTranslatorsPage()\n    fun closeTranslatorsPage()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/AddDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload\n\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nabstract class AddDownloadComponent(\n    ctx: ComponentContext,\n    val id: String,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n) : BaseComponent(ctx) {\n    companion object {\n        const val lastLocationsCacheSize = 4\n\n    }\n\n    abstract fun getCategoryPageManager(): CategoryDialogManager\n    fun onRequestAddCategory() {\n        getCategoryPageManager().openCategoryDialog(-1)\n    }\n\n    private var dialogUsed = false\n    protected fun consumeDialog(block: () -> Unit) {\n        if (dialogUsed) {\n            return\n        }\n        block()\n        dialogUsed = true\n    }\n\n    private val _lastUsedLocations = lastSavedLocationsStorage.lastUsedSaveLocations\n    val lastUsedLocations: StateFlow<List<String>> = _lastUsedLocations.asStateFlow()\n    fun addToLastUsedLocations(saveLocation: String) {\n        _lastUsedLocations.update {\n            buildList {\n                add(saveLocation)\n                addAll(it)\n            }\n                .distinct()\n                .take(lastLocationsCacheSize)\n        }\n    }\n\n    fun removeFromLastDownloadLocation(saveLocation: String) {\n        _lastUsedLocations.update {\n            it.filter { it != saveLocation }\n        }\n    }\n\n    abstract val shouldShowWindow: StateFlow<Boolean>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/AddDownloadConfig.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload\n\nimport kotlinx.serialization.Serializable\nimport java.util.*\n\n@Serializable\nsealed interface AddDownloadConfig {\n    val id: String\n    val importOptions: ImportOptions\n\n    @Serializable\n    data class SingleAddConfig(\n        val newDownload: AddDownloadCredentialsInUiProps,\n        override val importOptions: ImportOptions = ImportOptions(),\n        override val id: String = UUID.randomUUID().toString(),\n    ) : AddDownloadConfig\n\n    @Serializable\n    data class MultipleAddConfig(\n        val newDownloads: List<AddDownloadCredentialsInUiProps> = emptyList(),\n        override val importOptions: ImportOptions = ImportOptions(),\n        override val id: String = UUID.randomUUID().toString(),\n    ) : AddDownloadConfig\n\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/AddDownloadCredentialsInUiProps.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload\n\nimport com.abdownloadmanager.shared.util.FilenameFixer\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AddDownloadCredentialsInUiProps(\n    val credentials: IDownloadCredentials,\n    val extraConfig: Configs = Configs(),\n) {\n    @Serializable\n    data class Configs(\n        // don't consume it directly as it might not be a valid file name on user's current OS\n        val suggestedName: String? = null,\n    ) {\n        fun getAndFixSuggestedName(): String? {\n            return suggestedName?.let(FilenameFixer::fix)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/ImportOptions.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SilentImportOptions(\n    val silentDownload: Boolean,\n)\n\n@Serializable\ndata class ImportOptions(\n    val silentImport: SilentImportOptions? = null,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/multiple/BaseAddMultiDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload.multiple\n\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport arrow.core.Some\nimport com.abdownloadmanager.shared.downloaderinui.DownloadSize\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputs\nimport com.abdownloadmanager.shared.downloaderinui.add.NewDownloadInputsUniqueIdType\nimport com.abdownloadmanager.shared.downloaderinui.add.TANewDownloadInputs\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadComponent\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryItem\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.CategorySelectionMode\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.perhostsettings.getSettingsForURL\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.SelectionUtil\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.downloaditem.EmptyContext\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.ifThen\nimport ir.amirab.util.wildcardMatch\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nabstract class BaseAddMultiDownloadComponent(\n    ctx: ComponentContext,\n    id: String,\n    private val onRequestClose: () -> Unit,\n    private val onRequestAdd: OnRequestAdd,\n    private val appRepository: BaseAppRepository,\n    private val perHostSettingsManager: PerHostSettingsManager,\n    val downloadSystem: DownloadSystem,\n    val fileIconProvider: FileIconProvider,\n    private val categoryManager: CategoryManager,\n    val downloaderInUiRegistry: DownloaderInUiRegistry,\n    protected val queueManager: QueueManager,\n    lastSavedLocationsStorage: ILastSavedLocationsStorage,\n) : AddDownloadComponent(ctx, id, lastSavedLocationsStorage) {\n    override val shouldShowWindow: StateFlow<Boolean> = MutableStateFlow(true)\n\n    private val _folder = MutableStateFlow(appRepository.saveLocation.value)\n    val folder = _folder.asStateFlow()\n    fun setFolder(folder: String) {\n        this._folder.update { folder }\n        totalList.forEach {\n            it.folder.update { folder }\n        }\n    }\n\n    // when we select all files in one location let user option to auto categorize items\n    private val _alsoAutoCategorize = MutableStateFlow(true)\n    val alsoAutoCategorize = _alsoAutoCategorize.asStateFlow()\n    fun setAlsoAutoCategorize(value: Boolean) {\n        _alsoAutoCategorize.update { value }\n    }\n\n    val categories = categoryManager.categoriesFlow\n    private val _selectedCategory = MutableStateFlow<Category?>(null)\n    val selectedCategory = _selectedCategory.asStateFlow()\n\n    fun setSelectedCategory(category: Category?) {\n        _selectedCategory.update {\n            category\n        }\n    }\n\n    private val _allInSameLocation = MutableStateFlow(false)\n    val allInSameLocation = _allInSameLocation.asStateFlow()\n\n    fun setAllItemsInSameLocation(sameLocation: Boolean) {\n        _allInSameLocation.update { sameLocation }\n    }\n\n    private val _filterText = MutableStateFlow(\"\")\n    val filterText = _filterText.asStateFlow()\n    fun setFilterText(text: String) {\n        _filterText.update { text }\n    }\n\n    private fun newCheckerWithInputs(\n        addDownloadCredentialsInUiProps: AddDownloadCredentialsInUiProps\n    ): TANewDownloadInputs? {\n        val iDownloadCredentials = addDownloadCredentialsInUiProps.credentials\n        return downloaderInUiRegistry\n            .getDownloaderOf(iDownloadCredentials)\n            ?.createNewDownloadInputs(\n                initialCredentials = iDownloadCredentials,\n                initialName = addDownloadCredentialsInUiProps.extraConfig.getAndFixSuggestedName().orEmpty(),\n                initialFolder = folder.value,\n                downloadSystem = downloadSystem,\n                scope = scope,\n            )\n    }\n\n    fun addItems(list: List<AddDownloadCredentialsInUiProps>) {\n        val newItemsToAdd = list.filter {\n            it.credentials !in this.totalList.map {\n                it.credentials.value\n            }\n        }.mapNotNull {\n            newCheckerWithInputs(it)\n                ?.also { inputComponent ->\n                    val perHostSettingsItem = perHostSettingsManager\n                        .getSettingsForURL(it.credentials.link)\n                    perHostSettingsItem?.let {\n                        inputComponent\n                            .applyHostSettingsToExtraConfig(perHostSettingsItem)\n                    }\n                }\n        }\n        enqueueCheck(newItemsToAdd)\n        this.totalList = this.totalList.plus(newItemsToAdd)\n    }\n\n    var totalList: List<TANewDownloadInputs> by mutableStateOf(emptyList())\n\n    private val checkList = MutableSharedFlow<TANewDownloadInputs>()\n    private fun enqueueCheck(links: List<TANewDownloadInputs>) {\n        scope.launch {\n            for (i in links) {\n                checkList.emit(i)\n            }\n        }\n    }\n\n    init {\n        checkList.onEach {\n            it.downloadUiChecker.refresh()\n        }\n            .launchIn(scope)\n    }\n\n    var selectionList by mutableStateOf<List<NewDownloadInputsUniqueIdType>>(emptyList())\n\n    fun isSelected(itemId: NewDownloadInputsUniqueIdType): Boolean {\n        return itemId in selectionList\n    }\n\n    val isTotalSelected by derivedStateOf {\n        totalList.all { it.getUniqueId() in selectionList }\n    }\n\n    var lastSelectedId by mutableStateOf(null as NewDownloadInputsUniqueIdType?)\n\n    fun setSelect(id: NewDownloadInputsUniqueIdType, selected: Boolean) {\n        if (selected) {\n            lastSelectedId = id\n            if (!selectionList.contains(id)) {\n                selectionList = selectionList.plus(id)\n            }\n        } else {\n            selectionList = selectionList.minus(id)\n        }\n    }\n\n    fun resetSelectionTo(ids: List<NewDownloadInputsUniqueIdType>, boolean: Boolean) {\n        selectionList = ids.takeIf { boolean }\n            .orEmpty()\n    }\n\n    fun selectAll(value: Boolean) {\n        selectionList = if (value) {\n            filteredList.value.map { it.id }\n        } else {\n            emptyList()\n        }\n    }\n\n    fun toggleSelectInside() {\n        val list = filteredList.value\n        val listIds = list.map { it.id }\n        val selection = selectionList.filter {\n            it !in listIds\n        }\n        SelectionUtil.toggleSelectInside(\n            selectionList = selection,\n            fullSortedList = list,\n            getId = {\n                it.id\n            }\n        )?.let {\n            selectionList = it\n        }\n    }\n\n    fun inverseSelection() {\n        val list = filteredList.value\n        val listIds = list.map { it.id }\n        val selection = selectionList.filter {\n            it !in listIds\n        }\n        selectionList = SelectionUtil.invertSelection(\n            selectionList = selection,\n            all = list,\n            getId = {\n                it.id\n            }\n        )\n    }\n\n    val canClickAdd by derivedStateOf {\n        selectionList.isNotEmpty()\n    }\n    val queueList = queueManager.queues\n\n    private fun getFolderForItem(\n        categorySelectionMode: CategorySelectionMode?,\n        allInSameLocation: Boolean,\n        url: String,\n        fleName: String,\n        defaultFolder: String,\n    ): String {\n        if (allInSameLocation) return defaultFolder\n        return when (categorySelectionMode) {\n            CategorySelectionMode.Auto -> {\n                downloadSystem.categoryManager\n                    .getCategoryOf(\n                        CategoryItem(\n                            url = url,\n                            fileName = fleName,\n                        )\n                    )?.getDownloadPath()\n                    ?: defaultFolder\n            }\n\n            is CategorySelectionMode.Fixed -> {\n                downloadSystem.categoryManager\n                    .getCategoryById(categorySelectionMode.categoryId)?.getDownloadPath()\n                    ?: defaultFolder\n            }\n\n            null -> defaultFolder\n        }\n    }\n\n    fun requestAddDownloads(\n        queueId: Long?, startQueue: Boolean,\n    ) {\n\n        val categorySelectionMode = when {\n            alsoAutoCategorize.value -> CategorySelectionMode.Auto\n            else -> selectedCategory.value?.let {\n                CategorySelectionMode.Fixed(it.id)\n            }\n        }\n        val itemsToAdd = totalList\n            .filter { it.getUniqueId() in selectionList }\n            .filter {\n                val checker = it.downloadUiChecker\n                checker.canAdd.value\n                        || checker.isDuplicate.value // we add numbered file strategy\n            }\n            .map {\n                NewDownloadItemProps(\n                    downloadItem = it.downloadItem.value.copy(\n                        folder = Some(\n                            getFolderForItem(\n                                categorySelectionMode = categorySelectionMode,\n                                url = it.credentials.value.link,\n                                fleName = it.name.value,\n                                defaultFolder = it.folder.value,\n                                allInSameLocation = allInSameLocation.value\n                            )\n                        )\n                    ),\n                    extraConfig = it.downloadJobConfig.value,\n                    onDuplicateStrategy = OnDuplicateStrategy.AddNumbered,\n                    context = EmptyContext,\n                )\n            }\n        consumeDialog {\n            onRequestAdd(\n                items = itemsToAdd,\n                queueId = queueId,\n                categorySelectionMode = categorySelectionMode\n            ).invokeOnCompletion {\n                val folder = folder.value\n                if (allInSameLocation.value) {\n                    addToLastUsedLocations(folder)\n                }\n                if (startQueue && queueId != null) {\n                    scope.launch {\n                        downloadSystem.startQueue(queueId)\n                    }\n                }\n            }\n            requestClose()\n        }\n    }\n\n    var showAddToQueue by mutableStateOf(false)\n        private set\n\n    fun getIdOf(item: TANewDownloadInputs): Int {\n        return item.getUniqueId()\n    }\n\n    fun openConfigurableList(\n        itemID: NewDownloadInputsUniqueIdType?\n    ) {\n        currentDownloadConfigurableList.value = itemID?.let { id ->\n            totalList.find { getIdOf(it) == id }\n        }?.configurableList\n    }\n\n    val currentDownloadConfigurableList: MutableStateFlow<List<Configurable<*>>?> = MutableStateFlow(null)\n\n    fun openAddToQueueDialog() {\n        showAddToQueue = true\n    }\n\n    fun closeAddToQueue() {\n        showAddToQueue = false\n    }\n\n    fun requestClose() {\n        onRequestClose()\n    }\n\n    val listStateFlow: Flow<List<NewMultiDownloadState>> = snapshotFlow { totalList }\n        .flatMapLatest { downloadInputs ->\n            if (downloadInputs.isEmpty()) flowOf(emptyList())\n            else combine(\n                downloadInputs.map { it.asNewDownloadState() },\n            ) {\n                it.toList()\n            }\n        }\n\n    val filteredList = combine(\n        listStateFlow,\n        filterText,\n    ) { list, filterText ->\n        val filterText = filterText.trim()\n        list.ifThen(filterText.isNotBlank()) {\n            filter {\n                wildcardMatch(filterText, it.name)\n            }\n        }\n    }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    val selectedTotalSize = combine(\n        snapshotFlow { selectionList },\n        filteredList,\n    ) { selection, list ->\n        list.filter { it.id in selection }\n            .mapNotNull { it.size }\n            .groupBy { it::class }\n            .values\n            .map {\n                it.fold(it.first()) { acc, item ->\n                    acc.plus(item)\n                }\n            }\n    }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n    val isAllFilteredSelected = combine(\n        snapshotFlow { selectionList },\n        filteredList,\n    ) { selection, list ->\n        val ids = list.map { it.id }\n        ids.all { it in selection }\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n\n    private fun NewDownloadInputs<*, *, *, *, *>.asNewDownloadState(): Flow<NewMultiDownloadState> {\n        val id = this@asNewDownloadState.getUniqueId()\n        return combine(\n            name,\n            credentials,\n            downloadUiChecker.downloadSize,\n            lengthStringFlow,\n        ) { name, credentials, downloadSize, lengthString ->\n            NewMultiDownloadState(\n                id = id,\n                name = name,\n                size = downloadSize,\n                sizeString = lengthString,\n                link = credentials.link,\n            )\n        }\n    }\n\n}\n\n/**\n * this is used to represent multiple download list table\n */\ndata class NewMultiDownloadState(\n    val id: NewDownloadInputsUniqueIdType,\n    val name: String,\n    val size: DownloadSize?,\n    val sizeString: StringSource,\n    val link: String,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/multiple/OnRequestAdd.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload.multiple\n\nimport com.abdownloadmanager.shared.util.category.CategorySelectionMode\nimport ir.amirab.downloader.NewDownloadItemProps\nimport kotlinx.coroutines.Deferred\n\nfun interface OnRequestAdd {\n    operator fun invoke(\n        items: List<NewDownloadItemProps>,\n        queueId: Long?,\n        categorySelectionMode: CategorySelectionMode?,\n    ): Deferred<List<Long>>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/adddownload/single/BaseAddSingleDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.adddownload.single\n\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadComponent\nimport androidx.compose.runtime.*\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.adddownload.ImportOptions\nimport com.abdownloadmanager.shared.pages.adddownload.SilentImportOptions\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.*\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport ir.amirab.downloader.utils.orDefault\nimport ir.amirab.util.flow.*\nimport kotlinx.coroutines.flow.*\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryItem\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.downloaderinui.add.CanAddResult\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUi\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.abdownloadmanager.shared.util.perhostsettings.getSettingsForURL\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.EmptyContext\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.queue.DefaultQueueInfo\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.selects.select\nimport org.koin.core.component.inject\nimport kotlin.getValue\n\nabstract class BaseAddSingleDownloadComponent(\n    ctx: ComponentContext,\n    val onRequestClose: () -> Unit,\n    val onRequestDownload: OnRequestDownloadSingleItem,\n    private val onRequestAddToQueue: OnRequestAddSingleItem,\n    val openExistingDownload: (Long) -> Unit,\n    val updateExistingDownloadCredentials: (Long, IDownloadCredentials, DownloadJobExtraConfig?) -> Unit,\n    protected val downloadItemOpener: DownloadItemOpener,\n    protected val lastSavedLocationsStorage: ILastSavedLocationsStorage,\n    protected val appScope: CoroutineScope,\n    protected val appSettings: BaseAppSettingsStorage,\n    protected val appRepository: BaseAppRepository,\n    protected val perHostSettingsManager: PerHostSettingsManager,\n    protected val categoryManager: CategoryManager,\n    val downloadSystem: DownloadSystem,\n    val iconProvider: FileIconProvider,\n    protected val queueManager: QueueManager,\n    importOptions: ImportOptions,\n    id: String,\n    downloaderInUi: DownloaderInUi<IDownloadCredentials, *, *, *, *, *, *, *, *, *>,\n    initialCredentials: AddDownloadCredentialsInUiProps,\n) : AddDownloadComponent(ctx, id, lastSavedLocationsStorage),\n    ContainsEffects<BaseAddSingleDownloadComponent.Effects> by supportEffects() {\n    private val _shouldShowWindow = MutableStateFlow(importOptions.silentImport == null)\n    override val shouldShowWindow: StateFlow<Boolean> = _shouldShowWindow.asStateFlow()\n    val downloadInputsComponent = downloaderInUi.createNewDownloadInputs(\n        initialFolder = appRepository.saveLocation.value,\n        initialName = initialCredentials.extraConfig.getAndFixSuggestedName().orEmpty(),\n        downloadSystem = downloadSystem,\n        scope = scope,\n        initialCredentials = initialCredentials.credentials,\n    )\n    val downloadChecker = downloadInputsComponent.downloadUiChecker\n\n    val categories = categoryManager.categoriesFlow\n    private val _selectedCategory: MutableStateFlow<Category?> = MutableStateFlow(categories.value.firstOrNull())\n    val selectedCategory = _selectedCategory.asStateFlow()\n\n    private val _useCategory = MutableStateFlow(false)\n    val useCategory = _useCategory.asStateFlow()\n    fun setUseCategory(useCategory: Boolean) {\n        _useCategory.update { useCategory }\n        if (useCategory) {\n            val usedCategoryFolder = useCategoryFolder(_useCategory.value)\n            if (!usedCategoryFolder) {\n                useDefaultFolder()\n            }\n        } else {\n            useDefaultFolder()\n        }\n    }\n\n    private fun useCategoryFolder(\n        useCategory: Boolean,\n    ): Boolean {\n        val category = selectedCategory.value\n        if (useCategory && category != null) {\n            category.getDownloadPath()?.let {\n                setFolder(it)\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun useDefaultFolder() {\n        setFolder(appRepository.saveLocation.value)\n    }\n\n\n    fun setSelectedCategory(category: Category) {\n        _selectedCategory.update { category }\n        val useCategory = useCategory.value\n        if (useCategory) {\n            val used = useCategoryFolder(useCategory)\n            if (!used) {\n                useDefaultFolder()\n            }\n        }\n    }\n\n\n    //inputs\n    val credentials = downloadChecker.credentials.asStateFlow()\n    val name = downloadChecker.name.asStateFlow()\n    val folder = downloadChecker.folder.asStateFlow()\n    val onDuplicateStrategy: MutableStateFlow<OnDuplicateStrategy?> = MutableStateFlow(null)\n\n    fun setCredentials(downloadCredentials: IDownloadCredentials) {\n        downloadChecker.credentials.update { downloadCredentials }\n    }\n\n    fun setFolder(folder: String) {\n        downloadChecker.folder.update { folder }\n    }\n\n    fun setName(name: String) {\n        downloadChecker.name.update { name }\n    }\n\n    fun setOnDuplicateStrategy(onDuplicateStrategy: OnDuplicateStrategy) {\n        this.onDuplicateStrategy.update { onDuplicateStrategy }\n    }\n\n    fun getLengthString(): StringSource {\n        return downloadInputsComponent.getLengthString()\n    }\n\n    init {\n        credentials\n            .map { it.link }\n            .distinctUntilChanged()\n            .debounce(250)\n            .onEachLatest { link ->\n                perHostSettingsManager\n                    .getSettingsForURL(link)\n                    ?.let(downloadInputsComponent::applyHostSettingsToExtraConfig)\n            }\n            .flowOn(Dispatchers.IO)\n            .launchIn(scope)\n        merge(\n            credentials.mapStateFlow { it.link },\n            name,\n            folder,\n        )\n            .onEachLatest { onDuplicateStrategy.update { null } }\n            .launchIn(scope)\n        combine(\n            name,\n            credentials.map { it.link },\n        ) { name, link ->\n            val category = categoryManager.getCategoryOf(\n                CategoryItem(\n                    fileName = name,\n                    url = link,\n                )\n            )\n            val globalUseCategoryByDefault = appSettings.useCategoryByDefault.value\n            val suggestedUseCategory: Boolean\n            if (category == null) {\n                suggestedUseCategory = false\n            } else {\n                setSelectedCategory(category)\n                suggestedUseCategory = true\n            }\n            if (globalUseCategoryByDefault) {\n                setUseCategory(suggestedUseCategory)\n            }\n        }.launchIn(scope)\n    }\n\n\n    val canAddResult = downloadChecker.canAddToDownloadResult.asStateFlow()\n    private val canAdd = downloadChecker.canAdd\n    private val isDuplicate = downloadChecker.isDuplicate\n\n    val isLinkLoading = downloadChecker.gettingResponseInfo\n    val linkResponseInfo = downloadChecker.responseInfo\n\n    val canAddToDownloads = combineStateFlows(\n        canAdd, isDuplicate, onDuplicateStrategy, isLinkLoading\n    ) { canAdd, isDuplicate, onDuplicateStrategy, isLinkLoading ->\n        if (isLinkLoading) {\n            // link is loading wait for it...\n            return@combineStateFlows false\n        }\n        if (canAdd) {\n            true\n        } else if (isDuplicate && onDuplicateStrategy != null) {\n            true\n        } else {\n            false\n        }\n    }\n\n    val downloadItem = downloadInputsComponent.downloadItem\n    val downloadJobConfig = downloadInputsComponent.downloadJobConfig\n\n\n    var showMoreSettings by mutableStateOf(false)\n\n\n    val configurables = downloadInputsComponent.configurableList\n\n    val queues = queueManager.queues\n        .stateIn(\n            scope,\n            SharingStarted.WhileSubscribed(),\n            emptyList()\n        )\n\n    fun refresh() {\n        downloadChecker.refresh()\n    }\n\n    fun onRequestDownload() {\n        val downloadItem = this@BaseAddSingleDownloadComponent.downloadItem.value\n        val downloadJobExtraConfig = downloadJobConfig.value\n        consumeDialog {\n            saveLocationIfNecessary(downloadItem.folder)\n            onRequestDownload(\n                item = NewDownloadItemProps(\n                    downloadItem = downloadItem,\n                    extraConfig = downloadJobExtraConfig,\n                    onDuplicateStrategy = onDuplicateStrategy.value.orDefault(),\n                    context = EmptyContext\n                ),\n                categoryId = getCategoryIfUseCategoryIsOn()?.id\n            )\n            onRequestClose()\n        }\n    }\n\n    private fun getCategoryIfUseCategoryIsOn(): Category? {\n        return if (useCategory.value)\n            selectedCategory.value\n        else\n            null\n    }\n\n    private fun saveLocationIfNecessary(folder: String) {\n        val category = getCategoryIfUseCategoryIsOn()\n        val shouldAdd = if (category == null) {\n            // always add if user don't use category\n            true\n        } else {\n            // only add if category path is not the same as provided path\n            category.getDownloadPath() != folder\n        }\n        if (shouldAdd) {\n            addToLastUsedLocations(folder)\n        }\n    }\n\n\n    fun onRequestAddToQueue(\n        queueId: Long?,\n        startQueue: Boolean,\n    ) {\n        val downloadItem = downloadItem.value\n        val downloadJobConfig = downloadJobConfig.value\n        consumeDialog {\n            saveLocationIfNecessary(downloadItem.folder)\n            onRequestAddToQueue(\n                item = NewDownloadItemProps(\n                    downloadItem = downloadItem,\n                    extraConfig = downloadJobConfig,\n                    onDuplicateStrategy = onDuplicateStrategy.value.orDefault(),\n                    context = EmptyContext,\n                ),\n                queueId = queueId,\n                categoryId = getCategoryIfUseCategoryIsOn()?.id,\n            ).invokeOnCompletion {\n                if (queueId != null && startQueue) {\n                    GlobalScope.launch {\n                        downloadSystem.startQueue(queueId)\n                    }\n                }\n            }\n            onRequestClose()\n        }\n    }\n\n    fun openDownloadFileForCurrentLink() {\n        (canAddResult.value as? CanAddResult.DownloadAlreadyExists)\n            ?.itemId\n            ?.let {\n                openExistingDownload(it)\n                onRequestClose()\n            }\n    }\n\n    fun updateDownloadCredentialsOfOriginalDownload() {\n        (canAddResult.value as? CanAddResult.DownloadAlreadyExists)\n            ?.itemId\n            ?.let {\n                updateExistingDownloadCredentials(it, downloadItem.value, downloadJobConfig.value)\n                onRequestClose()\n            }\n    }\n\n    var showSolutionsOnDuplicateDownloadUi by mutableStateOf(false)\n\n    var shouldShowAddToQueue by mutableStateOf(false)\n\n    val shouldShowOpenFile = combine(\n        onDuplicateStrategy, canAddResult,\n    ) { onDuplicateStrategy, result ->\n        if (result is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {\n            val item = downloadSystem.getDownloadItemById(result.itemId) ?: return@combine false\n            if (item.status != DownloadStatus.Completed) {\n                return@combine false\n            }\n            downloadSystem.getDownloadFile(item).exists()\n        } else false\n    }.stateIn(scope, SharingStarted.WhileSubscribed(), false)\n\n    fun openExistingFile() {\n        val itemId = (canAddResult.value as? CanAddResult.DownloadAlreadyExists)?.itemId ?: return\n        consumeDialog {\n            appScope.launch {\n                downloadItemOpener.openDownloadItem(itemId)\n            }\n            onRequestClose()\n        }\n    }\n\n    fun addNewCategory() {\n        onRequestAddCategory()\n    }\n\n    init {\n        importOptions.silentImport?.let {\n            handleSilentImport(it)\n        }\n    }\n\n    fun handleSilentImport(silentImport: SilentImportOptions) {\n        scope.launch {\n            try {\n                withTimeout(2_000) {\n                    // ensure all values are set!\n                    credentials.map { it.link }.first { it.isNotEmpty() }\n                    folder.first { it.isNotEmpty() }\n                    name.first { it.isNotEmpty() }\n                }\n            } catch (_: Exception) {\n                onRequestClose()\n                return@launch\n            }\n            val failAutoAdd = async {\n                try {\n                    // although we don't need timeout, but I add this timeout here maybe there is a bug\n                    // and I don't want this coroutine to be halted infinitely\n                    withTimeout(10_000) {\n                        canAddToDownloads.first { it }\n                        if (silentImport.silentDownload) {\n                            onRequestDownload()\n                        } else {\n                            onRequestAddToQueue(\n                                DefaultQueueInfo.ID,\n                                false,\n                            )\n                        }\n                    }\n                    false\n                } catch (_: Exception) {\n                    true\n                }\n            }\n\n            val errorDuringWait = async {\n                val channel = canAddResult.produceIn(this)\n                try {\n                    val startTime = System.currentTimeMillis()\n                    for (i in channel) {\n                        when (i) {\n                            is CanAddResult.DownloadAlreadyExists,\n                            CanAddResult.CantWriteInThisFolder -> {\n                                return@async true\n                            }\n\n                            CanAddResult.InvalidUrl,\n                            CanAddResult.InvalidFileName -> {\n                                // we may get invalid filename/invalid url at the beginning! because the name is empty\n                                if (System.currentTimeMillis() - startTime >= 1000) {\n                                    return@async true\n                                }\n                            }\n\n                            CanAddResult.CanAdd -> {\n                                // we must not break here because it cancels [failAutoAdd]\n                                // instead we wait for [failAutoAdd] to be finished and we will be cancelled automatically after select is done!\n                            }\n\n                            null -> {}\n                        }\n                    }\n                    return@async true\n                } finally {\n                    channel.cancel()\n                }\n            }\n            val failedToAutoAdd = try {\n                select {\n                    failAutoAdd.onAwait { failed ->\n                        failed\n                    }\n                    errorDuringWait.onAwait { errorDuringWait ->\n                        errorDuringWait\n                    }\n                }\n            } catch (_: Exception) {\n                true\n            } finally {\n                runCatching {\n                    failAutoAdd.cancelAndJoin()\n                    errorDuringWait.cancelAndJoin()\n                }\n            }\n            if (failedToAutoAdd) {\n                // needs adjustments by user!\n                _shouldShowWindow.value = true\n            }\n        }\n    }\n    sealed interface Effects {\n        sealed interface Common : Effects {\n            data class SuggestUrl(val link: String) : Common\n        }\n\n        interface Platform : Effects\n    }\n}\n\nfun interface OnRequestAddSingleItem {\n    operator fun invoke(\n        item: NewDownloadItemProps,\n        queueId: Long?,\n        categoryId: Long?,\n    ): Deferred<Long>\n}\n\nfun interface OnRequestDownloadSingleItem {\n    operator fun invoke(\n        item: NewDownloadItemProps,\n        categoryId: Long?,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/batchdownload/BaseBatchDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.batchdownload\n\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.math.max\n\nopen class BaseBatchDownloadComponent(\n    ctx: ComponentContext,\n    val onClose: () -> Unit,\n    val importLinks: (List<String>) -> Unit,\n) : BaseComponent(ctx), ContainsEffects<BaseBatchDownloadComponent.Effects> by supportEffects() {\n\n    private val _link = MutableStateFlow(\"\")\n    val link = _link.asStateFlow()\n\n    fun setLink(link: String) {\n        _link.value = link\n    }\n\n    private val _start = MutableStateFlow(\"\")\n    val start = _start.asStateFlow()\n\n    fun setStart(start: String) {\n        _start.value = start\n    }\n\n    private val _end = MutableStateFlow(\"\")\n    val end = _end.asStateFlow()\n\n    fun setEnd(end: String) {\n        _end.value = end\n    }\n\n    private val _wildcardLength = MutableStateFlow<WildcardLength>(WildcardLength.Auto)\n    val wildcardLength = _wildcardLength\n    fun setWildCardLength(wildcardLength: WildcardLength) {\n        _wildcardLength.value = wildcardLength\n    }\n\n    init {\n        fillLinkIfUrlIsInClipboard()\n    }\n\n    private fun fillLinkIfUrlIsInClipboard() {\n        scope.launch {\n            withContext(Dispatchers.Default) {\n                val clipboard = ClipboardUtil.read() ?: return@withContext\n                if (HttpUrlUtils.isValidUrl(clipboard)) {\n                    setLink(clipboard.trim())\n                }\n            }\n        }\n    }\n\n    @Suppress(\"NAME_SHADOWING\")\n    private val batch = combineStateFlows(\n        link,\n        start,\n        end,\n        wildcardLength,\n    ) { link, start, end, wildcardLength ->\n        val minimumSize = max(start.length, end.length)\n        val start = start.toIntOrNull() ?: return@combineStateFlows null\n        val end = end.toIntOrNull() ?: return@combineStateFlows null\n        if (start < 0) return@combineStateFlows null\n        if (end < 0 || end < start) return@combineStateFlows null\n        WildcardString(\n            string = link.trim(),\n            range = start..end,\n            wildcardLength = wildcardLength,\n            minimumAllowed = minimumSize,\n        )\n    }\n\n\n    val startLinkResult: StateFlow<String> = batch\n        .mapStateFlow { it?.first() ?: \"\" }\n    val endLinkResult: StateFlow<String> = batch\n        .mapStateFlow { it?.last() ?: \"\" }\n\n\n    val validationResult = batch.mapStateFlow {\n        when (it) {\n            null -> BatchDownloadValidationResult.Others\n            else -> {\n                val listSize = it.size()\n                when {\n                    listSize < 1 -> BatchDownloadValidationResult.Others\n                    listSize > MAX_ALLOWED_RANGE -> BatchDownloadValidationResult.MaxRangeExceed(MAX_ALLOWED_RANGE)\n                    !HttpUrlUtils.isValidUrl(it.first()) -> BatchDownloadValidationResult.URLInvalid\n                    else -> BatchDownloadValidationResult.Ok\n                }\n            }\n        }\n\n    }\n\n    val canConfirm = validationResult.mapStateFlow {\n        it is BatchDownloadValidationResult.Ok\n    }\n\n    fun confirm() {\n        if (!canConfirm.value) {\n            println(batch.value?.toList())\n            return\n        }\n        val items = batch.value?.toList()?.takeIf { it.isNotEmpty() }\n        if (items != null) {\n            importLinks(items)\n        }\n        onClose()\n    }\n\n    companion object {\n        const val MAX_ALLOWED_RANGE = 1000\n    }\n\n    sealed interface Effects {\n        interface PlatformEffects : Effects\n    }\n}\n\nsealed interface BatchDownloadValidationResult {\n    data object Ok : BatchDownloadValidationResult\n    data object Others : BatchDownloadValidationResult\n    data class MaxRangeExceed(val allowed: Int) : BatchDownloadValidationResult\n    data object URLInvalid : BatchDownloadValidationResult\n}\n\nsealed class WildcardLength {\n    data object Auto : WildcardLength()\n    data object Unspecified : WildcardLength()\n    data class Custom(val v: Int) : WildcardLength()\n}\n\ndata class WildcardString(\n    val string: String,\n    val range: IntRange,\n    val wildcardLength: WildcardLength,\n    val minimumAllowed: Int = range.last.toString().length,\n) : Iterable<String> {\n    private fun transformIndex(index: Int): String {\n        var str = index.toString()\n        if (wildcardLength is WildcardLength.Unspecified) {\n            return str\n        }\n        val length = when (wildcardLength) {\n            is WildcardLength.Custom -> wildcardLength.v.coerceAtLeast(minimumAllowed)\n            WildcardLength.Auto -> minimumAllowed\n            WildcardLength.Unspecified -> null\n        }\n        if (length != null) {\n            str = str.padStart(length, '0')\n        }\n        return str\n    }\n\n    fun get(index: Int): String {\n        return string.replace(\"*\", transformIndex(index))\n    }\n\n    fun first(): String {\n        return get(range.first)\n    }\n\n    fun last(): String {\n        return get(range.last)\n    }\n\n    fun size() = range.last - range.first + 1\n\n    override fun iterator(): Iterator<String> {\n        return range\n            .asSequence()\n            .map(::get)\n            .iterator()\n    }\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/category/CategoryComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.category\n\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.iconSource\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.osfileutil.FileUtils\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport java.io.File\n\nclass CategoryComponent(\n    ctx: ComponentContext,\n    val id: Long,\n    val close: () -> Unit,\n    private val submit: (Category) -> Unit,\n) : BaseComponent(ctx), KoinComponent {\n    private val appRepository: BaseAppRepository by inject()\n    val defaultDownloadLocation = appRepository.saveLocation\n    private val categoryManager: CategoryManager by inject()\n    private val iconResolver: IIconResolver by inject()\n\n    init {\n        if (id >= 0) {\n            loadCategoryData()\n        }\n    }\n\n    fun loadCategoryData() {\n        scope.launch {\n            val category = categoryManager.getCategoryById(id) ?: return@launch\n            setIcon(category.iconSource(iconResolver))\n            setName(category.name)\n            setTypesEnabled(category.acceptedFileTypes.isNotEmpty())\n            setTypes(category.acceptedFileTypes.joinToString(\" \"))\n            setUrlPatternsEnabled(category.acceptedUrlPatterns.isNotEmpty())\n            setUrlPatterns(category.acceptedUrlPatterns.joinToString(\" \"))\n            setPath(category.path)\n            setUsePath(category.usePath)\n        }\n    }\n\n    private val _icon = MutableStateFlow(null as IconSource?)\n    val icon = _icon.asStateFlow()\n    fun setIcon(iconSource: IconSource?) {\n        _icon.value = iconSource\n    }\n\n    private val _name = MutableStateFlow(\"\")\n    val name = _name.asStateFlow()\n    fun setName(name: String) {\n        _name.value = name\n    }\n\n    private val _typesEnabled = MutableStateFlow(false)\n    val typesEnabled = _typesEnabled.asStateFlow()\n    fun setTypesEnabled(value: Boolean) {\n        _typesEnabled.value = value\n    }\n\n    private val _types = MutableStateFlow(\"\")\n    val types = _types.asStateFlow()\n    fun setTypes(types: String) {\n        _types.value = types\n    }\n\n    private val _urlPatternsEnabled = MutableStateFlow(false)\n    val urlPatternsEnabled = _urlPatternsEnabled.asStateFlow()\n    fun setUrlPatternsEnabled(urlPatterns: Boolean) {\n        _urlPatternsEnabled.value = urlPatterns\n    }\n\n    private val _urlPatterns = MutableStateFlow(\"\")\n    val urlPatterns = _urlPatterns.asStateFlow()\n    fun setUrlPatterns(urlPatterns: String) {\n        _urlPatterns.value = urlPatterns\n    }\n\n    private val _path = MutableStateFlow(\"\")\n    val path = _path.asStateFlow()\n    fun setPath(path: String) {\n        _path.value = path\n    }\n\n    private val _usePath = MutableStateFlow(false)\n    val usePath = _usePath.asStateFlow()\n    fun setUsePath(usePath: Boolean) {\n        _usePath.value = usePath\n    }\n\n    val canSubmit = combineStateFlows(\n        icon,\n        name,\n        types,\n        path,\n        usePath,\n    ) { icon, name, types, path, usePath ->\n        val iconOk = icon != null\n        val nameOk = name.isNotBlank()\n        val pathOk = FileUtils.Companion.canWriteInThisFolder(path) || !usePath\n        iconOk && nameOk && pathOk\n    }\n    val isEditMode = id >= 0\n\n    fun submit() {\n        if (!canSubmit.value) {\n            return\n        }\n        val path = path.value\n        runCatching {\n            File(path).mkdirs()\n        }\n        submit(\n            Category(\n                id = id,\n                name = name.value,\n                acceptedFileTypes = if (typesEnabled.value) {\n                    types.value\n                        .split(\" \")\n                        .filterNot { it.isBlank() }\n                        .distinct()\n                } else {\n                    emptyList()\n                },\n                icon = icon\n                    .value!!\n                    .uri!!,\n                path = path,\n                usePath = usePath.value,\n                acceptedUrlPatterns = if (urlPatternsEnabled.value) {\n                    urlPatterns.value\n                        .split(\" \")\n                        .filterNot { it.isBlank() }\n                        .distinct()\n                } else {\n                    emptyList()\n                },\n                items = emptyList() // ignored!\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/checksum/BaseFileChecksumComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.checksum\n\nimport androidx.compose.runtime.Immutable\nimport arrow.core.Some\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport com.abdownloadmanager.shared.util.FileChecksumAlgorithm\nimport com.abdownloadmanager.shared.util.HashUtil\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.ContainsScreenState\nimport com.abdownloadmanager.shared.util.mvi.SupportsScreenState\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport ir.amirab.util.ifThen\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.properties.Delegates\n\nopen class BaseFileChecksumComponent(\n    ctx: ComponentContext,\n    val id: String,\n    val itemIds: List<Long>,\n    private val closeComponent: () -> Unit,\n    val downloadSystem: DownloadSystem\n) : BaseComponent(ctx),\n    ContainsScreenState<FileChecksumUiState> by SupportsScreenState(FileChecksumUiState.default()),\n    ContainsEffects<BaseFileChecksumComponent.Effects> by supportEffects() {\n\n    private var downloadItems: List<IDownloadItem> by Delegates.notNull()\n\n    private val isChecking = MutableStateFlow(false)\n    private val selectedDefaultAlgorithm: MutableStateFlow<FileChecksumAlgorithm> =\n        MutableStateFlow(FileChecksumAlgorithm.Companion.default())\n\n    fun onAlgorithmChange(algorithm: FileChecksumAlgorithm) {\n        this.selectedDefaultAlgorithm.update { algorithm }\n    }\n\n    fun isDefaultAlgorithmNeeded(): Boolean {\n        return state.value.items.any {\n            it.savedChecksum == null\n        }\n    }\n\n    init {\n        scope.launch {\n            load(itemIds)\n            setup()\n\n            if (!isDefaultAlgorithmNeeded()) {\n                // user don't need to manually set checksum algorithm\n                // start checking immediately\n                startCheck()\n            }\n        }\n        isChecking.onEach { isChecking ->\n            setState { fileChecksumUiState ->\n                fileChecksumUiState.copy(isChecking = isChecking)\n            }\n        }.launchIn(scope)\n        selectedDefaultAlgorithm.onEach { algorithm ->\n            setState { fileChecksumUiState ->\n                fileChecksumUiState.copy(\n                    // reset checksum algorithm\n                    items = fileChecksumUiState.items.map { itemWithChecksum ->\n                        itemWithChecksum.copy(\n                            algorithm = getChecksumAlgorithmForItem(itemWithChecksum.downloadItem)\n                        )\n                    },\n                    defaultAlgorithm = algorithm\n                )\n            }\n        }.launchIn(scope)\n    }\n\n    private fun setup() {\n        setState {\n            it.copy(\n                items = downloadItems.map { downloadItem ->\n                    val savedChecksum = FileChecksum.Companion.fromNullableString(downloadItem.fileChecksum)\n                    DownloadItemWithChecksum(\n                        downloadItem = downloadItem,\n                        checksumStatus = ChecksumStatus.Waiting,\n                        algorithm = savedChecksum?.algorithm ?: selectedDefaultAlgorithm.value.algorithm,\n                        savedChecksum = savedChecksum?.value,\n                        calculatedChecksum = null,\n                    )\n                },\n            )\n        }\n    }\n\n    private suspend fun load(items: List<Long>) {\n        downloadItems = items.mapNotNull {\n            downloadSystem.getDownloadItemById(it)\n        }\n    }\n\n    fun updateChecksum(\n        downloadId: Long,\n        fileChecksum: FileChecksum?,\n    ) {\n        scope.launch {\n            val newChecksumString = fileChecksum?.toString()\n            downloadSystem.downloadManager.updateDownloadItem(\n                id = downloadId,\n                updater = {\n                    it.fileChecksum = newChecksumString\n                },\n                downloadJobExtraConfig = null,\n            )\n            // update this class internal download items\n            downloadItems = downloadItems.map {\n                it.ifThen(it.id == downloadId) {\n                    copy(fileChecksum = Some(newChecksumString))\n                }\n            }\n            updateItem(downloadId) {\n                var modified: DownloadItemWithChecksum = it\n                // update the download item (in this component only)\n                modified = modified.copy(\n                    downloadItem = it.downloadItem.copy(\n                        fileChecksum = Some(newChecksumString)\n                    ),\n                    savedChecksum = fileChecksum?.value\n                )\n                // update hash compare\n                if (fileChecksum != null) {\n                    val algorithm = fileChecksum.algorithm\n                    if (it.algorithm != fileChecksum.algorithm) {\n                        // reset calculated hash if the previous algorithm is different from the new one!\n                        modified = modified.copy(\n                            algorithm = algorithm,\n                            calculatedChecksum = null,\n                            checksumStatus = ChecksumStatus.Waiting,\n                        )\n                    } else if (modified.calculatedChecksum != null) {\n                        // user previously started the check, and he calculated the hash\n                        // so we compare saved hash with calculated hash for him\n                        modified = modified.copy(\n                            algorithm = algorithm,\n                            checksumStatus = compareHashes(\n                                savedChecksum = fileChecksum,\n                                calculatedChecksum = FileChecksum(algorithm, modified.calculatedChecksum)\n                            )\n                        )\n                    }\n                } else {\n                    // we don't have saved checksum, so we don't know if its matches or not!\n                    if (it.checksumStatus is ChecksumStatus.Finished) {\n                        modified = modified.copy(\n                            checksumStatus = ChecksumStatus.Finished.Done,\n                        )\n                    }\n                }\n                modified\n            }\n        }\n    }\n\n    private suspend fun startCheck() {\n        // clean old statuses\n        setup()\n\n        isChecking.update { true }\n        try {\n            withContext(Dispatchers.IO) {\n                // some dude may change checksum when we are busy here\n                // so always use the latest download items object!\n                for (index in downloadItems.indices) {\n                    processItem(downloadItems[index])\n                }\n            }\n        } finally {\n            isChecking.update { false }\n        }\n    }\n\n    private fun processItem(item: IDownloadItem) {\n        val file = downloadSystem.getDownloadFile(item)\n        if (item.status != DownloadStatus.Completed) {\n            scope.launch {\n                updateItemStatus(item.id, ChecksumStatus.Error.DownloadNotFinished)\n            }\n            return\n        }\n        if (!file.isFile) {\n            scope.launch {\n                updateItemStatus(item.id, ChecksumStatus.Error.FileNotFound)\n            }\n            return\n        }\n        try {\n            val algorithm = getChecksumAlgorithmForItem(item)\n            val hash = HashUtil.fileHash(\n                algorithm = algorithm,\n                file = file,\n                onNewPercent = { percent ->\n                    scope.launch {\n                        updateItemStatus(item.id, ChecksumStatus.Checking(percent))\n                    }\n                }\n            )\n            val newStatus = compareHashes(\n                FileChecksum.Companion.fromNullableString(item.fileChecksum),\n                FileChecksum(algorithm, hash),\n            )\n            scope.launch {\n                updateItem(item.id) {\n                    it.copy(\n                        checksumStatus = newStatus,\n                        calculatedChecksum = hash,\n                    )\n                }\n            }\n        } catch (e: Exception) {\n            scope.launch {\n                updateItemStatus(item.id, ChecksumStatus.Error.Exception(e))\n            }\n        }\n    }\n\n    private fun compareHashes(\n        savedChecksum: FileChecksum?, calculatedChecksum: FileChecksum\n    ): ChecksumStatus.Finished {\n        return if (savedChecksum == null) {\n            ChecksumStatus.Finished.Done\n        } else {\n            if (savedChecksum == calculatedChecksum) {\n                ChecksumStatus.Finished.Matches\n            } else {\n                ChecksumStatus.Finished.NotMatches\n            }\n        }\n    }\n\n    private fun getChecksumAlgorithmForItem(downloadItem: IDownloadItem): String {\n        return downloadItem.fileChecksum?.let {\n            FileChecksum.Companion.fromString(it).algorithm\n        } ?: selectedDefaultAlgorithm.value.algorithm\n    }\n\n    private fun updateItem(id: Long, updater: (DownloadItemWithChecksum) -> DownloadItemWithChecksum) {\n        setState {\n            it.copy(\n                items = it.items.map { itemWithChecksum ->\n                    itemWithChecksum.ifThen(itemWithChecksum.downloadItem.id == id) {\n                        updater(itemWithChecksum)\n                    }\n                }\n            )\n        }\n    }\n\n    private fun updateItemStatus(id: Long, status: ChecksumStatus) {\n        updateItem(id) {\n            it.copy(checksumStatus = status)\n        }\n    }\n\n    fun onRequestClose() {\n        closeComponent()\n    }\n\n    fun onRequestStartCheck() {\n        scope.launch {\n            startCheck()\n        }\n    }\n\n    interface Config {\n        val itemIds: List<Long>\n    }\n\n    @Immutable\n    sealed interface Effects {\n        interface Platform : Effects\n        // no common effects\n    }\n\n}\n\n@Immutable\nsealed interface ChecksumStatus {\n    sealed interface Finished : ChecksumStatus {\n        data object Matches : Finished\n        data object NotMatches : Finished\n\n        // just finished there is no saved checksum to compare it\n        data object Done : Finished\n    }\n\n    data class Checking(val percent: Int) : ChecksumStatus\n    sealed interface Error : ChecksumStatus {\n        data object FileNotFound : Error\n        data object DownloadNotFinished : Error\n        data class Exception(val t: Throwable) : Error\n    }\n\n    data object Waiting : ChecksumStatus\n}\n\n@Immutable\ndata class DownloadItemWithChecksum(\n    val downloadItem: IDownloadItem,\n    val checksumStatus: ChecksumStatus,\n    val algorithm: String,\n    val savedChecksum: String?,\n    val calculatedChecksum: String?,\n) {\n    val isProcessing = checksumStatus is ChecksumStatus.Checking\n    val isError = checksumStatus is ChecksumStatus.Error\n}\n\n@Immutable\ndata class FileChecksumUiState(\n    val items: List<DownloadItemWithChecksum>,\n    val isChecking: Boolean,\n    val defaultAlgorithm: FileChecksumAlgorithm,\n) {\n\n    companion object {\n        fun default() = FileChecksumUiState(\n            items = emptyList(),\n            isChecking = false,\n            defaultAlgorithm = FileChecksumAlgorithm.default(),\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/credits/translators/LanguageTranslationInfo.kt",
    "content": "package com.abdownloadmanager.shared.pages.credits.translators\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\ndata class LanguageTranslationInfo(\n    val locale: String,// en,es_ES etc...\n    val englishName: String,//Persian etc...\n    val nativeName: String,//فارسی ...\n    val translators: List<Translator>,\n)\n\ntypealias TranslatorData = @Serializable Map<String, List<Translator>>\n\n@Serializable\ndata class Translator(\n    @SerialName(\"name\")\n    val name: String,\n    @SerialName(\"link\")\n    val link: String,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/editdownload/BaseEditDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.editdownload\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.downloaderinui.edit.DownloadConflictDetector\nimport com.abdownloadmanager.shared.downloaderinui.edit.EditDownloadInputs\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.DownloadJobExtraConfig\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.downloader.downloaditem.IDownloadItem\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nopen class BaseEditDownloadComponent(\n    ctx: ComponentContext,\n    private val downloaderInUiRegistry: DownloaderInUiRegistry,\n    val iconProvider: FileIconProvider,\n    val downloadSystem: DownloadSystem,\n    val onRequestClose: () -> Unit,\n    val downloadId: Long,\n    val acceptEdit: StateFlow<Boolean>,\n    private val onEdited: ((IDownloadItem) -> Unit, DownloadJobExtraConfig?) -> Unit,\n) : BaseComponent(ctx) {\n\n    val editDownloadUiChecker =\n        MutableStateFlow(null as EditDownloadInputs<IDownloadItem, IDownloadCredentials, *, *, *, *>?)\n\n    init {\n        scope.launch {\n            load(downloadId)\n        }\n    }\n\n    private var pendingCredential: IDownloadCredentials? = null\n    private val _credentialsImportedFromExternal = MutableStateFlow(false)\n    val credentialsImportedFromExternal = _credentialsImportedFromExternal.asStateFlow()\n    fun importCredential(credentials: IDownloadCredentials) {\n        editDownloadUiChecker.value?.let {\n            it.importCredentials(credentials)\n        } ?: run {\n            pendingCredential = credentials\n        }\n        _credentialsImportedFromExternal.value = true\n    }\n\n    private suspend fun load(id: Long) {\n        val downloadItem = downloadSystem.getDownloadItemById(id = id)\n        if (downloadItem == null) {\n            onRequestClose()\n            println(\"item with id $id not found\")\n            return\n        }\n        val downloader = downloaderInUiRegistry.getDownloaderOf(downloadItem)\n        if (downloader == null) {\n            onRequestClose()\n            println(\"downloader for id $id not found\")\n            return\n        }\n        val httpEditDownloadInputs = downloader.createEditDownloadInputs(\n            currentDownloadItem = MutableStateFlow(downloadItem),\n            editedDownloadItem = MutableStateFlow(downloadItem),\n            conflictDetector = DownloadConflictDetector(downloadSystem),\n            scope = scope,\n        )\n        editDownloadUiChecker.value = httpEditDownloadInputs\n        pendingCredential?.let { credentials ->\n            httpEditDownloadInputs.importCredentials(credentials)\n            pendingCredential = null\n        }\n    }\n\n\n    fun onRequestEdit() {\n        if (!acceptEdit.value) {\n            return\n        }\n        editDownloadUiChecker.value?.let { editDownloadUiChecker ->\n            onEdited(editDownloadUiChecker::applyEditedItemTo, editDownloadUiChecker.downloadJobConfig.value)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/enterurl/BaseEnterNewURLComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.enterurl\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.downloaderinui.TADownloaderInUI\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.StringUrlExtractor\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\n\nopen class BaseEnterNewURLComponent(\n    val ctx: ComponentContext,\n    val config: Config,\n    val downloaderInUiRegistry: DownloaderInUiRegistry,\n    private val onCloseRequest: () -> Unit,\n    private val onRequestFinished: (IDownloadCredentials) -> Unit,\n) : BaseComponent(ctx),\n    ContainsEffects<BaseEnterNewURLComponent.Effects> by supportEffects() {\n    private val _url: MutableStateFlow<String> = MutableStateFlow(\"\")\n    val url = _url.asStateFlow()\n\n    val bestDownloader = url.mapStateFlow {\n        downloaderInUiRegistry.bestMatchForThisLink(it)\n    }\n    private val _downloaderSelection = MutableStateFlow<DownloaderSelection>(\n        DownloaderSelection.Auto\n    )\n\n    val downloaderSelection = _downloaderSelection.asStateFlow()\n    fun selectDownloader(downloaderSelection: DownloaderSelection) {\n        _downloaderSelection.value = downloaderSelection\n    }\n\n\n    val downloaderToPickup: StateFlow<TADownloaderInUI?> = combineStateFlows(\n        bestDownloader,\n        downloaderSelection,\n    ) { bestDownloader, userSelection ->\n        when (userSelection) {\n            DownloaderSelection.Auto -> bestDownloader\n            is DownloaderSelection.Fixed -> userSelection.downloaderInUi\n        }\n    }\n    val canAdd = combineStateFlows(\n        downloaderToPickup,\n        url,\n    ) { downloader, url ->\n        url.isNotBlank() && downloader != null\n    }\n\n\n    fun setURL(url: String) {\n        _url.value = url\n    }\n\n    var firstTimeOpened = false\n    open val shouldFillWithClipboard = true\n    fun onPageOpen() {\n        if (!firstTimeOpened) {\n            if (shouldFillWithClipboard) {\n                scope.launch {\n                    if (fillLinkIfThereIsALinkInClipboard()) {\n                        sendEffect(Effects.LinkSelectAll)\n                    }\n                }\n            }\n            firstTimeOpened = true\n        }\n    }\n\n    private fun fillLinkIfThereIsALinkInClipboard(): Boolean {\n        val possibleLinks = ClipboardUtil.read() ?: return false\n        val downloadLinks = StringUrlExtractor.extract(possibleLinks)\n        if (downloadLinks.size == 1) {\n            setURL(downloadLinks.first())\n            return true\n        }\n        return false\n    }\n\n\n    fun close() {\n        scope.launch {\n            onCloseRequest()\n        }\n    }\n\n    fun newDownloadEntered() {\n        val downloader = downloaderToPickup.value ?: return\n        val link = url.value\n        onRequestFinished(\n            downloader.createMinimumCredentials(link)\n        )\n        onCloseRequest()\n    }\n\n    val possibleValues = buildList {\n        add(DownloaderSelection.Auto)\n        addAll(downloaderInUiRegistry.getAll().map {\n            DownloaderSelection.Fixed(it)\n        })\n    }\n\n    interface Config\n\n    sealed interface Effects {\n        data object LinkSelectAll : Effects\n        interface PlatformEffects : Effects\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/enterurl/DownloaderSelection.kt",
    "content": "package com.abdownloadmanager.shared.pages.enterurl\n\nimport com.abdownloadmanager.shared.downloaderinui.TADownloaderInUI\n\nsealed interface DownloaderSelection {\n    data object Auto : DownloaderSelection\n    data class Fixed(\n        val downloaderInUi: TADownloaderInUI,\n    ) : DownloaderSelection\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/AbstractDownloadActions.kt",
    "content": "package com.abdownloadmanager.shared.pages.home\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.action.createMoveToCategoryAction\nimport com.abdownloadmanager.shared.action.createMoveToQueueAction\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.util.ClipboardUtil\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.extractors.linkextractor.DownloadCredentialsFromCurl\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.isFinished\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.isNotNull\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.merge\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.launch\n\nabstract class AbstractDownloadActions(\n    private val scope: CoroutineScope,\n    downloadSystem: DownloadSystem,\n    downloadDialogManager: DownloadDialogManager,\n    editDownloadDialogManager: EditDownloadDialogManager,\n    fileChecksumDialogManager: FileChecksumDialogManager,\n    val selections: StateFlow<List<IDownloadItemState>>,\n    private val mainItem: StateFlow<Long?>,\n    private val queueManager: QueueManager,\n    private val categoryManager: CategoryManager,\n    private val openFile: (Long) -> Unit,\n    private val requestDelete: (List<Long>) -> Unit,\n) {\n    val defaultItem = combineStateFlows(\n        selections,\n        mainItem,\n    ) { selections, mainItem ->\n        selections.let {\n            it.find {\n                it.id == mainItem\n            } ?: it.firstOrNull()\n        }\n    }\n    val resumableSelections = selections.mapStateFlow {\n        it.filter { state ->\n            if (state is ProcessingDownloadItemState) {\n                state.canBeResumed()\n            } else {\n                false\n            }\n        }\n    }\n    val pausableSelections = selections.mapStateFlow {\n        it.filter { state ->\n            if (state is ProcessingDownloadItemState) {\n                state.canBePaused()\n            } else {\n                false\n            }\n        }\n    }\n    val openFileAction = simpleAction(\n        title = Res.string.open.asStringSource(),\n        icon = MyIcons.fileOpen,\n        checkEnable = defaultItem.mapStateFlow {\n            it?.statusOrFinished() is DownloadJobStatus.Finished\n        },\n        onActionPerformed = {\n            scope.launch {\n                val d = defaultItem.value ?: return@launch\n                openFile(d.id)\n            }\n        }\n    )\n\n    val deleteAction = simpleAction(\n        title = Res.string.delete.asStringSource(),\n        icon = MyIcons.remove,\n        checkEnable = selections.mapStateFlow { it.isNotEmpty() },\n        onActionPerformed = {\n            scope.launch {\n                requestDelete(selections.value.map { it.id })\n            }\n        },\n    )\n\n    val resumeAction = simpleAction(\n        title = Res.string.resume.asStringSource(),\n        icon = MyIcons.resume,\n        checkEnable = resumableSelections.mapStateFlow {\n            it.isNotEmpty()\n        },\n        onActionPerformed = {\n            scope.launch {\n                resumableSelections.value.forEach {\n                    runCatching {\n                        downloadSystem.userManualResume(it.id)\n                    }\n                }\n            }\n        }\n    )\n\n    val reDownloadAction = simpleAction(\n        Res.string.restart_download.asStringSource(),\n        MyIcons.refresh\n    ) {\n        scope.launch {\n            selections.value.forEach {\n                scope.launch {\n                    runCatching {\n                        downloadSystem.reset(it.id)\n                        downloadSystem.userManualResume(it.id)\n                    }\n                }\n            }\n        }\n    }\n\n    val pauseAction = simpleAction(\n        title = Res.string.pause.asStringSource(),\n        icon = MyIcons.pause,\n        checkEnable = pausableSelections.mapStateFlow {\n            it.isNotEmpty()\n        },\n        onActionPerformed = {\n            scope.launch {\n                pausableSelections.value.forEach {\n                    runCatching {\n                        downloadSystem.manualPause(it.id)\n                    }\n                }\n            }\n        }\n    )\n    val editDownloadAction = simpleAction(\n        title = Res.string.edit.asStringSource(),\n        icon = MyIcons.edit,\n        checkEnable = defaultItem.mapStateFlow { state ->\n            state ?: return@mapStateFlow false\n            // don't allow edit if download is active\n            if (state is ProcessingDownloadItemState) {\n                !state.canBePaused()\n            } else {\n                true\n            }\n        },\n        onActionPerformed = {\n            scope.launch {\n                val item = defaultItem.value ?: return@launch\n                editDownloadDialogManager.openEditDownloadDialog(item.id)\n            }\n        }\n    )\n\n    val copyDownloadLinkAction = simpleAction(\n        title = Res.string.copy_link.asStringSource(),\n        icon = MyIcons.copy,\n        checkEnable = selections.mapStateFlow { it.isNotEmpty() },\n        onActionPerformed = {\n            scope.launch {\n                ClipboardUtil.copy(\n                    selections.value.joinToString(System.lineSeparator()) { it.downloadLink }\n                )\n            }\n        }\n    )\n\n    val copyDownloadCredentialsAsCurlAction = simpleAction(\n        title = Res.string.copy_as_curl.asStringSource(),\n        icon = MyIcons.copy,\n        checkEnable = selections.mapStateFlow { it.isNotEmpty() },\n        onActionPerformed = {\n            scope.launch {\n                val credentialsList = selections.value\n                    .mapNotNull { downloadSystem.getDownloadItemById(it.id) }\n                    .filterIsInstance<HttpDownloadItem>()\n                    .map { HttpDownloadCredentials.from(it) }\n                ClipboardUtil.copy(DownloadCredentialsFromCurl.generateCurlCommands(credentialsList).joinToString(\"\\n\"))\n            }\n        }\n    )\n\n    val openDownloadDialogAction = simpleAction(\n        Res.string.show_properties.asStringSource(),\n        MyIcons.info,\n        checkEnable = defaultItem.mapStateFlow(::isNotNull)\n    ) {\n        defaultItem.value?.let { itemState ->\n            downloadDialogManager.openDownloadDialog(itemState.id)\n        }\n    }\n    protected val fileChecksumAction = simpleAction(\n        title = Res.string.file_checksum.asStringSource(), MyIcons.info,\n        checkEnable = selections.mapStateFlow { list ->\n            list.any { iiDownloadItemState ->\n                iiDownloadItemState.isFinished()\n            }\n        }\n    ) {\n        fileChecksumDialogManager.openFileChecksumPage(\n            selections.value.map { it.id }\n        )\n    }\n\n    protected val moveToQueueItems = MenuItem.SubMenu(\n        title = Res.string.move_to_queue.asStringSource(),\n        items = emptyList()\n    ).apply {\n        merge(\n            queueManager.queues,\n            selections\n        ).onEach {\n            val qs = queueManager.queues.value\n            val list = qs.map { queue ->\n                createMoveToQueueAction(scope, downloadSystem, queue, selections.value.map { it.id })\n            }\n            setItems(list)\n        }.launchIn(scope)\n    }\n    protected val moveToCategoryAction = MenuItem.SubMenu(\n        title = Res.string.move_to_category.asStringSource(),\n        items = emptyList()\n    ).apply {\n        merge(\n            categoryManager.categoriesFlow.mapStateFlow {\n                it.map(Category::id)\n            },\n            selections\n        ).onEach {\n            val categories = categoryManager.categoriesFlow.value\n            val list = categories.map { category ->\n                createMoveToCategoryAction(\n                    scope = scope,\n                    category = category,\n                    downloadSystem = downloadSystem,\n                    itemIds = selections.value.map { iDownloadItemState ->\n                        iDownloadItemState.id\n                    }\n                )\n            }\n            setItems(list)\n        }.launchIn(scope)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/BaseHomeComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.home\n\nimport androidx.compose.runtime.snapshotFlow\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.AddDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.CategoryDialogManager\nimport com.abdownloadmanager.shared.pagemanager.DownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EditDownloadDialogManager\nimport com.abdownloadmanager.shared.pagemanager.EnterNewURLDialogManager\nimport com.abdownloadmanager.shared.pagemanager.FileChecksumDialogManager\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.pagemanager.QueuePageManager\nimport com.abdownloadmanager.shared.pages.adddownload.AddDownloadCredentialsInUiProps\nimport com.abdownloadmanager.shared.pages.home.category.DefinedStatusCategories\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.shared.pages.home.queue.QueueActions\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryItemWithId\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.downloaditem.contexts.RemovedBy\nimport ir.amirab.downloader.downloaditem.contexts.User\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.queue.queueModelsFlow\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.coroutines.combine\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.osfileutil.FileUtils\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.filterIsInstance\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport java.io.File\n\nabstract class BaseHomeComponent(\n    componentContext: ComponentContext,\n    protected val downloadItemOpener: DownloadItemOpener,\n    protected val downloadDialogManager: DownloadDialogManager,\n    protected val editDownloadDialogManager: EditDownloadDialogManager,\n    protected val addDownloadDialogManager: AddDownloadDialogManager,\n    protected val fileChecksumDialogManager: FileChecksumDialogManager,\n    protected val queuePageManager: QueuePageManager,\n    protected val categoryDialogManager: CategoryDialogManager,\n    protected val notificationSender: NotificationSender,\n    protected val downloadSystem: DownloadSystem,\n    val categoryManager: CategoryManager,\n    val queueManager: QueueManager,\n    protected val defaultCategories: DefaultCategories,\n    val fileIconProvider: FileIconProvider,\n) : BaseComponent(componentContext),\n    ContainsEffects<BaseHomeComponent.Effects> by supportEffects() {\n    protected abstract val enterNewURLDialogManager: EnterNewURLDialogManager\n    val filterState = FilterState()\n\n    protected fun requestDelete(\n        downloadList: List<Long>,\n    ) {\n        if (downloadList.isEmpty()) {\n            // nothing to delete!\n            return\n        }\n        scope.launch {\n            val unfinished = downloadSystem.getUnfinishedDownloadIds()\n                .count {\n                    it in downloadList\n                }\n            val finished = downloadSystem.getFinishedDownloadIds()\n                .count {\n                    it in downloadList\n                }\n            sendEffect(\n                Effects.Common.DeleteItems(\n                    list = downloadList,\n                    unfinishedCount = unfinished,\n                    finishedCount = finished,\n                )\n            )\n        }\n    }\n\n    fun onConfirmDeleteCategory(promptState: CategoryDeletePromptState) {\n        scope.launch {\n            categoryManager.deleteCategory(promptState.category)\n        }\n    }\n\n    fun confirmDelete(promptState: DeletePromptState) {\n        scope.launch {\n            val selectionList = promptState.downloadList\n            for (id in selectionList) {\n                downloadSystem.removeDownload(\n                    id = id,\n                    alsoRemoveFile = promptState.alsoDeleteFile,\n                    context = RemovedBy(User),\n                )\n            }\n        }\n    }\n\n    fun onConfirmAutoCategorize() {\n        val categorizedItems = categoryManager.getCategories()\n            .flatMap { it.items }\n        val allDownloads = activeDownloadList.value + completedList.value\n        val unCategorizedItems = allDownloads.filterNot {\n            it.id in categorizedItems\n        }\n        categoryManager\n            .autoAddItemsToCategoriesBasedOnFileNames(\n                unCategorizedItems.map {\n                    CategoryItemWithId(\n                        id = it.id,\n                        fileName = it.name,\n                        url = it.downloadLink,\n                    )\n                }\n            )\n    }\n\n    fun onConfirmResetCategories() {\n        scope.launch {\n            categoryManager.reset()\n        }\n    }\n\n    fun moveItemsToCategory(category: Category, items: List<Long>) {\n        scope.launch {\n            categoryManager.addItemsToCategory(category.id, items)\n        }\n    }\n\n    fun reorderCategory(index: Int, delta: Int) {\n        scope.launch {\n            categoryManager.reorderCategory(index, delta)\n        }\n    }\n\n    fun moveItemsToQueue(queue: DownloadQueue, items: List<Long>) {\n        scope.launch {\n            queueManager.addToQueue(queue.id, items)\n        }\n    }\n\n\n    fun requestAddNewDownload(\n        link: List<AddDownloadCredentialsInUiProps>,\n    ) {\n        addDownloadDialogManager.openAddDownloadDialog(link)\n    }\n\n    private val _selectionList = MutableStateFlow<List<Long>>(emptyList())\n    val selectionList = _selectionList.asStateFlow()\n\n    fun clearSelection() {\n        _selectionList.update { emptyList() }\n    }\n\n    fun selectAll() {\n        newSelection(\n            ids = downloadList.value.map { it.id }\n        )\n    }\n\n    fun newSelection(\n        ids: List<Long>,\n    ) {\n        _selectionList.update { ids }\n    }\n\n    fun onItemSelectionChange(id: Long, checked: Boolean) {\n        _selectionList.update { lastSelection ->\n            if (checked) {\n                if (!lastSelection.contains(id)) {\n                    lastSelection + id\n                } else {\n                    lastSelection\n                }\n            } else {\n                lastSelection - id\n            }\n        }\n\n    }\n\n    fun onCategoryFilterChange(\n        statusCategoryFilter: DownloadStatusCategoryFilter,\n        typeCategoryFilter: Category?,\n    ) {\n        this.filterState.queueFilter = null\n        this.filterState.statusFilter = statusCategoryFilter\n        this.filterState.typeCategoryFilter = typeCategoryFilter\n    }\n\n    fun onQueueFilterChange(\n        queueModel: QueueModel\n    ) {\n        this.filterState.statusFilter = DefinedStatusCategories.All\n        this.filterState.typeCategoryFilter = null\n        this.filterState.queueFilter = queueModel\n    }\n\n\n    val activeDownloadCountFlow = downloadSystem.downloadMonitor.activeDownloadCount\n    val globalSpeedFlow = downloadSystem.downloadMonitor.activeDownloadListFlow.map {\n        it.sumOf { it.speed }\n    }\n\n\n    val activeDownloadList = downloadSystem.downloadMonitor.activeDownloadListFlow\n    val completedList = downloadSystem.downloadMonitor.completedDownloadListFlow\n\n    init {\n        categoryManager.categoriesFlow.onEach { categories ->\n            val currentCategory = filterState.typeCategoryFilter ?: return@onEach\n            filterState.typeCategoryFilter = categories.find {\n                it.id == currentCategory.id\n            }\n        }.launchIn(scope)\n        queueManager.queueModelsFlow().onEach { queueModels ->\n            val currentQueueModel = filterState.queueFilter ?: return@onEach\n            filterState.queueFilter = queueModels.find {\n                it.id == currentQueueModel.id\n            }\n        }.launchIn(scope)\n    }\n\n    val downloadList = combine(\n        snapshotFlow { filterState.textToSearch },\n        activeDownloadList,\n        completedList,\n        snapshotFlow { filterState.typeCategoryFilter },\n        snapshotFlow { filterState.statusFilter },\n        snapshotFlow { filterState.queueFilter },\n    ) { textToSearch, activeDownloads, completeDownloads, categoryFilter, statusFilter, queueFilter ->\n        val isSearching = textToSearch.isNotBlank()\n        val allowedList = categoryFilter?.items ?: queueFilter?.queueItems\n        (activeDownloads + completeDownloads)\n            .filter {\n                val statusAccepted = filterState.statusFilter.accept(it)\n                val itemIsInAllowedList = allowedList?.contains(it.id) ?: true\n                val searchAccepted = if (isSearching) {\n                    it.name.contains(filterState.textToSearch, ignoreCase = true)\n                } else true\n                itemIsInAllowedList && statusAccepted && searchAccepted\n            }\n            // when restart a completed download item there is a duplication in list\n            // so make sure to not pass bad data to download list table as it has item.id as key\n            .distinctBy { it.id }\n    }\n        .withResumedLifecycle()\n        .stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())\n\n\n    init {\n        downloadList.onEach { downloads ->\n            _selectionList.value = selectionList.value.filter { previouslySelectedItem ->\n                downloads.any { it.id == previouslySelectedItem }\n            }\n        }.launchIn(scope)\n\n        downloadSystem.downloadManager.listOfJobsEvents\n            .filterIsInstance<DownloadManagerEvents.OnJobAdded>()\n            // wait until download list in table is also updated\n            // it also prevents extra emits when multiple download added at the same time\n            .debounce(100)\n            .onEach {\n                sendEffect(Effects.Common.ScrollToDownloadItem(it.downloadItem.id))\n            }.launchIn(scope)\n\n    }\n\n\n    protected val selectionListItems = combineStateFlows(\n        selectionList,\n        downloadList,\n    ) { selectionList, downloadList ->\n        val ids = selectionList\n        ids.mapNotNull { id ->\n            downloadList.find {\n                it.id == id\n            }\n        }\n    }\n\n    fun openFileOrShowProperties(id: Long) {\n        scope.launch {\n            val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch\n            if (dItem.status != DownloadStatus.Completed) {\n                downloadDialogManager.openDownloadDialog(id)\n                return@launch\n            }\n            downloadItemOpener.openDownloadItem(dItem)\n        }\n    }\n\n    fun openFile(id: Long) {\n        scope.launch {\n            val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch\n            if (dItem.status != DownloadStatus.Completed) {\n                notificationSender.sendNotification(\n                    Res.string.open_file,\n                    Res.string.cant_open_file.asStringSource(),\n                    Res.string.not_finished.asStringSource(),\n                    NotificationType.Error,\n                )\n                return@launch\n            }\n            downloadItemOpener.openDownloadItem(dItem)\n        }\n    }\n\n    val queueActions = MutableStateFlow(null as QueueActions?)\n\n    fun showCategoryOptions(queue: DownloadQueue?) {\n        queueActions.value = QueueActions(\n            scope = scope,\n            queueManager = queueManager,\n            mainQueueModel = queue?.queueModel?.value,\n            requestDelete = { queueModel ->\n                scope.launch {\n                    downloadSystem.deleteQueue(queueModel.id)\n                }\n            },\n            requestEdit = { queueModel ->\n                runCatching { queueManager.getQueue(queueModel.id) }\n                    // it shouldn't be happened however I add this\n                    .getOrNull()?.let { q ->\n                        queuePageManager.openQueues(q.id)\n                    }\n            },\n            requestClearItems = {\n                scope.launch {\n                    runCatching {\n                        queueManager.clearQueue(it.id)\n                    }\n                }\n            },\n            onRequestNewQueue = {\n                queuePageManager.openNewQueueDialog()\n            }\n        )\n    }\n\n    fun closeQueueOptions() {\n        queueActions.value = null\n    }\n\n    val categoryActions = MutableStateFlow(null as CategoryActions?)\n\n    fun showCategoryOptions(categoryItem: Category?) {\n        categoryActions.value = CategoryActions(\n            scope = scope,\n            categoryManager = categoryManager,\n            defaultCategories = defaultCategories,\n            categoryItem = categoryItem,\n            openFolder = {\n                runCatching {\n                    it.getDownloadPath()?.let {\n                        FileUtils.Companion.openFolder(File(it))\n                    }\n                }\n            },\n            onRequestAddCategory = {\n                categoryDialogManager.openCategoryDialog(-1)\n            },\n            requestDelete = {\n                sendEffect(\n                    Effects.Common.DeleteCategory(it)\n                )\n            },\n            requestEdit = {\n                categoryDialogManager.openCategoryDialog(it.id)\n            },\n            onRequestCategorizeItems = {\n                sendEffect(Effects.Common.AutoCategorize)\n            },\n            onRequestResetToDefaults = {\n                sendEffect(Effects.Common.ResetCategoriesToDefault)\n            }\n        )\n    }\n\n    fun closeCategoryOptions() {\n        categoryActions.value = null\n    }\n\n    fun requestEnterNewURL() {\n        enterNewURLDialogManager.openEnterNewURLWindow()\n    }\n    sealed interface Effects {\n        interface PlatformEffects : Effects\n\n        sealed interface Common : Effects {\n            data class DeleteItems(\n                val list: List<Long>,\n                val finishedCount: Int,\n                val unfinishedCount: Int,\n            ) : Common\n\n            data class DeleteCategory(\n                val category: Category,\n            ) : Common\n\n            data object ResetCategoriesToDefault : Common\n            data object AutoCategorize : Common\n            data class ScrollToDownloadItem(\n                val downloadId: Long,\n                val skipIfVisible: Boolean = false,\n            ) : Common\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/CategoryActions.kt",
    "content": "package com.abdownloadmanager.shared.pages.home\n\nimport androidx.compose.runtime.Stable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.category.Category\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\n\n@Stable\nclass CategoryActions(\n    private val scope: CoroutineScope,\n    private val categoryManager: CategoryManager,\n    private val defaultCategories: DefaultCategories,\n\n    val categoryItem: Category?,\n\n    private val openFolder: (Category) -> Unit,\n    private val requestDelete: (Category) -> Unit,\n    private val requestEdit: (Category) -> Unit,\n\n    private val onRequestResetToDefaults: () -> Unit,\n    private val onRequestCategorizeItems: () -> Unit,\n    private val onRequestAddCategory: () -> Unit,\n) {\n    private val mainItemExists = MutableStateFlow(categoryItem != null)\n    private val canBeOpened = MutableStateFlow(categoryItem?.usePath ?: false)\n    private inline fun useItem(\n        block: (Category) -> Unit,\n    ) {\n        categoryItem?.let(block)\n    }\n\n    val openCategoryFolderAction = simpleAction(\n        title = Res.string.open_folder.asStringSource(),\n        icon = MyIcons.folderOpen,\n        checkEnable = canBeOpened,\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    openFolder(it)\n                }\n            }\n        }\n    )\n\n    val deleteAction = simpleAction(\n        title = Res.string.delete_category.asStringSource(),\n        icon = MyIcons.remove,\n        checkEnable = mainItemExists,\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    requestDelete(it)\n                }\n            }\n        },\n    )\n    val editAction = simpleAction(\n        title = Res.string.edit_category.asStringSource(),\n        icon = MyIcons.settings,\n        checkEnable = mainItemExists,\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    requestEdit(it)\n                }\n            }\n        },\n    )\n\n    val addCategoryAction = simpleAction(\n        title = Res.string.add_category.asStringSource(),\n        icon = MyIcons.add,\n        onActionPerformed = {\n            scope.launch {\n                onRequestAddCategory()\n            }\n        },\n    )\n    val categorizeItemsAction = simpleAction(\n        title = Res.string.auto_categorize_downloads.asStringSource(),\n        icon = MyIcons.refresh,\n        onActionPerformed = {\n            scope.launch {\n                onRequestCategorizeItems()\n            }\n        },\n    )\n    val resetToDefaultAction = simpleAction(\n        title = Res.string.restore_defaults.asStringSource(),\n        icon = MyIcons.undo,\n        checkEnable = categoryManager\n            .categoriesFlow\n            .mapStateFlow { !defaultCategories.isDefault(it) },\n        onActionPerformed = {\n            scope.launch {\n                onRequestResetToDefaults()\n            }\n        },\n    )\n\n    val menu: List<MenuItem> = buildMenu {\n        +editAction\n        +openCategoryFolderAction\n        +deleteAction\n        separator()\n        +addCategoryAction\n        separator()\n        +categorizeItemsAction\n        +resetToDefaultAction\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/FilterState.kt",
    "content": "package com.abdownloadmanager.shared.pages.home\n\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport com.abdownloadmanager.shared.pages.home.category.DefinedStatusCategories\nimport com.abdownloadmanager.shared.pages.home.category.DownloadStatusCategoryFilter\nimport com.abdownloadmanager.shared.util.category.Category\nimport ir.amirab.downloader.db.QueueModel\n\n@Stable\nclass FilterState {\n    var textToSearch by mutableStateOf(\"\")\n    var typeCategoryFilter by mutableStateOf(null as Category?)\n    var queueFilter by mutableStateOf(null as QueueModel?)\n    var statusFilter by mutableStateOf<DownloadStatusCategoryFilter>(DefinedStatusCategories.All)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/PromptStates.kt",
    "content": "package com.abdownloadmanager.shared.pages.home\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport com.abdownloadmanager.shared.util.category.Category\nimport ir.amirab.util.compose.StringSource\n\n@Stable\nclass DeletePromptState(\n    val downloadList: List<Long>,\n    val finishedCount: Int,\n    val unfinishedCount: Int,\n) {\n    val hasFinishedDownloads = finishedCount > 0\n    var hasUnfinishedDownloads = unfinishedCount > 0\n    var alsoDeleteFile by mutableStateOf(false)\n\n    fun hasBothFinishedAndUnfinished(): Boolean {\n        return hasFinishedDownloads && hasUnfinishedDownloads\n    }\n}\n\n@Immutable\ndata class CategoryDeletePromptState(\n    val category: Category,\n)\n\n@Immutable\ndata class ConfirmPromptState(\n    val title: StringSource,\n    val description: StringSource,\n    val onConfirm: () -> Unit,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/category/DefinedStatusCategories.kt",
    "content": "package com.abdownloadmanager.shared.pages.home.category\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.util.compose.asStringSource\n\nobject DefinedStatusCategories {\n    fun values() = listOf(All, Finished, Unfinished)\n\n\n    val All = object : DownloadStatusCategoryFilter(\n        Res.string.all.asStringSource(),\n        MyIcons.folder,\n    ) {\n        override fun accept(iDownloadStatus: IDownloadItemState): Boolean = true\n    }\n    val Finished = DownloadStatusCategoryFilterByList(\n        Res.string.finished.asStringSource(),\n        MyIcons.folder,\n        listOf(DownloadStatus.Completed)\n    )\n    val Unfinished = DownloadStatusCategoryFilterByList(\n        Res.string.Unfinished.asStringSource(),\n        MyIcons.folder,\n        listOf(\n            DownloadStatus.Error,\n            DownloadStatus.Added,\n            DownloadStatus.Paused,\n            DownloadStatus.Downloading,\n        )\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/category/DownloadStatusCategoryFilter.kt",
    "content": "package com.abdownloadmanager.shared.pages.home.category\n\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\nabstract class DownloadStatusCategoryFilter(\n    val name: StringSource,\n    val icon: IconSource,\n) {\n    abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/category/DownloadStatusCategoryFilterByList.kt",
    "content": "package com.abdownloadmanager.shared.pages.home.category\n\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\nclass DownloadStatusCategoryFilterByList(\n    name: StringSource,\n    icon: IconSource,\n    val acceptedStatus: List<DownloadStatus>,\n) : DownloadStatusCategoryFilter(name, icon) {\n    override fun accept(iDownloadStatus: IDownloadItemState): Boolean {\n        return iDownloadStatus\n            .statusOrFinished()\n            .asDownloadStatus() in acceptedStatus\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/home/queue/QueueActions.kt",
    "content": "package com.abdownloadmanager.shared.pages.home.queue\n\nimport androidx.compose.runtime.Stable\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.downloader.db.QueueModel\nimport ir.amirab.downloader.queue.DefaultQueueInfo\nimport ir.amirab.downloader.queue.DownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.action.simpleAction\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\n\n@Stable\nclass QueueActions(\n    private val scope: CoroutineScope,\n    private val queueManager: QueueManager,\n    val mainQueueModel: QueueModel?,\n    private val requestDelete: (QueueModel) -> Unit,\n    private val requestEdit: (QueueModel) -> Unit,\n    private val requestClearItems: (QueueModel) -> Unit,\n    private val onRequestNewQueue: () -> Unit,\n) {\n    private val mainItemExists = MutableStateFlow(mainQueueModel != null)\n\n    fun downloadQueueOrNull(): DownloadQueue? {\n        val qId = mainQueueModel?.id ?: return null\n        return runCatching {\n            queueManager.getQueue(qId)\n        }.getOrNull()\n    }\n\n    private inline fun useItem(\n        block: (QueueModel) -> Unit,\n    ) {\n        mainQueueModel?.let(block)\n    }\n\n    val deleteAction = simpleAction(\n        title = Res.string.delete.asStringSource(),\n        icon = MyIcons.remove,\n        checkEnable = MutableStateFlow(run {\n            val item = mainQueueModel ?: return@run false\n            item.id != DefaultQueueInfo.ID\n        }),\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    requestDelete(it)\n                }\n            }\n        },\n    )\n    val editAction = simpleAction(\n        title = Res.string.edit.asStringSource(),\n        icon = MyIcons.settings,\n        checkEnable = mainItemExists,\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    requestEdit(it)\n                }\n            }\n        },\n    )\n    val clearItems = simpleAction(\n        title = Res.string.clear_queue_items.asStringSource(),\n        icon = MyIcons.clear,\n        checkEnable = mainItemExists,\n        onActionPerformed = {\n            scope.launch {\n                useItem {\n                    requestClearItems(it)\n                }\n            }\n        },\n    )\n\n    val addQueueAction = simpleAction(\n        title = Res.string.add_new_queue.asStringSource(),\n        icon = MyIcons.add,\n        onActionPerformed = {\n            scope.launch {\n                onRequestNewQueue()\n            }\n        },\n    )\n\n    val start = simpleAction(\n        title = Res.string.start_queue.asStringSource(),\n        icon = MyIcons.queueStart,\n        checkEnable = run {\n            downloadQueueOrNull()?.activeFlow?.mapStateFlow { !it }\n                ?: MutableStateFlow(false)\n        },\n        onActionPerformed = {\n            scope.launch {\n                downloadQueueOrNull()?.start()\n            }\n        },\n    )\n    val stop = simpleAction(\n        title = Res.string.stop_queue.asStringSource(),\n        icon = MyIcons.queueStop,\n        checkEnable = run {\n            downloadQueueOrNull()?.activeFlow\n                ?: MutableStateFlow(false)\n        },\n        onActionPerformed = {\n            scope.launch {\n                downloadQueueOrNull()?.stop()\n            }\n        },\n    )\n\n    val menu: List<MenuItem> = buildMenu {\n        +start\n        +stop\n        separator()\n        +editAction\n        +deleteAction\n        +clearItems\n        separator()\n        +addQueueAction\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/perhostsettings/PerHostSettingsComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.perhostsettings\n\nimport arrow.core.prependTo\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsManager\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport ir.amirab.util.flow.onEachLatest\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport java.util.UUID\n\ndata class PerHostSettingsItemWithId(\n    val perHostSettingsItem: PerHostSettingsItem,\n    val id: String = UUID.randomUUID().toString(),\n)\n\ndata class PerHostSettingsConfigurableWithId(\n    val configurableGroups: List<ConfigurableGroup>,\n    val id: String,\n)\n\nabstract class BasePerHostSettingsComponent(\n    ctx: ComponentContext,\n    private val perHostSettingsManager: PerHostSettingsManager,\n    private val appRepository: BaseAppRepository,\n    private val appScope: CoroutineScope,\n    private val closeRequested: () -> Unit,\n) : BaseComponent(ctx),\n    ContainsEffects<BasePerHostSettingsComponent.Effects> by supportEffects() {\n    val editedPerHostSettings: MutableStateFlow<List<PerHostSettingsItemWithId>>\n    val savedPerHostSettings: MutableStateFlow<List<PerHostSettingsItemWithId>>\n\n    init {\n        val data = getStorageDataWithUnitqueIds()\n        editedPerHostSettings = MutableStateFlow(data)\n        savedPerHostSettings = MutableStateFlow(data)\n    }\n\n    val selectedId = MutableStateFlow(null as String?)\n    val canSave = combineStateFlows(\n        savedPerHostSettings, editedPerHostSettings\n    ) { a, b ->\n        a != b\n    }\n\n    fun onHostSelected(host: String) {\n        selectedId.value = editedPerHostSettings.value.find {\n            it.perHostSettingsItem.host == host\n        }?.id\n    }\n\n    fun onIdSelected(id: String?) {\n        selectedId.value = id\n    }\n\n    fun load() {\n        val data = getStorageDataWithUnitqueIds()\n        editedPerHostSettings.value = data\n        savedPerHostSettings.value = data\n    }\n\n    private fun getStorageDataWithUnitqueIds(): List<PerHostSettingsItemWithId> {\n        return perHostSettingsManager.getStorageData().map {\n            PerHostSettingsItemWithId(perHostSettingsItem = it)\n        }\n    }\n\n    fun save() {\n        appScope.launch {\n            perHostSettingsManager.setSettingsData(editedPerHostSettings.value.map {\n                it.perHostSettingsItem\n            })\n        }\n    }\n\n    val selectedItemConfigurableList: MutableStateFlow<PerHostSettingsConfigurableWithId?> = MutableStateFlow(null)\n\n    init {\n        selectedId.onEachLatest { selectedId ->\n            coroutineScope {\n                selectedItemConfigurableList.value = run {\n                    if (selectedId != null) {\n                        val configWithId = editedPerHostSettings.value\n                            .find { it.id == selectedId } ?: return@run null\n                        val state = MutableStateFlow(configWithId.perHostSettingsItem)\n                        launch {\n                            performUpdatesFromStateFlow(state)\n                        }\n                        PerHostSettingsConfigurableWithId(\n                            id = selectedId,\n                            configurableGroups = createConfigurableGroupForItem(state)\n                        )\n                    } else {\n                        null\n                    }\n                }\n            }\n        }.launchIn(scope)\n    }\n\n    private suspend fun performUpdatesFromStateFlow(\n        state: MutableStateFlow<PerHostSettingsItem>\n    ) {\n        state.collect { newUpdate ->\n            editedPerHostSettings.value = editedPerHostSettings.value.map {\n                if (it.id == selectedId.value) {\n                    it.copy(perHostSettingsItem = newUpdate)\n                } else {\n                    it\n                }\n            }\n        }\n    }\n\n    private fun createConfigurableGroupForItem(\n        state: MutableStateFlow<PerHostSettingsItem>\n    ): List<ConfigurableGroup> {\n        return listOf(\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    StringConfigurable(\n                        title = Res.string.settings_per_host_settings_host.asStringSource(),\n                        description = Res.string.settings_per_host_settings_host_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.host\n                            },\n                            unMap = {\n                                copy(host = it)\n                            }\n                        ),\n                        describe = {\n                            \"\".asStringSource()\n                        }\n                    ),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    SpeedLimitConfigurable(\n                        title = Res.string.download_item_settings_speed_limit.asStringSource(),\n                        description = Res.string.download_item_settings_speed_limit_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.speedLimit ?: 0\n                            },\n                            unMap = {\n                                copy(speedLimit = it.takeIf { it != 0L })\n                            }\n                        ),\n                        describe = {\n                            if (it == 0L) Res.string.unlimited.asStringSource()\n                            else convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()\n                        }\n                    ),\n                    IntConfigurable(\n                        title = Res.string.download_item_settings_thread_count.asStringSource(),\n                        description = Res.string.download_item_settings_thread_count_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.threadCount ?: 0\n                            },\n                            unMap = {\n                                copy(threadCount = it.takeIf { it != 0 })\n                            }\n                        ),\n                        range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n                        describe = {\n                            if (it == 0) Res.string.use_global_settings.asStringSource()\n                            else Res.string.download_item_settings_thread_count_describe\n                                .asStringSourceWithARgs(\n                                    Res.string.download_item_settings_thread_count_describe_createArgs(\n                                        count = it.toString()\n                                    )\n                                )\n                        }\n                    ),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    StringConfigurable(\n                        title = Res.string.username.asStringSource(),\n                        description = Res.string.download_item_settings_username_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.username.orEmpty()\n                            },\n                            unMap = {\n                                copy(username = it.takeIf { it.isNotBlank() })\n                            }\n                        ),\n                        describe = {\n                            \"\".asStringSource()\n                        }\n                    ),\n                    StringConfigurable(\n                        title = Res.string.password.asStringSource(),\n                        description = Res.string.download_item_settings_password_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.password.orEmpty()\n                            },\n                            unMap = {\n                                copy(password = it.takeIf { it.isNotBlank() })\n                            }\n                        ),\n                        describe = {\n                            \"\".asStringSource()\n                        }\n                    ),\n                )\n            ),\n            ConfigurableGroup(\n                nestedConfigurable = listOf(\n                    StringConfigurable(\n                        title = Res.string.settings_default_user_agent.asStringSource(),\n                        description = Res.string.settings_default_user_agent_description.asStringSource(),\n                        backedBy = state.mapTwoWayStateFlow(\n                            map = {\n                                it.userAgent.orEmpty()\n                            },\n                            unMap = {\n                                copy(userAgent = it.takeIf { it.isNotBlank() })\n                            }\n                        ),\n                        describe = {\n                            \"\".asStringSource()\n                        }\n                    ),\n                )\n            )\n        )\n    }\n\n\n    fun onRequestAddNewHostSettingsItem() {\n        val perHostSettingsItemWithId = PerHostSettingsItemWithId(\n            PerHostSettingsItem(\"\")\n        )\n        editedPerHostSettings.update {\n            perHostSettingsItemWithId.prependTo(it)\n        }\n        selectedId.value = perHostSettingsItemWithId.id\n    }\n\n    fun onRequestDeleteConfig(id: String) {\n        val index = editedPerHostSettings.value.indexOfFirst {\n            it.id == id\n        }\n        editedPerHostSettings.update {\n            it.filterNot { item ->\n                item.id == id\n            }\n        }\n        selectedId.update { selectedId ->\n            if (selectedId == id) {\n                val editedConfigs = this.editedPerHostSettings.value\n                runCatching {\n                    index.coerceIn(editedConfigs.indices)\n                }.getOrNull()?.let {\n                    editedConfigs.getOrNull(it)?.id\n                }\n            } else {\n                selectedId\n            }\n        }\n    }\n\n    fun close() {\n        closeRequested()\n    }\n\n    fun saveAndClose() {\n        save()\n        close()\n    }\n\n    interface Config {\n        val openedHost: String?\n    }\n\n    sealed interface Effects {\n        interface Platform : Effects\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/updater/RenderUpdateNotifications.kt",
    "content": "package com.abdownloadmanager.shared.pages.updater\n\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 com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.NotificationType\nimport com.abdownloadmanager.shared.ui.widget.ShowNotification\nimport com.abdownloadmanager.shared.util.mvi.HandleEffects\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.StringSource.*\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n@Composable\nfun RenderUpdateNotifications(updateComponent: UpdateComponent) {\n    var message by remember { mutableStateOf(null as StringSource?) }\n    var notificationType by remember { mutableStateOf(null as NotificationType?) }\n    val scope = rememberCoroutineScope()\n    var clearMessageInJob by remember {\n        mutableStateOf(null as Job?)\n    }\n\n    fun clearMessageAfter(delay: Long) {\n        clearMessageInJob?.cancel()\n        clearMessageInJob = scope.launch {\n            delay(delay)\n            message = null\n        }\n    }\n    HandleEffects(updateComponent) {\n        when (it) {\n            UpdateComponent.Effects.CheckingForUpdate -> {\n                message = Res.string.update_checking_for_update.asStringSource()\n                notificationType = NotificationType.Loading(null)\n            }\n\n            is UpdateComponent.Effects.Error -> {\n                clearMessageAfter(3000)\n                message = CombinedStringSource(\n                    listOf(\n                        Res.string.update_check_error.asStringSource(),\n                        it.throwable.localizedMessage.orEmpty().asStringSource(),\n                    ),\n                    \"\\n\",\n                )\n                it.throwable.printStackTrace()\n                notificationType = NotificationType.Error\n            }\n\n            UpdateComponent.Effects.NoUpdate -> {\n                clearMessageAfter(3000)\n                message = Res.string.update_no_update.asStringSource()\n                notificationType = NotificationType.Info\n            }\n\n            UpdateComponent.Effects.NewUpdate -> {\n                message = null\n                notificationType = null\n            }\n        }\n    }\n\n    message?.let { message ->\n        ShowNotification(\n            title = Res.string.update_updater.asStringSource(),\n            description = message,\n            type = notificationType ?: NotificationType.Info,\n            tag = \"Updater\"\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/pages/updater/UpdateComponent.kt",
    "content": "package com.abdownloadmanager.shared.pages.updater\n\nimport com.abdownloadmanager.UpdateCheckStatus\nimport com.abdownloadmanager.shared.util.AppVersion\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.UpdateManager\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.NotificationSender\nimport com.abdownloadmanager.shared.ui.widget.MessageDialogType\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport org.koin.core.component.KoinComponent\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass UpdateComponent(\n    ctx: ComponentContext,\n    private val notificationSender: NotificationSender,\n    private val updateManager: UpdateManager,\n) : BaseComponent(ctx),\n    ContainsEffects<UpdateComponent.Effects> by supportEffects(),\n    KoinComponent {\n\n    fun isUpdateSupported(): Boolean {\n        return updateManager.isUpdateSupported()\n    }\n\n    val currentVersion = AppVersion.get()\n    val showNewUpdate = MutableStateFlow(false)\n    val newVersionData = updateManager.newVersionData\n    private var updateApplierJob: Job? = null\n\n    val updateCheckStatus = updateManager.updateCheckStatus\n\n    fun performUpdate() {\n        updateApplierJob?.cancel()\n        updateApplierJob = scope.launch {\n            try {\n                updateManager.update()\n            } catch (e: Exception) {\n                showMessage(e)\n            }\n        }\n    }\n\n    private fun showMessage(e: Exception) {\n        e.printStackTrace()\n        notificationSender.sendDialogNotification(\n            Res.string.update_error.asStringSource(),\n            e.localizedMessage.orEmpty().asStringSource(),\n            type = MessageDialogType.Error,\n        )\n    }\n\n    fun showNewUpdate() {\n        showNewUpdate.update { true }\n    }\n\n    fun requestCheckForUpdate() {\n        scope.launch {\n            updateManager.checkForUpdate()\n        }\n    }\n\n    fun requestClose() {\n        showNewUpdate.update { false }\n    }\n\n    init {\n        updateCheckStatus\n            .drop(1) // state flow\n            .onEach {\n                when (it) {\n                    UpdateCheckStatus.Checking -> {\n                        sendEffect(Effects.CheckingForUpdate)\n                    }\n\n                    is UpdateCheckStatus.Error -> {\n                        sendEffect(Effects.Error(it.e))\n                    }\n\n                    UpdateCheckStatus.NewUpdate -> {\n                        sendEffect(Effects.NewUpdate)\n                        showNewUpdate()\n                    }\n\n                    UpdateCheckStatus.NoUpdate -> {\n                        sendEffect(Effects.NoUpdate)\n                    }\n\n                    UpdateCheckStatus.IDLE -> {\n\n                    }\n                }\n            }.launchIn(scope)\n    }\n\n    sealed interface Effects {\n        data object CheckingForUpdate : Effects\n        data object NoUpdate : Effects\n        data object NewUpdate : Effects\n        data class Error(val throwable: Throwable) : Effects\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/repository/BaseAppRepository.kt",
    "content": "package com.abdownloadmanager.shared.repository\n\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.SupportedSizeUnits\nimport com.abdownloadmanager.shared.util.AutoStartManager\nimport com.abdownloadmanager.shared.util.SizeAndSpeedUnitProvider\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.autoremove.RemovedDownloadsFromDiskTracker\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.DownloadSettings\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.util.datasize.ConvertSizeConfig\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.flow.withPrevious\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nopen class BaseAppRepository(\n    protected val scope: CoroutineScope,\n    protected val appSettings: BaseAppSettingsStorage,\n    protected val proxyManager: ProxyManager,\n    protected val downloadSystem: DownloadSystem,\n    protected val downloadSettings: DownloadSettings,\n    protected val removedDownloadsFromDiskTracker: RemovedDownloadsFromDiskTracker,\n    protected val categoryManager: CategoryManager,\n) : SizeAndSpeedUnitProvider {\n    val theme = appSettings.theme\n    val uiScale = appSettings.uiScale\n    private val downloadManager: DownloadManager = downloadSystem.downloadManager\n    private val downloadMonitor: IDownloadMonitor = downloadSystem.downloadMonitor\n\n    val maxConcurrentDownloads = appSettings.maxConcurrentDownloads\n    val speedLimiter = appSettings.speedLimit\n    val threadCount = appSettings.threadCount\n    val dynamicPartCreation = appSettings.dynamicPartCreation\n    val useServerLastModifiedTime = appSettings.useServerLastModifiedTime\n    val appendExtensionToIncompleteDownloads = appSettings.appendExtensionToIncompleteDownloads\n    val useSparseFileAllocation = appSettings.useSparseFileAllocation\n    val maxDownloadRetryCount = appSettings.maxDownloadRetryCount\n    val useAverageSpeed = appSettings.useAverageSpeed\n    val saveLocation = appSettings.defaultDownloadFolder\n    val integrationEnabled = appSettings.browserIntegrationEnabled\n    val integrationPort = appSettings.browserIntegrationPort\n    val trackDeletedFilesOnDisk = appSettings.trackDeletedFilesOnDisk\n\n    override val sizeUnit = appSettings.sizeUnit.mapStateFlow {\n        it.toConfig()\n    }\n    override val speedUnit = appSettings.speedUnit.mapStateFlow {\n        it.toConfig()\n    }\n\n\n    fun setSizeUnit(sizeUnit: ConvertSizeConfig) {\n        SupportedSizeUnits.Companion.fromConfig(sizeUnit)?.let {\n            appSettings.sizeUnit.value = it\n        }\n    }\n\n    fun setSpeedUnit(speedUnit: ConvertSizeConfig) {\n        SupportedSizeUnits.Companion.fromConfig(speedUnit)?.let {\n            appSettings.speedUnit.value = it\n        }\n    }\n\n    fun boot() {\n        updateDownloadSettings()\n    }\n\n    private fun updateDownloadSettings() {\n        downloadSettings.defaultThreadCount = threadCount.value\n        downloadSettings.dynamicPartCreationMode = dynamicPartCreation.value\n        downloadSettings.useServerLastModifiedTime = useServerLastModifiedTime.value\n        downloadSettings.appendExtensionToIncompleteDownloads = appendExtensionToIncompleteDownloads.value\n        downloadSettings.useSparseFileAllocation = useSparseFileAllocation.value\n        downloadSettings.maxDownloadRetryCount = maxDownloadRetryCount.value\n        downloadSettings.globalSpeedLimit = speedLimiter.value\n    }\n\n    init {\n        saveLocation\n            .debounce(500)\n            .withPrevious()\n            .onEach { (oldDownloadFolder, newDownloadFolder) ->\n                if (oldDownloadFolder == null) {\n                    return@onEach\n                }\n                categoryManager.updateCategoryFoldersBasedOnDefaultDownloadFolder(\n                    previousDownloadFolder = oldDownloadFolder,\n                    currentDownloadFolder = newDownloadFolder,\n                )\n            }.launchIn(scope)\n        //maybe its better to move this to another place\n        appSettings.autoStartOnBoot\n            .debounce(500)\n            .onEach { enabled ->\n                AutoStartManager.startOnBoot(enabled)\n            }.launchIn(scope)\n        speedLimiter\n            .debounce(500)\n            .onEach {\n                downloadSettings.globalSpeedLimit = it\n                downloadManager.limitGlobalSpeed(it)\n            }.launchIn(scope)\n        useAverageSpeed\n            .debounce(500)\n            .onEach {\n                downloadMonitor.useAverageSpeed = it\n            }.launchIn(scope)\n        threadCount\n            .debounce(500)\n            .onEach {\n                downloadSettings.defaultThreadCount = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        dynamicPartCreation\n            .debounce(500)\n            .onEach {\n                downloadSettings.dynamicPartCreationMode = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        useServerLastModifiedTime\n            .debounce(500)\n            .onEach {\n                downloadSettings.useServerLastModifiedTime = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        appendExtensionToIncompleteDownloads\n            .debounce(500)\n            .onEach {\n                downloadSettings.appendExtensionToIncompleteDownloads = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        useSparseFileAllocation\n            .debounce(500)\n            .onEach {\n                downloadSettings.useSparseFileAllocation = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        maxDownloadRetryCount\n            .debounce(500)\n            .onEach {\n                downloadSettings.maxDownloadRetryCount = it\n                downloadManager.reloadSetting()\n            }.launchIn(scope)\n        trackDeletedFilesOnDisk\n            .debounce(500)\n            .onEach { enabled ->\n                if (enabled) {\n                    removedDownloadsFromDiskTracker.removeDownloadsThatFilesAreMissing()\n                    removedDownloadsFromDiskTracker.start()\n                } else {\n                    removedDownloadsFromDiskTracker.stop()\n                }\n            }.launchIn(scope)\n        maxConcurrentDownloads\n            .debounce(500)\n            .onEach {\n                downloadSystem.manualDownloadQueue.setMaxConcurrent(it)\n            }.launchIn(scope)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/settings/BaseSettingsComponent.kt",
    "content": "package com.abdownloadmanager.shared.settings\n\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableGroup\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport kotlinx.coroutines.flow.StateFlow\n\nabstract class BaseSettingsComponent(\n    context: ComponentContext\n) : BaseComponent(\n    context\n),\n    ContainsEffects<BaseSettingsComponent.Effects> by supportEffects() {\n    abstract val configurables: StateFlow<List<ConfigurableGroup>>\n\n    sealed interface Effects {\n        interface Platform : Effects\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/settings/CommonSettings.kt",
    "content": "package com.abdownloadmanager.shared.settings\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.pagemanager.PerHostSettingsPageManager\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.EnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.FolderConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.NavigatableConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ThemeConfigurable\nimport com.abdownloadmanager.shared.ui.theme.ThemeManager\nimport com.abdownloadmanager.shared.util.MaximumDownloadRetriesLimitation\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.proxy.ProxyManager\nimport com.abdownloadmanager.shared.util.proxy.ProxyMode\nimport com.abdownloadmanager.shared.util.ui.theme.DEFAULT_UI_SCALE\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.compose.combineStringSources\nimport ir.amirab.util.compose.localizationmanager.LanguageInfo\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.datasize.CommonSizeConvertConfigs\nimport ir.amirab.util.datasize.ConvertSizeConfig\nimport ir.amirab.util.datasize.SizeFactors\nimport ir.amirab.util.datasize.SizeUnit\nimport ir.amirab.util.flow.createMutableStateFlowFromStateFlow\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.osfileutil.FileUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlin.math.roundToInt\n\nobject CommonSettings {\n\n    fun threadCountConfig(appRepository: BaseAppRepository): IntConfigurable {\n        return IntConfigurable(\n            title = Res.string.settings_download_thread_count.asStringSource(),\n            description = Res.string.settings_download_thread_count_description.asStringSource(),\n            backedBy = appRepository.threadCount,\n            range = 1..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n            renderMode = IntConfigurable.RenderMode.TextField,\n            describe = {\n                buildList {\n                    add(\n                        Res.string.settings_download_thread_count_describe\n                            .asStringSourceWithARgs(\n                                Res.string.settings_download_thread_count_describe_createArgs(\n                                    count = it.toString()\n                                )\n                            )\n                    )\n                    if (it > ThreadCountLimitation.MAX_NORMAL_VALUE) {\n                        add(\n                            Res.string.settings_download_thread_count_with_large_value_describe.asStringSource()\n                        )\n                    }\n                }.combineStringSources(\"\\n\")\n            },\n        )\n    }\n    fun maxConcurrentDownloads(appRepository: BaseAppRepository): IntConfigurable {\n        return IntConfigurable(\n            title = Res.string.settings_download_max_concurrent_downloads.asStringSource(),\n            description = Res.string.settings_download_max_concurrent_downloads_description.asStringSource(),\n            backedBy = appRepository.maxConcurrentDownloads,\n            range = 0..Int.MAX_VALUE,\n            renderMode = IntConfigurable.RenderMode.TextField,\n            describe = {\n                if (it == 0) {\n                    Res.string.unlimited.asStringSource()\n                } else {\n                    \"$it\".asStringSource()\n                }\n            },\n        )\n    }\n\n    fun maxDownloadRetryCount(appRepository: BaseAppRepository): IntConfigurable {\n        return IntConfigurable(\n            title = Res.string.settings_download_max_retries_count.asStringSource(),\n            description = Res.string.settings_download_max_retries_count_description.asStringSource(),\n            backedBy = appRepository.maxDownloadRetryCount,\n            range = 0..MaximumDownloadRetriesLimitation.MAX_ALLOWED_RETRIES,\n            renderMode = IntConfigurable.RenderMode.TextField,\n            describe = {\n                if (it == 0) {\n                    Res.string.settings_download_max_retries_count_describe_no_retries.asStringSource()\n                } else {\n                    Res.string.settings_download_max_retries_count_describe_n_retries\n                        .asStringSourceWithARgs(\n                            Res.string.settings_download_max_retries_count_describe_n_retries_createArgs(\n                                count = \"$it\"\n                            )\n                        )\n                }\n            },\n        )\n    }\n\n    fun dynamicPartDownloadConfig(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_dynamic_part_creation.asStringSource(),\n            description = Res.string.settings_dynamic_part_creation_description.asStringSource(),\n            backedBy = appRepository.dynamicPartCreation,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useServerLastModified(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_use_server_last_modified_time.asStringSource(),\n            description = Res.string.settings_use_server_last_modified_time_description.asStringSource(),\n            backedBy = appRepository.useServerLastModifiedTime,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun appendExtensionToIncompleteDownloads(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_append_extension_to_incomplete_downloads.asStringSource(),\n            description = Res.string.settings_append_extension_to_incomplete_downloads_description.asStringSource(),\n            backedBy = appRepository.appendExtensionToIncompleteDownloads,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useSparseFileAllocation(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_use_sparse_file_allocation.asStringSource(),\n            description = Res.string.settings_use_sparse_file_allocation_description.asStringSource(),\n            backedBy = appRepository.useSparseFileAllocation,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun trackDeletedFilesOnDisk(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_track_deleted_files_on_disk.asStringSource(),\n            description = Res.string.settings_track_deleted_files_on_disk_description.asStringSource(),\n            backedBy = appRepository.trackDeletedFilesOnDisk,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun deletePartialFileOnDownloadCancellation(appSettingsStorage: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_delete_partial_file_on_download_cancellation.asStringSource(),\n            description = Res.string.settings_delete_partial_file_on_download_cancellation_description.asStringSource(),\n            backedBy = appSettingsStorage.deletePartialFileOnDownloadCancellation,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun ignoreSSLCertificates(appSettingsStorage: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_ignore_ssl_certificates.asStringSource(),\n            description = Res.string.settings_ignore_ssl_certificates_description.asStringSource(),\n            backedBy = appSettingsStorage.ignoreSSLCertificates,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun userAgent(appSettingsStorage: BaseAppSettingsStorage): StringConfigurable {\n        return StringConfigurable(\n            title = Res.string.settings_default_user_agent.asStringSource(),\n            description = Res.string.settings_default_user_agent_description.asStringSource(),\n            backedBy = appSettingsStorage.userAgent,\n            describe = {\n                if (it.isBlank()) {\n                    Res.string.disabled.asStringSource()\n                } else {\n                    \"\".asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useCategoryByDefault(appSettingsStorage: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_use_category_by_default.asStringSource(),\n            description = Res.string.settings_use_category_by_default_description.asStringSource(),\n            backedBy = appSettingsStorage.useCategoryByDefault,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun sizeUnit(\n        appRepository: BaseAppRepository,\n        scope: CoroutineScope\n    ): EnumConfigurable<ConvertSizeConfig> {\n        return EnumConfigurable(\n            title = Res.string.settings_download_size_unit.asStringSource(),\n            description = Res.string.settings_download_size_unit_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                appRepository.sizeUnit,\n                updater = { appRepository.setSizeUnit(it) },\n                scope = scope\n            ),\n            possibleValues = listOf(\n                CommonSizeConvertConfigs.BinaryBytes,\n                CommonSizeConvertConfigs.DecimalBytes,\n            ),\n            describe = {\n                val sizeUnit = SizeUnit(\n                    SizeFactors.FactorValue.Kilo,\n                    it.baseSize,\n                    it.factors,\n                )\n                \"$sizeUnit\".asStringSource()\n            },\n        )\n    }\n\n    fun speedUnit(\n        appRepository: BaseAppRepository,\n        scope: CoroutineScope\n    ): EnumConfigurable<ConvertSizeConfig> {\n        return EnumConfigurable(\n            title = Res.string.settings_download_speed_unit.asStringSource(),\n            description = Res.string.settings_download_speed_unit_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                appRepository.speedUnit,\n                updater = { appRepository.setSpeedUnit(it) },\n                scope = scope\n            ),\n            possibleValues = listOf(\n                CommonSizeConvertConfigs.BinaryBytes,\n                CommonSizeConvertConfigs.DecimalBytes,\n                CommonSizeConvertConfigs.BinaryBits,\n                CommonSizeConvertConfigs.DecimalBits,\n            ),\n            describe = {\n                val sizeUnit = SizeUnit(\n                    SizeFactors.FactorValue.Kilo,\n                    it.baseSize,\n                    it.factors,\n                )\n                val extraInfo = \"${it.factors.baseValue} ${it.baseSize.longString()}/s\"\n                \"${sizeUnit}/s ($extraInfo)\".asStringSource()\n            },\n        )\n    }\n\n    fun showDownloadFinishWindow(settingsStorage: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_show_completion_dialog.asStringSource(),\n            description = Res.string.settings_show_completion_dialog_description.asStringSource(),\n            backedBy = settingsStorage.showDownloadCompletionDialog,\n            describe = {\n                (if (it) Res.string.enabled else Res.string.disabled).asStringSource()\n            },\n        )\n    }\n\n    fun autoShowDownloadProgressWindow(settingsStorage: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_show_download_progress_dialog.asStringSource(),\n            description = Res.string.settings_show_download_progress_dialog_description.asStringSource(),\n            backedBy = settingsStorage.showDownloadProgressDialog,\n            describe = {\n                (if (it) Res.string.enabled else Res.string.disabled).asStringSource()\n            },\n        )\n    }\n\n    fun perHostSettings(perHostSettingsPageManager: PerHostSettingsPageManager): NavigatableConfigurable {\n        return NavigatableConfigurable(\n            title = Res.string.settings_per_host_settings.asStringSource(),\n            description = Res.string.settings_per_host_settings_descriptions.asStringSource(),\n            onRequestNavigate = {\n                perHostSettingsPageManager.openPerHostSettings(null)\n            },\n        )\n    }\n\n    fun speedLimitConfig(appRepository: BaseAppRepository): SpeedLimitConfigurable {\n        return SpeedLimitConfigurable(\n            title = Res.string.settings_global_speed_limiter.asStringSource(),\n            description = Res.string.settings_global_speed_limiter_description.asStringSource(),\n            backedBy = appRepository.speedLimiter,\n            describe = {\n                if (it == 0L) {\n                    Res.string.unlimited.asStringSource()\n                } else {\n                    convertPositiveSpeedToHumanReadable(\n                        it,\n                        appRepository.speedUnit.value\n                    ).asStringSource()\n                }\n            }\n        )\n    }\n\n    fun useAverageSpeedConfig(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_show_average_speed.asStringSource(),\n            description = Res.string.settings_show_average_speed_description.asStringSource(),\n            backedBy = appRepository.useAverageSpeed,\n            describe = {\n                if (it) Res.string.average_speed.asStringSource()\n                else Res.string.exact_speed.asStringSource()\n            }\n        )\n    }\n\n    fun defaultDownloadFolderConfig(appSettings: BaseAppSettingsStorage): FolderConfigurable {\n        return FolderConfigurable(\n            title = Res.string.settings_default_download_folder.asStringSource(),\n            description = Res.string.settings_default_download_folder_description.asStringSource(),\n            backedBy = appSettings.defaultDownloadFolder,\n            validate = {\n                FileUtils.Companion.canWriteInThisFolder(it)\n            },\n            describe = {\n                Res.string\n                    .settings_default_download_folder_describe\n                    .asStringSourceWithARgs(\n                        Res.string.settings_default_download_folder_describe_createArgs(\n                            folder = it\n                        )\n                    )\n            }\n        )\n    }\n\n    fun uiScaleConfig(appSettings: BaseAppSettingsStorage): EnumConfigurable<Float> {\n        return EnumConfigurable(\n            title = Res.string.settings_ui_scale.asStringSource(),\n            description = Res.string.settings_ui_scale_description.asStringSource(),\n            backedBy = appSettings.uiScale,\n            possibleValues = listOf(\n                0.8f,\n                0.9f,\n                1f,\n                1.1f,\n                1.25f,\n                1.5f,\n                1.75f,\n                2f,\n            ),\n            renderMode = EnumConfigurable.RenderMode.Spinner,\n            describe = {\n                val percent = (it * 100).roundToInt()\n                if (it == DEFAULT_UI_SCALE) {\n                    StringSource.CombinedStringSource(\n                        listOf(\n                            Res.string.system.asStringSource(),\n                            \"($percent%)\".asStringSource()\n                        ),\n                        \" \"\n                    )\n                } else {\n                    \"$percent%\".asStringSource()\n                }\n            }\n        )\n    }\n\n    fun themeConfig(\n        themeManager: ThemeManager,\n        scope: CoroutineScope,\n    ): ThemeConfigurable {\n        val currentThemeInfo = themeManager.currentThemeInfo\n        val themes = themeManager.selectableThemes\n        return ThemeConfigurable(\n            title = Res.string.settings_theme.asStringSource(),\n            description = Res.string.settings_theme_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = currentThemeInfo,\n                updater = {\n                    themeManager.setTheme(it.id)\n                },\n                scope = scope,\n            ),\n            possibleValues = themes.value,\n            describe = {\n                it.name\n            },\n        )\n    }\n\n    fun defaultDarkThemeConfig(\n        themeManager: ThemeManager,\n        scope: CoroutineScope,\n    ): ThemeConfigurable {\n        val currentDefaultDarkThemeInfo = themeManager.selectedDarkThemeInfo\n        val darkThemes = themeManager.selectableDarkThemes\n        return ThemeConfigurable(\n            title = Res.string.settings_default_dark_theme.asStringSource(),\n            description = Res.string.settings_default_dark_theme_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = currentDefaultDarkThemeInfo,\n                updater = {\n                    themeManager.setDarkTheme(it.id)\n                },\n                scope = scope,\n            ),\n            possibleValues = darkThemes.value,\n            describe = {\n                it.name\n            },\n        )\n    }\n\n    fun defaultLightThemeConfig(\n        themeManager: ThemeManager,\n        scope: CoroutineScope,\n    ): ThemeConfigurable {\n        val currentDefaultLightThemeInfo = themeManager.selectedLightThemeInfo\n        val lightThemes = themeManager.selectableLightThemes\n        return ThemeConfigurable(\n            title = Res.string.settings_default_light_theme.asStringSource(),\n            description = Res.string.settings_default_light_theme_description.asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = currentDefaultLightThemeInfo,\n                updater = {\n                    themeManager.setLightTheme(it.id)\n                },\n                scope = scope,\n            ),\n            possibleValues = lightThemes.value,\n            describe = {\n                it.name\n            },\n        )\n    }\n\n    fun languageConfig(\n        languageManager: LanguageManager,\n        scope: CoroutineScope,\n    ): EnumConfigurable<LanguageInfo?> {\n        val currentLanguageName = languageManager.selectedLanguageInStorage\n        val allLanguages = languageManager.languageList.value\n        return EnumConfigurable(\n            title = Res.string.settings_language.asStringSource(),\n            description = \"\".asStringSource(),\n            backedBy = createMutableStateFlowFromStateFlow(\n                flow = currentLanguageName.mapStateFlow { language ->\n                    language?.let {\n                        allLanguages.find {\n                            it.toLocaleString() == language\n                        }\n                    }\n                },\n                updater = { languageInfo ->\n                    languageManager.selectLanguage(languageInfo)\n                },\n                scope = scope,\n            ),\n            valueToString = {\n                if (it == null) {\n                    emptyList()\n                } else {\n                    listOfNotNull(\n                        it.nativeName,\n                        it.locale.languageCode,\n                        it.locale.countryCode,\n                    )\n                }\n            },\n            possibleValues = listOf(null).plus(allLanguages),\n            describe = {\n                val isAuto = it == null\n                val language = it ?: languageManager.systemLanguageOrDefault\n                val languageName = language.nativeName\n                if (isAuto) {\n                    // always use english here!\n                    \"System ($languageName)\".asStringSource()\n                } else {\n                    languageName.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun showIconLabels(appSettings: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_show_icon_labels.asStringSource(),\n            description = Res.string.settings_show_icon_labels_description.asStringSource(),\n            backedBy = appSettings.showIconLabels,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n    fun useRelativeDateTime(appSettings: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_use_relative_date_time.asStringSource(),\n            description = Res.string.settings_use_relative_date_time_description.asStringSource(),\n            backedBy = appSettings.useRelativeDateTime,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            },\n        )\n    }\n\n\n    fun autoStartConfig(appSettings: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_start_on_boot.asStringSource(),\n            description = Res.string.settings_start_on_boot_description.asStringSource(),\n            backedBy = appSettings.autoStartOnBoot,\n            renderMode = BooleanConfigurable.RenderMode.Switch,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            }\n        )\n    }\n\n    fun playSoundNotification(appSettings: BaseAppSettingsStorage): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_notification_sound.asStringSource(),\n            description = Res.string.settings_notification_sound_description.asStringSource(),\n            backedBy = appSettings.notificationSound,\n            renderMode = BooleanConfigurable.RenderMode.Switch,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            }\n        )\n    }\n\n    fun browserIntegrationEnabled(appRepository: BaseAppRepository): BooleanConfigurable {\n        return BooleanConfigurable(\n            title = Res.string.settings_browser_integration.asStringSource(),\n            description = Res.string.settings_browser_integration_description.asStringSource(),\n            backedBy = appRepository.integrationEnabled,\n            renderMode = BooleanConfigurable.RenderMode.Switch,\n            describe = {\n                if (it) {\n                    Res.string.enabled.asStringSource()\n                } else {\n                    Res.string.disabled.asStringSource()\n                }\n            }\n        )\n    }\n\n    fun browserIntegrationPort(appRepository: BaseAppRepository): IntConfigurable {\n        return IntConfigurable(\n            title = Res.string.settings_browser_integration_server_port.asStringSource(),\n            description = Res.string.settings_browser_integration_server_port_description.asStringSource(),\n            backedBy = appRepository.integrationPort,\n            describe = {\n                Res.string.settings_browser_integration_server_port_describe\n                    .asStringSourceWithARgs(\n                        Res.string.settings_browser_integration_server_port_describe_createArgs(\n                            port = it.toString()\n                        )\n                    )\n            },\n            range = 0..65000,\n        )\n    }\n    fun proxyConfig(proxyManager: ProxyManager): ProxyConfigurable {\n        return ProxyConfigurable(\n            title = Res.string.settings_use_proxy.asStringSource(),\n            description = Res.string.settings_use_proxy_description.asStringSource(),\n            backedBy = proxyManager.proxyData,\n\n            validate = {\n                true\n            },\n            describe = {\n                when (it.proxyMode) {\n                    ProxyMode.Direct -> Res.string.settings_use_proxy_describe_no_proxy.asStringSource()\n                    ProxyMode.UseSystem -> Res.string.settings_use_proxy_describe_system_proxy.asStringSource()\n                    ProxyMode.Manual -> Res.string.settings_use_proxy_describe_manual_proxy\n                        .asStringSourceWithARgs(\n                            Res.string.settings_use_proxy_describe_manual_proxy_createArgs(\n                                value = it.proxyWithRules.proxy.run { \"$type $host:$port\" }\n                            )\n                        )\n\n                    ProxyMode.Pac -> {\n                        Res.string.settings_use_proxy_describe_pac_proxy\n                            .asStringSourceWithARgs(\n                                Res.string.settings_use_proxy_describe_pac_proxy_createArgs(\n                                    value = it.pac.uri\n                                )\n                            )\n                    }\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/singledownloadpage/BaseSingleDownloadComponent.kt",
    "content": "package com.abdownloadmanager.shared.singledownloadpage\n\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.storage.ExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraDownloadItemSettings\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.util.BaseComponent\nimport com.abdownloadmanager.shared.util.DownloadItemOpener\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.shared.util.FileIconProvider\nimport com.abdownloadmanager.shared.util.ThreadCountLimitation\nimport com.abdownloadmanager.shared.util.TimeNames\nimport com.abdownloadmanager.shared.util.convertDurationToHumanReadable\nimport com.abdownloadmanager.shared.util.convertPositiveSizeToHumanReadable\nimport com.abdownloadmanager.shared.util.convertPositiveSpeedToHumanReadable\nimport com.abdownloadmanager.shared.util.convertTimeRemainingToHumanReadable\nimport com.abdownloadmanager.shared.util.mvi.ContainsEffects\nimport com.abdownloadmanager.shared.util.mvi.supportEffects\nimport com.arkivanov.decompose.ComponentContext\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.CompletedDownloadItemState\nimport ir.amirab.downloader.monitor.DurationBasedProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.createMutableStateFlowFromFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.filterIsInstance\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport org.koin.core.component.KoinComponent\nimport kotlin.getValue\n\nabstract class BaseSingleDownloadComponent<\n        TExtraDownloadItemSettings : IExtraDownloadItemSettings\n        >(\n    ctx: ComponentContext,\n    val downloadItemOpener: DownloadItemOpener,\n    private val extraDownloadSettingsStorage: ExtraDownloadSettingsStorage<TExtraDownloadItemSettings>,\n    private val onDismiss: () -> Unit,\n    val downloadId: Long,\n    private val downloadSystem: DownloadSystem,\n    private val appSettings: BaseAppSettingsStorage,\n    private val appRepository: BaseAppRepository,\n    private val applicationScope: CoroutineScope,\n    val fileIconProvider: FileIconProvider,\n) : BaseComponent(ctx),\n    ContainsEffects<BaseSingleDownloadComponent.Effects> by supportEffects(),\n    KoinComponent {\n    open val defaultShowPartInfo: Boolean = true\n\n\n    private val downloadMonitor: IDownloadMonitor = downloadSystem.downloadMonitor\n    private val downloadManager: DownloadManager = downloadSystem.downloadManager\n\n    val itemStateFlow = MutableStateFlow<IDownloadItemState?>(null)\n    protected val globalShowCompletionDialog: StateFlow<Boolean> = appSettings.showDownloadCompletionDialog\n    protected val itemShouldShowCompletionDialog: MutableStateFlow<Boolean?> = MutableStateFlow(null as Boolean?)\n    private val shouldShowCompletionDialog = combineStateFlows(\n        globalShowCompletionDialog,\n        itemShouldShowCompletionDialog,\n    ) { global, item ->\n        item ?: global\n    }\n    val deletePartialFileOnDownloadCancellation = appSettings.deletePartialFileOnDownloadCancellation.asStateFlow()\n\n    val extraDownloadItemSettingsFlow = createMutableStateFlowFromFlow(\n        extraDownloadSettingsStorage\n            .getExternalDownloadItemSettingsAsFlow(downloadId, initialEmit = false),\n        extraDownloadSettingsStorage\n            .getExtraDownloadItemSettings(downloadId),\n        {\n            scope.launch {\n                extraDownloadSettingsStorage.setExtraDownloadItemSettings(\n                    it\n                )\n            }\n        },\n        scope,\n    )\n\n\n    private fun shouldShowCompletionDialog(): Boolean {\n        return shouldShowCompletionDialog.value\n    }\n\n    init {\n        downloadMonitor\n            .downloadListFlow\n            // downloadListFlow (combinedStateFlow { active + completed } downloads) emits null sometimes when download item removed from active downloads and also not exists in completed downloads yet (exactly at the moment that download finishes)\n            // however if the download removed by user (item == null)  this component will be closed outside of this component we don't need to handle this case here\n            // I explicitly filter nulls here to make onEach function predictable\n            // if I fix downloadListFlow to not emit nulls I can remove this filter later\n            .mapNotNull { it.firstOrNull { it.id == downloadId } }\n            .distinctUntilChanged()\n            .onEach {\n                val item = it\n                val previous = itemStateFlow.value\n                if (previous is ProcessingDownloadItemState && item is CompletedDownloadItemState) {\n                    // if It was opened to show progress\n                    if (shouldShowCompletionDialog()) {\n                        itemStateFlow.value = item\n                    } else {\n                        itemStateFlow.value = null\n                        // app component tries to create this component if user want to auto open completion dialog and this component is not created yet\n                        // so we keep this component active a while to prevent create new component\n                        // this prevents opening this window if global [appSettings.showDownloadCompletionDialog] is true but user explicitly tells that he don't want to open completion dialog for this item\n                        delay(100)\n                        close()\n                    }\n                } else {\n                    itemStateFlow.value = item\n                }\n            }.launchIn(scope)\n    }\n\n    private val _showPartInfo = MutableStateFlow(defaultShowPartInfo)\n    val showPartInfo = _showPartInfo.asStateFlow()\n    open fun setShowPartInfo(value: Boolean) {\n        _showPartInfo.value = value\n    }\n\n    // TODO this can be moved to a nested component to reduce system resource usage\n    val extraDownloadProgressInfo: StateFlow<List<SingleDownloadPagePropertyItem>> = itemStateFlow\n        .filterIsInstance<ProcessingDownloadItemState>()\n        .map {\n            buildList {\n                add(SingleDownloadPagePropertyItem(Res.string.name.asStringSource(), it.name.asStringSource()))\n                add(SingleDownloadPagePropertyItem(Res.string.status.asStringSource(), createStatusString(it)))\n                if (it is DurationBasedProcessingDownloadItemState) {\n                    add(\n                        SingleDownloadPagePropertyItem(\n                            Res.string.size.asStringSource(),\n                            it.duration\n                                ?.let(::convertDurationToHumanReadable)\n                                ?: Res.string.unknown.asStringSource()\n                        )\n                    )\n                } else {\n                    add(\n                        SingleDownloadPagePropertyItem(\n                            Res.string.size.asStringSource(),\n                            convertPositiveSizeToHumanReadable(it.contentLength, appRepository.sizeUnit.value)\n                        )\n                    )\n                }\n                add(\n                    SingleDownloadPagePropertyItem(\n                        Res.string.download_page_downloaded_size.asStringSource(),\n                        StringSource.CombinedStringSource(\n                            buildList {\n                                add(convertPositiveSizeToHumanReadable(it.progress, appRepository.sizeUnit.value))\n                                if (it.percent != null) {\n                                    add(\"(${it.percent}%)\".asStringSource())\n                                }\n                            },\n                            \" \"\n                        )\n                    )\n                )\n                add(\n                    SingleDownloadPagePropertyItem(\n                        Res.string.speed.asStringSource(),\n                        convertPositiveSpeedToHumanReadable(it.speed, appRepository.speedUnit.value).asStringSource()\n                    )\n                )\n                add(\n                    SingleDownloadPagePropertyItem(\n                        Res.string.time_left.asStringSource(),\n                        (it.remainingTime?.let { remainingTime ->\n                            convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)\n                        }.orEmpty()).asStringSource()\n                    )\n                )\n                add(\n                    SingleDownloadPagePropertyItem(\n                        Res.string.resume_support.asStringSource(),\n                        when (it.supportResume) {\n                            true -> Res.string.yes.asStringSource()\n                            false -> Res.string.no.asStringSource()\n                            null -> Res.string.unknown.asStringSource()\n                        },\n                        when (it.supportResume) {\n                            true -> SingleDownloadPagePropertyItem.ValueType.Success\n                            false -> SingleDownloadPagePropertyItem.ValueType.Error\n                            null -> SingleDownloadPagePropertyItem.ValueType.Normal\n                        }\n                    )\n                )\n            }\n        }.stateIn(scope, SharingStarted.Eagerly, emptyList())\n\n\n\n    fun openFolder() {\n        val itemState = itemStateFlow.value\n        applicationScope.launch {\n            if (itemState is CompletedDownloadItemState) {\n                downloadItemOpener.openDownloadItemFolder(downloadId)\n            }\n        }\n        onDismiss()\n    }\n\n    fun openFile(alsoClose: Boolean = true) {\n        val itemState = itemStateFlow.value\n        applicationScope.launch {\n            if (itemState is CompletedDownloadItemState) {\n                runCatching {\n                    downloadItemOpener.openDownloadItem(downloadId)\n                }\n            }\n        }\n        if (alsoClose) {\n            onDismiss()\n        }\n    }\n\n    fun toggle() {\n        val state = itemStateFlow.value as? ProcessingDownloadItemState ?: return\n        scope.launch {\n            when {\n                state.canBePaused() -> downloadSystem.manualPause(downloadId)\n                state.canBeResumed() -> downloadSystem.userManualResume(downloadId)\n            }\n        }\n    }\n\n    fun resume() {\n        val state = itemStateFlow.value as? ProcessingDownloadItemState ?: return\n        scope.launch {\n            if (state.canBeResumed()) {\n                downloadSystem.userManualResume(downloadId)\n            }\n        }\n    }\n\n    fun pause() {\n        val state = itemStateFlow.value as? ProcessingDownloadItemState ?: return\n        scope.launch {\n            if (state.canBePaused()) {\n                downloadSystem.manualPause(downloadId)\n            }\n        }\n    }\n\n    fun close() {\n        scope.launch {\n            onDismiss()\n        }\n    }\n\n    fun cancel() {\n        applicationScope.launch {\n            val state = itemStateFlow.value as? ProcessingDownloadItemState\n            if (deletePartialFileOnDownloadCancellation.value) {\n                downloadSystem.reset(downloadId)\n            } else {\n                if (state?.canBePaused() ?: false) {\n                    downloadSystem.manualPause(downloadId)\n                }\n            }\n        }\n        scope.launch {\n            onDismiss()\n        }\n    }\n\n\n    private val threadCount: MutableStateFlow<Int>\n    private val speedLimit: MutableStateFlow<Long>\n\n    init {\n        val dItem = runBlocking {\n            downloadManager.dlListDb.getById(downloadId)\n        }\n        threadCount = MutableStateFlow(\n            dItem?.preferredConnectionCount ?: 0\n        )\n        speedLimit = MutableStateFlow(dItem?.speedLimit ?: 0)\n        downloadManager.listOfJobsEvents\n            .filterIsInstance<DownloadManagerEvents.OnJobChanged>()\n            .filter {\n                it.downloadItem.id == dItem?.id\n            }\n            .onEach { event ->\n                threadCount.update {\n                    event.downloadItem.preferredConnectionCount ?: 0\n                }\n                speedLimit.update {\n                    event.downloadItem.speedLimit\n                }\n            }.launchIn(scope)\n\n\n        threadCount\n            .drop(1)\n            .debounce(500)\n            .onEach { count ->\n                downloadManager.updateDownloadItem(\n                    id = downloadId,\n                    downloadJobExtraConfig = null\n                ) {\n                    it.preferredConnectionCount = count.takeIf { it > 0 }\n                }\n            }.launchIn(scope)\n        speedLimit\n            .drop(1)\n            .debounce(500)\n            .onEach { limit ->\n                downloadManager.updateDownloadItem(\n                    id = downloadId,\n                    downloadJobExtraConfig = null\n                ) {\n                    it.speedLimit = limit\n                }\n            }.launchIn(scope)\n    }\n\n\n    val settings by lazy {\n        listOf(\n            IntConfigurable(\n                title = Res.string.download_item_settings_thread_count.asStringSource(),\n                description = Res.string.download_item_settings_thread_count_description.asStringSource(),\n                backedBy = threadCount,\n                describe = {\n                    if (it == 0) {\n                        Res.string.use_global_settings.asStringSource()\n                    } else {\n                        Res.string.download_item_settings_thread_count_describe\n                            .asStringSourceWithARgs(\n                                Res.string.download_item_settings_thread_count_describe_createArgs(\n                                    count = it.toString()\n                                )\n                            )\n                    }\n                },\n                range = 0..ThreadCountLimitation.MAX_ALLOWED_THREAD_COUNT,\n                renderMode = IntConfigurable.RenderMode.TextField,\n            ),\n            SpeedLimitConfigurable(\n                title = Res.string.download_item_settings_speed_limit.asStringSource(),\n                description = Res.string.download_item_settings_speed_limit_description.asStringSource(),\n                backedBy = speedLimit,\n                describe = {\n                    if (it == 0L) {\n                        Res.string.unlimited.asStringSource()\n                    } else {\n                        convertPositiveSpeedToHumanReadable(it, appRepository.speedUnit.value).asStringSource()\n                    }\n                },\n            ),\n        )\n    }\n\n\n    interface Config {\n        val id: Long\n    }\n\n    sealed interface Effects {\n        sealed interface Common : Effects\n        interface Platform : Effects\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/singledownloadpage/DownloadStatusStringSource.kt",
    "content": "package com.abdownloadmanager.shared.singledownloadpage\n\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.downloader.downloaditem.DownloadJobStatus\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.statusOrFinished\nimport ir.amirab.downloader.utils.ExceptionUtils\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\n\nfun createStatusString(it: IDownloadItemState): StringSource {\n    if (it is ProcessingDownloadItemState && it.isWaiting) {\n        return Res.string.waiting.asStringSource()\n    }\n    return when (val status = it.statusOrFinished()) {\n        is DownloadJobStatus.Canceled -> {\n            if (ExceptionUtils.isNormalCancellation(status.e)) {\n                Res.string.paused\n            } else {\n                Res.string.error\n            }\n        }\n\n        DownloadJobStatus.Downloading -> Res.string.downloading\n        DownloadJobStatus.Finished -> Res.string.finished\n        DownloadJobStatus.IDLE -> Res.string.idle\n        is DownloadJobStatus.PreparingFile -> Res.string.preparing_file\n        DownloadJobStatus.Resuming -> Res.string.resuming\n        is DownloadJobStatus.Retrying -> Res.string.retrying\n    }.asStringSource()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/singledownloadpage/SingleDownloadPagePropertyItem.kt",
    "content": "package com.abdownloadmanager.shared.singledownloadpage\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.util.compose.StringSource\n\n@Immutable\ndata class SingleDownloadPagePropertyItem(\n    val name: StringSource,\n    val value: StringSource,\n    val valueState: ValueType = ValueType.Normal,\n) {\n    enum class ValueType { Normal, Error, Success }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/BaseAppSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport com.abdownloadmanager.shared.ui.theme.ThemeSettingsStorage\nimport ir.amirab.util.compose.localizationmanager.LanguageStorage\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface IAppSettingsModel {\n    val theme: String\n    val defaultDarkTheme: String\n    val defaultLightTheme: String\n    val language: String?\n    val font: String?\n    val uiScale: Float?\n    val showIconLabels: Boolean\n    val useRelativeDateTime: Boolean\n    val threadCount: Int\n    val maxConcurrentDownloads: Int\n    val maxDownloadRetryCount: Int\n    val dynamicPartCreation: Boolean\n    val useServerLastModifiedTime: Boolean\n    val appendExtensionToIncompleteDownloads: Boolean\n    val useSparseFileAllocation: Boolean\n    val useAverageSpeed: Boolean\n    val showDownloadProgressDialog: Boolean\n    val showDownloadCompletionDialog: Boolean\n    val speedLimit: Long\n    val autoStartOnBoot: Boolean\n    val notificationSound: Boolean\n    val defaultDownloadFolder: String\n    val browserIntegrationEnabled: Boolean\n    val browserIntegrationPort: Int\n    val trackDeletedFilesOnDisk: Boolean\n    val deletePartialFileOnDownloadCancellation: Boolean\n    val sizeUnit: SupportedSizeUnits\n    val speedUnit: SupportedSizeUnits\n    val ignoreSSLCertificates: Boolean\n    val useCategoryByDefault: Boolean\n    val userAgent: String\n}\n\n\ninterface BaseAppSettingsStorage :\n    LanguageStorage,\n    ThemeSettingsStorage {\n    override val theme: MutableStateFlow<String>\n    override val defaultDarkTheme: MutableStateFlow<String>\n    override val defaultLightTheme: MutableStateFlow<String>\n    override val selectedLanguage: MutableStateFlow<String?>\n    val font: MutableStateFlow<String?>\n    val uiScale: MutableStateFlow<Float>\n    val showIconLabels: MutableStateFlow<Boolean>\n    val useRelativeDateTime: MutableStateFlow<Boolean>\n    val threadCount: MutableStateFlow<Int>\n    val maxConcurrentDownloads: MutableStateFlow<Int>\n    val dynamicPartCreation: MutableStateFlow<Boolean>\n    val useServerLastModifiedTime: MutableStateFlow<Boolean>\n    val appendExtensionToIncompleteDownloads: MutableStateFlow<Boolean>\n    val useSparseFileAllocation: MutableStateFlow<Boolean>\n    val useAverageSpeed: MutableStateFlow<Boolean>\n    val maxDownloadRetryCount: MutableStateFlow<Int>\n    val showDownloadProgressDialog: MutableStateFlow<Boolean>\n    val showDownloadCompletionDialog: MutableStateFlow<Boolean>\n    val speedLimit: MutableStateFlow<Long>\n    val autoStartOnBoot: MutableStateFlow<Boolean>\n    val notificationSound: MutableStateFlow<Boolean>\n    val defaultDownloadFolder: MutableStateFlow<String>\n    val browserIntegrationEnabled: MutableStateFlow<Boolean>\n    val browserIntegrationPort: MutableStateFlow<Int>\n    val trackDeletedFilesOnDisk: MutableStateFlow<Boolean>\n    val deletePartialFileOnDownloadCancellation: MutableStateFlow<Boolean>\n    val sizeUnit: MutableStateFlow<SupportedSizeUnits>\n    val speedUnit: MutableStateFlow<SupportedSizeUnits>\n    val ignoreSSLCertificates: MutableStateFlow<Boolean>\n    val useCategoryByDefault: MutableStateFlow<Boolean>\n    val userAgent: MutableStateFlow<String>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/ExtraDownloadSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport ir.amirab.downloader.db.TransactionalFileSaver\nimport ir.amirab.downloader.utils.SuspendLockList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.emitAll\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nclass ExtraDownloadSettingsStorage<T : IExtraDownloadItemSettings>(\n    private val folder: File,\n    private val transactionalFileSaver: TransactionalFileSaver,\n    private val dataClassDefinitions: IExtraDownloadItemSettings.DataClassDefinitions<T>\n) : IExtraDownloadSettingsStorage<T> {\n    private fun getFileOf(id: Long) = folder\n        .resolve(\"${id}.json\")\n\n    private val updateLocks = SuspendLockList<Long>()\n    private val lastEmits = MutableSharedFlow<T>(\n        extraBufferCapacity = 64,// too big!\n        onBufferOverflow = BufferOverflow.DROP_OLDEST,\n    )\n\n    override suspend fun deleteExtraDownloadItemSettings(\n        downloadId: Long,\n    ) {\n        lastEmits.tryEmit(\n            dataClassDefinitions.createDefault(downloadId)\n        )\n        getFileOf(downloadId).delete()\n    }\n\n    override suspend fun setExtraDownloadItemSettings(\n        extraDownloadItemSettings: T,\n    ) {\n        require(extraDownloadItemSettings.id >= 0) {\n            \"downloadId must be >= 0\"\n        }\n        val file = getFileOf(extraDownloadItemSettings.id)\n        lastEmits.tryEmit(extraDownloadItemSettings)\n        return withContext(Dispatchers.IO) {\n            updateLocks.withLock(extraDownloadItemSettings.id) {\n                transactionalFileSaver.writeObject(\n                    file,\n                    extraDownloadItemSettings,\n                    dataClassDefinitions.serializer\n                )\n            }\n        }\n    }\n\n    override fun getExtraDownloadItemSettings(downloadId: Long): T {\n        val file = getFileOf(downloadId)\n        return transactionalFileSaver\n            .readObject(file, dataClassDefinitions.serializer)\n            ?: dataClassDefinitions.createDefault(downloadId)\n    }\n\n    override fun getExternalDownloadItemSettingsAsFlow(\n        id: Long, initialEmit: Boolean,\n    ): Flow<T> {\n        return flow {\n            if (initialEmit) {\n                emit(getExtraDownloadItemSettings(id))\n            }\n            emitAll(lastEmits.filter { it.id == id })\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/ExtraQueueSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport ir.amirab.downloader.db.TransactionalFileSaver\nimport ir.amirab.downloader.utils.SuspendLockList\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.emitAll\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nclass ExtraQueueSettingsStorage<T : IExtraQueueSettings>(\n    private val folder: File,\n    private val transactionalFileSaver: TransactionalFileSaver,\n    private val dataClassDefinitions: IExtraQueueSettings.DataClassDefinitions<T>,\n) : IExtraQueueSettingsStorage<T> {\n    private fun getFileOf(id: Long) = folder\n        .resolve(\"${id}.json\")\n\n    private val updateLocks = SuspendLockList<Long>()\n    private val lastEmits = MutableSharedFlow<T>(\n        extraBufferCapacity = 64,// too big!\n        onBufferOverflow = BufferOverflow.DROP_OLDEST,\n    )\n\n    override suspend fun deleteExtraQueueSettings(\n        queueId: Long,\n    ) {\n        lastEmits.tryEmit(\n            dataClassDefinitions.createDefault(queueId)\n        )\n        getFileOf(queueId).delete()\n    }\n\n    override suspend fun setExtraQueueSettings(\n        extraQueueSettings: T,\n    ) {\n        require(extraQueueSettings.id >= 0) {\n            \"queueId must be >= 0. given ${extraQueueSettings.id}\"\n        }\n        val file = getFileOf(extraQueueSettings.id)\n        lastEmits.tryEmit(extraQueueSettings)\n        return withContext(Dispatchers.IO) {\n            updateLocks.withLock(extraQueueSettings.id) {\n                transactionalFileSaver.writeObject(\n                    file,\n                    extraQueueSettings,\n                    dataClassDefinitions.serializer,\n                )\n            }\n        }\n    }\n\n    override fun getExtraQueueSettings(queueId: Long): T {\n        val file = getFileOf(queueId)\n        return transactionalFileSaver\n            .readObject(file, dataClassDefinitions.serializer)\n            ?: dataClassDefinitions.createDefault(queueId)\n    }\n\n    override fun getExternalQueueSettingsAsFlow(\n        id: Long, initialEmit: Boolean,\n    ): Flow<T> {\n        return flow {\n            if (initialEmit) {\n                emit(getExtraQueueSettings(id))\n            }\n            emitAll(lastEmits.filter { it.id == id })\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/IExtraDownloadSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.KSerializer\n\ninterface IExtraDownloadSettingsStorage<T : IExtraDownloadItemSettings> {\n    suspend fun deleteExtraDownloadItemSettings(downloadId: Long)\n    suspend fun setExtraDownloadItemSettings(extraDownloadItemSettings: T)\n    fun getExtraDownloadItemSettings(downloadId: Long): T\n    fun getExternalDownloadItemSettingsAsFlow(id: Long, initialEmit: Boolean = false): Flow<T>\n}\n\ninterface IExtraDownloadItemSettings {\n    val id: Long\n\n    interface DataClassDefinitions<T : IExtraDownloadItemSettings> {\n        fun createDefault(id: Long): T\n        val serializer: KSerializer<T>\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/IExtraQueueSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.KSerializer\n\ninterface IExtraQueueSettingsStorage<T : IExtraQueueSettings> {\n    suspend fun deleteExtraQueueSettings(queueId: Long)\n    suspend fun setExtraQueueSettings(extraQueueSettings: T)\n    fun getExtraQueueSettings(queueId: Long): T\n    fun getExternalQueueSettingsAsFlow(id: Long, initialEmit: Boolean = false): Flow<T>\n}\n\ninterface IExtraQueueSettings {\n    val id: Long\n\n    interface DataClassDefinitions<T : IExtraQueueSettings> {\n        fun createDefault(id: Long): T\n        val serializer: KSerializer<T>\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/ILastSavedLocationsStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface ILastSavedLocationsStorage {\n    val lastUsedSaveLocations: MutableStateFlow<List<String>>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/PerHostSettingsDatastoreStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport androidx.datastore.core.DataStore\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\nimport com.abdownloadmanager.shared.util.perhostsettings.IPerHostSettingsStorage\nimport com.abdownloadmanager.shared.util.perhostsettings.PerHostSettingsItem\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass PerHostSettingsDatastoreStorage(\n    dataStore: DataStore<List<PerHostSettingsItem>>,\n) : IPerHostSettingsStorage, ConfigBaseSettingsByJson<List<PerHostSettingsItem>>(dataStore) {\n    override val perHostSettingsFlow: MutableStateFlow<List<PerHostSettingsItem>> = data\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/ProxyDatastoreStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport androidx.datastore.core.DataStore\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\nimport com.abdownloadmanager.shared.util.proxy.IProxyStorage\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\n\nclass ProxyDatastoreStorage(\n    dataStore: DataStore<ProxyData>,\n) : IProxyStorage, ConfigBaseSettingsByJson<ProxyData>(dataStore) {\n    override val proxyDataFlow = data\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/SupportedSizeUnits.kt",
    "content": "package com.abdownloadmanager.shared.storage\n\nimport ir.amirab.util.datasize.CommonSizeConvertConfigs\nimport ir.amirab.util.datasize.ConvertSizeConfig\n\nenum class SupportedSizeUnits {\n    BinaryBits,\n    BinaryBytes,\n    DecimalBits,\n    DecimalBytes;\n\n    fun toConfig(): ConvertSizeConfig {\n        return when (this) {\n            BinaryBits -> CommonSizeConvertConfigs.BinaryBits\n            BinaryBytes -> CommonSizeConvertConfigs.BinaryBytes\n            DecimalBits -> CommonSizeConvertConfigs.DecimalBits\n            DecimalBytes -> CommonSizeConvertConfigs.DecimalBytes\n        }\n    }\n\n    companion object {\n        fun fromConfig(config: ConvertSizeConfig): SupportedSizeUnits? {\n            return when (config) {\n                CommonSizeConvertConfigs.BinaryBits -> BinaryBits\n                CommonSizeConvertConfigs.BinaryBytes -> BinaryBytes\n                CommonSizeConvertConfigs.DecimalBits -> DecimalBits\n                CommonSizeConvertConfigs.DecimalBytes -> DecimalBytes\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/storage/impl/LastSavedLocationStorage.kt",
    "content": "package com.abdownloadmanager.shared.storage.impl\n\nimport androidx.datastore.core.DataStore\nimport com.abdownloadmanager.shared.storage.ILastSavedLocationsStorage\nimport com.abdownloadmanager.shared.util.ConfigBaseSettingsByJson\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass LastSavedLocationStorage(\n    dataStore: DataStore<List<String>>\n) : ConfigBaseSettingsByJson<List<String>>(dataStore), ILastSavedLocationsStorage {\n    override val lastUsedSaveLocations: MutableStateFlow<List<String>> = data\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/Bootstrap.kt",
    "content": "package com.abdownloadmanager.shared.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport com.abdownloadmanager.shared.repository.BaseAppRepository\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport com.abdownloadmanager.shared.ui.configurable.ConfigurableRendererRegistry\nimport com.abdownloadmanager.shared.ui.configurable.LocalConfigurationRendererRegistry\nimport com.abdownloadmanager.shared.util.LocalUseRelativeDateTime\nimport com.abdownloadmanager.shared.util.ProvideSizeAndSpeedUnit\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.LocalIconFromUriResolver\n\n\n@Composable\nfun ProvideCommonSettings(\n    appSettings: BaseAppSettingsStorage,\n    iconProvider: IIconResolver,\n    configurableRendererRegistry: ConfigurableRendererRegistry,\n    content: @Composable () -> Unit,\n) {\n    val useNativeDateTime by appSettings.useRelativeDateTime.collectAsState()\n    CompositionLocalProvider(\n        LocalUseRelativeDateTime provides useNativeDateTime,\n        LocalIconFromUriResolver provides iconProvider,\n        LocalConfigurationRendererRegistry provides configurableRendererRegistry,\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun ProvideSizeUnits(\n    appRepository: BaseAppRepository,\n    content: @Composable () -> Unit,\n) {\n    ProvideSizeAndSpeedUnit(\n        sizeUnitConfig = appRepository.sizeUnit.collectAsState().value,\n        speedUnitConfig = appRepository.speedUnit.collectAsState().value,\n        content = content\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/BaseEnumConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nabstract class BaseEnumConfigurable<T>(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<T>,\n    describe: ((T) -> StringSource),\n    val possibleValues: List<T>,\n    val valueToString: (T) -> List<String>,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<T>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = {\n        it in possibleValues\n    },\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/BaseLongConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nabstract class BaseLongConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Long>,\n    describe: ((Long) -> StringSource),\n    val range: LongRange,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Long>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = {\n        it in range\n    },\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/CommonConfigurableRenderers.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport com.abdownloadmanager.shared.ui.configurable.item.BooleanConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.DayOfWeekConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.EnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.FileChecksumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.FloatConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.FolderConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.IntConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.LongConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.NavigatableConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ProxyConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.SpeedLimitConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.StringConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.ThemeConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.item.TimeConfigurable\n\ninterface ContainsConfigurableRenderers {\n    fun getAllRenderers(): Map<Configurable.Key, ConfigurableRenderer<*>>\n}\n\ndata class CommonConfigurableRenderers(\n    val booleanConfigurableRenderer: ConfigurableRenderer<BooleanConfigurable>,\n    val dayOfWeekConfigurableRenderer: ConfigurableRenderer<DayOfWeekConfigurable>,\n    val fileChecksumConfigurableRenderer: ConfigurableRenderer<FileChecksumConfigurable>,\n    val floatConfigurableRenderer: ConfigurableRenderer<FloatConfigurable>,\n    val folderConfigurableRenderer: ConfigurableRenderer<FolderConfigurable>,\n    val intConfigurableRenderer: ConfigurableRenderer<IntConfigurable>,\n    val longConfigurableRenderer: ConfigurableRenderer<LongConfigurable>,\n    val perHostSettingsConfigurableRenderer: ConfigurableRenderer<NavigatableConfigurable>,\n    val enumConfigurableRenderer: ConfigurableRenderer<EnumConfigurable<Any>>,\n    val speedConfigurableRenderer: ConfigurableRenderer<SpeedLimitConfigurable>,\n    val stringConfigurableRenderer: ConfigurableRenderer<StringConfigurable>,\n    val themeConfigurableRenderer: ConfigurableRenderer<ThemeConfigurable>,\n    val timeConfigurableRenderer: ConfigurableRenderer<TimeConfigurable>,\n    val proxyConfigurableRenderer: ConfigurableRenderer<ProxyConfigurable>,\n\n    ) : ContainsConfigurableRenderers {\n    override fun getAllRenderers(): Map<Configurable.Key, ConfigurableRenderer<*>> {\n        return mapOf(\n            BooleanConfigurable.Key to booleanConfigurableRenderer,\n            DayOfWeekConfigurable.Key to dayOfWeekConfigurableRenderer,\n            FileChecksumConfigurable.Key to fileChecksumConfigurableRenderer,\n            FloatConfigurable.Key to floatConfigurableRenderer,\n            FolderConfigurable.Key to folderConfigurableRenderer,\n            IntConfigurable.Key to intConfigurableRenderer,\n            LongConfigurable.Key to longConfigurableRenderer,\n            NavigatableConfigurable.Key to perHostSettingsConfigurableRenderer,\n            EnumConfigurable.Key to enumConfigurableRenderer,\n            SpeedLimitConfigurable.Key to speedConfigurableRenderer,\n            StringConfigurable.Key to stringConfigurableRenderer,\n            ThemeConfigurable.Key to themeConfigurableRenderer,\n            TimeConfigurable.Key to timeConfigurableRenderer,\n            ProxyConfigurable.Key to proxyConfigurableRenderer,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/Configurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nabstract class Configurable<T>(\n    val title: StringSource,\n    val description: StringSource,\n    val backedBy: MutableStateFlow<T>,\n    val validate: (T) -> Boolean = { true },\n    val describe: (T) -> StringSource,\n    val enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    val visible: StateFlow<Boolean> = DefaultVisibleValue,\n) {\n    // each configurable should have a unique key\n    // we use this to retrieve its renderer from registry\n    interface Key\n\n    abstract fun getKey(): Key\n\n    val stateFlow = backedBy.asStateFlow()\n    fun set(value: T): Boolean {\n        if (validate(value)) {\n            // don't use update function here maybe this is a mappedByTwoWayMutableStateFlow\n            // IMPROVE\n            backedBy.value = value\n            return true\n        }\n        return false\n    }\n\n    companion object {\n        val DefaultEnabledValue get() = MutableStateFlow(true)\n        val DefaultVisibleValue get() = MutableStateFlow(true)\n    }\n}\n\n@Immutable\ndata class ConfigurableUiProps(\n    val modifier: Modifier = Modifier,\n    val itemPaddingValues: PaddingValues = PaddingValues.Zero,\n)\ninterface ConfigurableRenderer<TConfigurable : Configurable<*>> {\n    @Composable\n    fun RenderConfigurable(\n        configurable: TConfigurable,\n        configurableUiProps: ConfigurableUiProps,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/ConfigurableGroup.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport androidx.compose.runtime.Stable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\n@Stable\ndata class ConfigurableGroup(\n    val groupTitle: StateFlow<StringSource?> = MutableStateFlow(null),\n    val mainConfigurable:Configurable<*>?=null,\n    val nestedEnabled:StateFlow<Boolean> =MutableStateFlow(true),\n    val nestedVisible:StateFlow<Boolean> =MutableStateFlow(true),\n    val nestedConfigurable: List<Configurable<*>>,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/ConfigurableRendererRegistry.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.Modifier\n\n// >>>>>>>>>\n// only use these in public\nfun ConfigurableRendererRegistry(\n    builder: ConfigurableRendererRegistryBuilder.() -> Unit\n): ConfigurableRendererRegistry {\n    return ConfigurableRendererRegistry(\n        map = ConfigurableRendererRegistryBuilder()\n            .apply(builder).map\n    )\n}\n\n@Composable\nfun <T> Configurable<T>.Render(\n    configurableUiProps: ConfigurableUiProps,\n) {\n    LocalConfigurationRendererRegistry.current\n        .get(this)\n        .RenderConfigurable(this, configurableUiProps)\n}\n\n// <<<<<<<<\n// end of public api\n\n\nclass ConfigurableRendererRegistryBuilder internal constructor() {\n    @PublishedApi\n    internal val map: MutableMap<Configurable.Key, ConfigurableRenderer<*>> = mutableMapOf()\n    fun <\n            TConfigurable : Configurable<*>,\n            TConfigurableRenderer : ConfigurableRenderer<TConfigurable>\n            > register(\n        key: Configurable.Key,\n        renderer: TConfigurableRenderer\n    ) {\n        map[key] = renderer\n    }\n}\n\n\nclass ConfigurableRendererRegistry internal constructor(\n    private val map: MutableMap<Configurable.Key, ConfigurableRenderer<*>>\n) {\n    fun <\n            TConfigurable : Configurable<*>,\n            TConfigurableRenderer : ConfigurableRenderer<TConfigurable>\n            > get(\n        configurable: TConfigurable\n    ): TConfigurableRenderer {\n        val renderer = requireNotNull(map[configurable.getKey()]) {\n            \"renderer for $configurable not found\"\n        }\n        @Suppress(\"UNCHECKED_CAST\")\n        return renderer as TConfigurableRenderer\n    }\n}\n\nval LocalConfigurationRendererRegistry = staticCompositionLocalOf<ConfigurableRendererRegistry> {\n    error(\"LocalConfigurationRendererRegistry not provided\")\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/RenderConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\n\n@Immutable\ndata class ConfigGroupInfo(\n    val visible: Boolean,\n    val enabled: Boolean,\n)\n\n@Suppress(\"UNCHECKED_CAST\")\n@Composable\nfun RenderConfigurable(\n    cfg: Configurable<*>,\n    configurableUiProps: ConfigurableUiProps,\n    groupInfo: ConfigGroupInfo? = null,\n) {\n    ConfigurationWrapper(\n        configurable = cfg,\n        groupInfo = groupInfo\n    ) {\n        cfg.Render(configurableUiProps)\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/RenderConfigurableGroup.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\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.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.ifThen\n\n@Composable\nfun RenderConfigurableGroup(\n    group: ConfigurableGroup,\n    modifier: Modifier,\n    itemPadding: PaddingValues = PaddingValues(),\n    spaceBy: Dp = 8.dp,\n) {\n    val enabled by group.nestedEnabled.collectAsState()\n    val visible by group.nestedVisible.collectAsState()\n    val title by group.groupTitle.collectAsState()\n    Column(\n        modifier\n            .clip(myShapes.defaultRounded)\n            .background(myColors.surface / 0.5f)\n    ) {\n        title?.rememberString()?.let {\n            Text(\n                text = it,\n                fontSize = myTextSizes.base,\n                fontWeight = FontWeight.Bold,\n                modifier = Modifier\n                    .padding(start = 8.dp)\n                    .padding(vertical = 8.dp)\n            )\n            Spacer(\n                Modifier\n                    .fillMaxWidth()\n                    .height(1.dp)\n                    .background(myColors.onSurface / 0.1f)\n            )\n        }\n        group.mainConfigurable?.let {\n            RenderConfigurable(\n                it,\n                configurableUiProps = ConfigurableUiProps(\n                    modifier = Modifier.fillMaxWidth()\n                        .ifThen(title != null) {\n                            padding(top = 4.dp)\n                        }\n                        .ifThen(visible) {\n                            padding(bottom = 4.dp)\n                        },\n                    itemPaddingValues = itemPadding\n                )\n\n            )\n        }\n        AnimatedVisibility(visible) {\n            Column(\n                Modifier\n                    .ifThen(group.mainConfigurable != null) {\n                        padding(top = 4.dp)\n                    },\n                verticalArrangement = Arrangement.spacedBy(spaceBy)\n            ) {\n                group.nestedConfigurable.forEach {\n                    RenderConfigurable(\n                        cfg = it,\n                        configurableUiProps = ConfigurableUiProps(\n                            modifier = Modifier\n                                .fillMaxWidth(),\n                            itemPaddingValues = itemPadding\n                        ),\n                        groupInfo = ConfigGroupInfo(\n                            enabled = enabled,\n                            visible = visible,\n                        ),\n                    )\n                }\n            }\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/Shared.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable\n\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.input.key.utf16CodePoint\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInParent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.shared.ui.widget.Help\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport io.github.oikvpqya.compose.fastscroller.rememberScrollbarAdapter\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlin.math.roundToInt\nfun <T> defaultValueToString(item: T): List<String> {\n    return emptyList()\n}\n\nprivate const val SEARCH_RESET_TIMEOUT = 2_000L\nprivate fun Modifier.onSearch(\n    searchDelayTimeout: Long = SEARCH_RESET_TIMEOUT,\n    onSearchRequested: (String) -> Unit\n): Modifier {\n    return composed {\n        var textToSearch by remember {\n            mutableStateOf(\"\")\n        }\n        LaunchedEffect(textToSearch) {\n            if (textToSearch.isNotEmpty()) {\n                onSearchRequested(textToSearch)\n                delay(searchDelayTimeout)\n                textToSearch = \"\"\n            }\n        }\n        onKeyEvent {\n            if (it.type == KeyEventType.KeyDown) {\n                val char = it.utf16CodePoint.toChar()\n                if (char.isLetterOrDigit()) {\n                    textToSearch += char\n                    true\n                } else {\n                    false\n                }\n            } else {\n                false\n            }\n        }\n    }\n}\n\n@Composable\nfun <T> RenderSpinner(\n    possibleValues: List<T>,\n    value: T,\n    onSelect: (T) -> Unit,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    valueToString: (T) -> List<String> = ::defaultValueToString,\n//    minWidth:Dp,\n    render: @Composable (T) -> Unit,\n) {\n    val verticalPadding = 4.dp\n    val horizontalPadding = 4.dp\n\n    var isOpen by remember { mutableStateOf(false) }\n    val shape = myShapes.defaultRounded\n    val borderWidth = 1.dp\n    val borderColor = myColors.onBackground / 10\n    var widthForPopup by remember {\n        mutableStateOf(0.dp)\n    }\n    val density = LocalDensity.current\n    Box {\n        Row(\n            modifier = modifier\n//                .widthIn(min = minWidth)\n                .clip(shape)\n                .height(IntrinsicSize.Max)\n                .onGloballyPositioned {\n                    widthForPopup = with(density) {\n                        it.size.width.toDp()\n                    }\n                }\n                .background(myColors.surface)\n                .border(borderWidth, borderColor, shape)\n                .heightIn(mySpacings.thumbSize)\n                .clickable(enabled = enabled) {\n                    isOpen = true\n                },\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            WithContentAlpha(1f) {\n                Box(\n                    Modifier\n                        .padding(vertical = verticalPadding)\n                        .padding(horizontal = horizontalPadding)\n                ) {\n                    render(value)\n                }\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    Spacer(\n                        Modifier.fillMaxHeight()\n                            .padding(vertical = borderWidth)\n                            .width(borderWidth).background(myColors.onBackground / 10)\n                    )\n                    MyIcon(MyIcons.down, null, Modifier.padding(4.dp).size(mySpacings.iconSize))\n                }\n            }\n        }\n        if (isOpen) {\n            Popup(\n                popupPositionProvider = rememberMyComponentRectPositionProvider(\n                    offset = DpOffset(y = 2.dp, x = 0.dp)\n                ),\n                onDismissRequest = { isOpen = false },\n                properties = PopupProperties(\n                    focusable = true\n                )\n            ) {\n                val coroutineScope = rememberCoroutineScope()\n                val focusRequester = remember { FocusRequester() }\n                LaunchedEffect(Unit) {\n                    focusRequester.requestFocus()\n                }\n                val possibleValuePositions = remember(possibleValues) {\n                    mutableStateMapOf<Int, Float>()\n                }\n                var itemToBeIndicated: Int by remember {\n                    mutableStateOf(-1)\n                }\n                LaunchedEffect(itemToBeIndicated) {\n                    if (itemToBeIndicated != -1) {\n                        delay(SEARCH_RESET_TIMEOUT)\n                        itemToBeIndicated = -1\n                    }\n                }\n                Box {\n                    val scrollState = rememberScrollState()\n                    Column(\n                        Modifier\n                            .clip(shape)\n                            .width(IntrinsicSize.Max)\n                            .widthIn(widthForPopup)\n                            .heightIn(max = 360.dp)\n                            .onSearch { searchText ->\n                                val itemIndex = possibleValues\n                                    .indexOfFirst { value ->\n                                        valueToString(value).any { string ->\n                                            string.startsWith(\n                                                searchText,\n                                                ignoreCase = true,\n                                            )\n                                        }\n                                    }\n                                if (itemIndex == -1) {\n                                    return@onSearch\n                                }\n                                val position = possibleValuePositions[itemIndex]?.roundToInt()\n                                coroutineScope.launch {\n                                    position?.let {\n                                        scrollState.scrollTo(it)\n                                        itemToBeIndicated = itemIndex\n                                    }\n                                }\n                            }\n                            .focusRequester(focusRequester)\n                            .focusable()\n                            .background(myColors.surface)\n                            .border(borderWidth, borderColor, shape)\n                            .padding(borderWidth)\n                            .clip(shape)\n                            .verticalScroll(scrollState)\n                    ) {\n                        WithContentColor(myColors.onSurface) {\n                            for ((index, p) in possibleValues.withIndex()) {\n                                key(p) {\n                                    val isIndicating = itemToBeIndicated == index\n                                    Row(\n                                        modifier = Modifier\n                                            .onGloballyPositioned {\n                                                possibleValuePositions[index] = it.positionInParent().y\n                                            }\n                                            .ifThen(isIndicating) {\n                                                background(\n                                                    myColors.onBackground / 0.05f\n                                                )\n                                            }\n                                            .heightIn(mySpacings.thumbSize)\n                                            .clickable(onClick = {\n                                                isOpen = false\n                                                onSelect(p)\n                                            }),\n                                        verticalAlignment = Alignment.CenterVertically,\n                                    ) {\n                                        val selected = p == value\n                                        WithContentAlpha(if (selected) 1f else 0.75f) {\n                                            Box(\n                                                Modifier\n                                                    .weight(1f)\n                                                    .padding(vertical = verticalPadding)\n                                                    .padding(horizontal = horizontalPadding)\n                                            ) {\n                                                render(p)\n                                            }\n                                        }\n                                        Spacer(\n                                            Modifier.width(borderWidth)\n                                        )\n                                        if (selected) {\n                                            MyIcon(MyIcons.check, null, Modifier.padding(4.dp).size(mySpacings.iconSize))\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    MultiplatformVerticalScrollbar(\n                        rememberScrollbarAdapter(scrollState),\n                        modifier = Modifier\n                            .padding(vertical = borderWidth)\n                            .matchParentSize().wrapContentWidth(Alignment.End)\n                    )\n                }\n            }\n        }\n    }\n}\n\n\nprivate val LocalConfigurableIsEnabled = compositionLocalOf<Boolean> {\n    error(\"LocalConfigurableIsEnabled not provided\")\n}\nprivate val LocalConfigurableIsVisible = compositionLocalOf<Boolean> {\n    error(\"LocalConfigurableIsVisible not provided\")\n}\n\n@Composable\nfun isConfigVisible(): Boolean {\n    return LocalConfigurableIsVisible.current\n}\n\n@Composable\nfun isConfigEnabled(): Boolean {\n    return LocalConfigurableIsEnabled.current\n}\n\n@Composable\nfun ConfigurationWrapper(\n    configurable: Configurable<*>,\n    groupInfo: ConfigGroupInfo? = null,\n    content: @Composable () -> Unit,\n) {\n    val enabled by configurable.enabled.collectAsState()\n    val visible by configurable.visible.collectAsState()\n    CompositionLocalProvider(\n        LocalConfigurableIsEnabled provides (enabled && groupInfo?.enabled ?: true),\n        LocalConfigurableIsVisible provides (visible && groupInfo?.visible ?: true),\n    ) {\n        AnimatedVisibility(\n            visible = visible,\n            exit = shrinkVertically(),\n            enter = expandVertically(),\n        ) {\n            content()\n        }\n    }\n}\n\n@Composable\nfun Help(\n    modifier: Modifier = Modifier,\n    cfg: Configurable<*>,\n) {\n    Help(cfg.description.rememberString(), modifier)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/BooleanConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass BooleanConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Boolean>,\n    describe: ((Boolean) -> StringSource),\n    val renderMode: RenderMode = RenderMode.Switch,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Boolean>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = { true },\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n\n    enum class RenderMode {\n        Checkbox, Switch,\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/DayOfWeekConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.datetime.DayOfWeek\n\nclass DayOfWeekConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Set<DayOfWeek>>,\n    describe: (Set<DayOfWeek>) -> StringSource,\n    validate: (Set<DayOfWeek>) -> Boolean,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Set<DayOfWeek>>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    validate = validate,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/EnumConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.BaseEnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.configurable.defaultValueToString\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nopen class EnumConfigurable<T>(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<T>,\n    describe: ((T) -> StringSource),\n    possibleValues: List<T>,\n    valueToString: (T) -> List<String> = ::defaultValueToString,\n    val renderMode: RenderMode = RenderMode.Spinner,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : BaseEnumConfigurable<T>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    possibleValues = possibleValues,\n    valueToString = valueToString,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n\n    enum class RenderMode {\n        Spinner,\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/FileChecksumConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.util.FileChecksum\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass FileChecksumConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<FileChecksum?>,\n    describe: (FileChecksum?) -> StringSource,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<FileChecksum?>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/FloatConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass FloatConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Float>,\n    val range: ClosedFloatingPointRange<Float>,\n    val steps: Int = 0,\n    val renderMode: RenderMode = RenderMode.TextField,\n\n    describe: ((Float) -> StringSource),\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Float>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = { it in range },\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n\n    enum class RenderMode {\n        TextField,\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/FolderConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass FolderConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<String>,\n    describe: ((String) -> StringSource),\n    validate: (String) -> Boolean,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : StringConfigurable(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = validate,\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey(): Configurable.Key = Key\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/IntConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass IntConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Int>,\n    describe: ((Int) -> StringSource),\n    val range: IntRange,\n    val renderMode: RenderMode = RenderMode.TextField,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Int>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = {\n        it in range\n    },\n    describe = describe,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n\n    enum class RenderMode {\n        TextField,\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/LongConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.BaseLongConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass LongConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Long>,\n    describe: ((Long) -> StringSource),\n    range: LongRange,\n    val renderMode: RenderMode = RenderMode.TextField,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : BaseLongConfigurable(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    range = range,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n\n    enum class RenderMode {\n        TextField,\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/NavigatableConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass NavigatableConfigurable(\n    title: StringSource,\n    description: StringSource,\n    val onRequestNavigate: () -> Unit,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<Unit>(\n    title = title,\n    description = description,\n    backedBy = MutableStateFlow(Unit),\n    describe = { \"\".asStringSource() },\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/ProxyConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.util.proxy.ProxyData\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass ProxyConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<ProxyData>,\n    describe: (ProxyData) -> StringSource,\n    validate: (ProxyData) -> Boolean,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<ProxyData>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    validate = validate,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/SpeedLimitConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.BaseLongConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass SpeedLimitConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<Long>,\n    describe: (Long) -> StringSource,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : BaseLongConfigurable(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    range = 0..Long.MAX_VALUE,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/StringConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nopen class StringConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<String>,\n    describe: ((String) -> StringSource),\n    validate: (String) -> Boolean = { true },\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<String>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    validate = validate,\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey(): Configurable.Key = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/ThemeConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.BaseEnumConfigurable\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport com.abdownloadmanager.shared.ui.theme.ThemeInfo\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nclass ThemeConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<ThemeInfo>,\n    describe: (ThemeInfo) -> StringSource,\n    possibleValues: List<ThemeInfo>,\n    valueToString: (ThemeInfo) -> List<String> = {\n        listOf(it.name.getString())\n    },\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : BaseEnumConfigurable<ThemeInfo>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    possibleValues = possibleValues,\n    valueToString = valueToString,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/configurable/item/TimeConfigurable.kt",
    "content": "package com.abdownloadmanager.shared.ui.configurable.item\n\nimport com.abdownloadmanager.shared.ui.configurable.Configurable\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.datetime.LocalTime\n\nclass TimeConfigurable(\n    title: StringSource,\n    description: StringSource,\n    backedBy: MutableStateFlow<LocalTime>,\n    describe: (LocalTime) -> StringSource,\n    enabled: StateFlow<Boolean> = DefaultEnabledValue,\n    visible: StateFlow<Boolean> = DefaultVisibleValue,\n) : Configurable<LocalTime>(\n    title = title,\n    description = description,\n    backedBy = backedBy,\n    describe = describe,\n    enabled = enabled,\n    visible = visible,\n) {\n    object Key : Configurable.Key\n\n    override fun getKey() = Key\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/modifier/PointerHoverIcon.kt",
    "content": "package com.abdownloadmanager.shared.ui.modifier\n\nimport androidx.compose.ui.Modifier\n\nsealed interface MyPointerHoverIcon {\n    data object Default : MyPointerHoverIcon\n    data object Crosshair : MyPointerHoverIcon\n    data object Text : MyPointerHoverIcon\n    data object Hand : MyPointerHoverIcon\n    data object VerticalResize : MyPointerHoverIcon\n    data object HorizontalResize : MyPointerHoverIcon\n}\n\nexpect fun Modifier.myPointerHoverIcon(\n    pointerHoverIcon: MyPointerHoverIcon,\n    overrideDescendants: Boolean,\n): Modifier\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/ABDownloaderTheme.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.TextUnit\nimport com.abdownloadmanager.shared.util.ui.*\nimport com.abdownloadmanager.shared.util.ui.theme.*\nimport com.abdownloadmanager.shared.util.ui.theme.UiScaledContent\n\n\n@Composable\nfun ABDownloaderTheme(\n    myColors: MyColors,\n    fontFamily: FontFamily? = null,\n    uiScale: Float = DEFAULT_UI_SCALE,\n    content: @Composable () -> Unit,\n) {\n    val systemDensity = LocalDensity.current\n    val textSizes = myPlatformTextSizes()\n    CompositionLocalProvider(\n        LocalMyColors provides animatedColors(myColors),\n        LocalUiScale provides uiScale,\n        LocalSystemDensity provides systemDensity,\n        LocalMyShapes provides myPlatformShapes(),\n        LocalSpacing provides myPlatformSpacing(),\n    ) {\n        CompositionLocalProvider(\n            LocalMultiplatformScrollbarStyle provides myPlatformScrollbarStyle(),\n            LocalIndication provides ripple(),\n            LocalContentColor provides myColors.onBackground,\n            LocalContentAlpha provides 1f,\n            LocalTextSizes provides textSizes,\n            LocalTextStyle provides LocalTextStyle.current.copy(\n                lineHeight = TextUnit.Unspecified,\n                fontSize = textSizes.base,\n                fontFamily = fontFamily,\n            ),\n        ) {\n            PlatformDependentProviders {\n                // it is overridden by [Window] Composable,\n                // but I put this here. maybe I need this outside of window  scope!\n                UiScaledContent {\n                    content()\n                }\n            }\n\n        }\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/DefaultThemes.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.ui.graphics.Color\nimport com.abdownloadmanager.shared.util.ui.MyColors\n\nobject DefaultThemes {\n    val dark = MyColors(\n        id = \"dark\",\n        name = \"Dark\",\n        primary = Color(0xFF4791BF),\n        primaryVariant = Color(0xFF60A6D9),\n        onPrimary = Color(0xFFEFF2F6),\n        secondary = Color(0xFFB85DFF),\n        secondaryVariant = Color(0xFFD1A6FF),\n        onSecondary = Color(0xFFEFF2F6),\n        background = Color(0xFF1E1F22),\n        onBackground = Color(0xFFD6D6D6),\n        surface = Color(0xFF2A2B2F),\n        onSurface = Color(0xFFE0E0E0),\n        error = Color(0xFFEA4C3C),\n        onError = Color(0xFFEFEFEF),\n        success = Color(0xFF45B36B),\n        onSuccess = Color(0xFFE5E5E5),\n        warning = Color(0xFFF6C244),\n        onWarning = Color(0xFF1E1E1E),\n        info = Color(0xFF40A9F3),\n        onInfo = Color(0xFF1E1E1E),\n        isLight = false\n    )\n\n    val light = MyColors(\n        id = \"light\",\n        name = \"Light\",\n        primary = Color(0xFF4791BF),\n        primaryVariant = Color(0xFF3576A1),\n        onPrimary = Color(0xFFFFFFFF),\n        secondary = Color(0xFFB85DFF),\n        secondaryVariant = Color(0xFF9700FF),\n        onSecondary = Color(0xFFFFFFFF),\n        background = Color(0xFFFFFFFF),\n        onBackground = Color(0xFF232323),\n        surface = Color(0xFFF2F2F2),\n        onSurface = Color(0xFF232323),\n        error = Color(0xFFEA4C3C),\n        onError = Color(0xFFFFFFFF),\n        success = Color(0xFF45B36B),\n        onSuccess = Color(0xFFFFFFFF),\n        warning = Color(0xFFF6C244),\n        onWarning = Color(0xFF232323),\n        info = Color(0xFF40A9F3),\n        onInfo = Color(0xFF232323),\n        isLight = true\n    )\n\n    val obsidian = MyColors(\n        id = \"obsidian\",\n        name = \"Obsidian\",\n        primary = Color(0xFF4791BF),\n        onPrimary = Color.White,\n        secondary = Color(0xFFB85DFF),\n        onSecondary = Color.White,\n        background = Color(0xFF16161E),\n        onBackground = Color(0xFFBBBBBB),\n        onSurface = Color(0xFFBBBBBB),\n        surface = Color(0xFF22222A),\n        error = Color(0xffff5757),\n        onError = Color.White,\n        success = Color(0xff69BA5A),\n        onSuccess = Color.White,\n        warning = Color(0xFFffbe56),\n        onWarning = Color.White,\n        info = Color(0xFF2f77d4),\n        onInfo = Color.White,\n        isLight = false,\n    )\n\n    val deepOcean = MyColors(\n        id = \"deep_ocean\",\n        name = \"Deep Ocean\",\n        primary = Color(0xFF4791BF),\n        primaryVariant = Color(0xFF60A6D9),\n        onPrimary = Color(0xFFEFF2F6),\n        secondary = Color(0xFFB85DFF),\n        secondaryVariant = Color(0xFFD1A6FF),\n        onSecondary = Color(0xFFEFF2F6),\n        background = Color(0xFF17212B),\n        onBackground = Color(0xFFE5EAF2),\n        surface = Color(0xFF242F3D),\n        onSurface = Color(0xFFE5EAF2),\n        error = Color(0xFFEA4C3C),\n        onError = Color(0xFFE5E5E5),\n        success = Color(0xFF45B36B),\n        onSuccess = Color(0xFFE5E5E5),\n        warning = Color(0xFFF6C244),\n        onWarning = Color(0xFF232323),\n        info = Color(0xFF40A9F3),\n        onInfo = Color(0xFF232323),\n        isLight = false\n    )\n\n    val black = MyColors(\n        id = \"black\",\n        name = \"Black\",\n        primary = Color(0xFF4791BF),\n        primaryVariant = Color(0xFF60A6D9),\n        onPrimary = Color(0xFFEFF2F6),\n        secondary = Color(0xFFB85DFF),\n        secondaryVariant = Color(0xFFD1A6FF),\n        onSecondary = Color(0xFFEFF2F6),\n        background = Color(0xFF000000),\n        onBackground = Color(0xFFEFEFEF),\n        surface = Color(0xFF1A1F26),\n        onSurface = Color(0xFFEFEFEF),\n        error = Color(0xFFEA4C3C),\n        onError = Color(0xFFFFFFFF),\n        success = Color(0xFF45B36B),\n        onSuccess = Color(0xFFFFFFFF),\n        warning = Color(0xFFF6C244),\n        onWarning = Color(0xFF000000),\n        info = Color(0xFF40A9F3),\n        onInfo = Color(0xFF000000),\n        isLight = false\n    )\n\n    val lightGray = MyColors(\n        id = \"light_gray\",\n        name = \"Light Gray\",\n        primary = Color(0xFF4791BF),\n        primaryVariant = Color(0xFF60A6D9),\n        onPrimary = Color(0xFF20303A),\n        secondary = Color(0xFFB85DFF),\n        secondaryVariant = Color(0xFFD1A6FF),\n        onSecondary = Color(0xFF20303A),\n        background = Color(0xFFF0F0F0),\n        onBackground = Color(0xFF232323),\n        surface = Color(0xFFE0E0E0),\n        onSurface = Color(0xFF232323),\n        error = Color(0xFFEA4C3C),\n        onError = Color(0xFFFFFFFF),\n        success = Color(0xFF45B36B),\n        onSuccess = Color(0xFFFFFFFF),\n        warning = Color(0xFFF6C244),\n        onWarning = Color(0xFF232323),\n        info = Color(0xFF40A9F3),\n        onInfo = Color(0xFF232323),\n        isLight = true\n    )\n\n\n    fun getAll(): List<MyColors> {\n        return listOf(\n            dark,\n            light,\n            obsidian,\n            deepOcean,\n            black,\n            lightGray,\n        )\n    }\n\n    fun getDefaultDark() = dark\n    fun getDefaultLight() = light\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/Markdown.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextDecoration\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.LocalTextStyle\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.mikepenz.markdown.model.DefaultMarkdownColors\nimport com.mikepenz.markdown.model.DefaultMarkdownTypography\n\n@Composable\nfun myMarkdownColors(): DefaultMarkdownColors {\n    val currentColor = LocalContentColor.current\n    return DefaultMarkdownColors(\n        text = currentColor,\n        codeBackground = myColors.surface,\n        dividerColor = currentColor.copy(alpha = 0.1f),\n        inlineCodeBackground = myColors.surface,\n        tableBackground = Color.Transparent,\n    )\n}\n\n@Composable\nfun myMarkdownTypography(): DefaultMarkdownTypography {\n    val defaultTextStyle = LocalTextStyle.current\n    val textSizes = myTextSizes\n    val colors = myColors\n    return DefaultMarkdownTypography(\n        h1 = defaultTextStyle.copy(\n            fontSize = textSizes.xl * 1.1f,\n            fontWeight = FontWeight.Bold,\n        ),\n        h2 = defaultTextStyle.copy(\n            fontSize = textSizes.xl,\n            fontWeight = FontWeight.Bold,\n        ),\n        h3 = defaultTextStyle.copy(\n            fontSize = textSizes.lg,\n            fontWeight = FontWeight.Bold,\n        ),\n        h4 = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n            fontWeight = FontWeight.Bold,\n        ),\n        h5 = defaultTextStyle.copy(\n            fontSize = textSizes.sm,\n            fontWeight = FontWeight.Bold,\n        ),\n        h6 = defaultTextStyle.copy(\n            fontSize = textSizes.xs,\n            fontWeight = FontWeight.Bold,\n        ),\n        text = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n            fontWeight = FontWeight.Bold,\n        ),\n        code = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n            fontWeight = FontWeight.Normal,\n            fontFamily = FontFamily.Monospace,\n        ),\n        inlineCode = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n            fontWeight = FontWeight.Normal,\n            fontFamily = FontFamily.Monospace,\n        ),\n        quote = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n        ),\n        paragraph = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n        ),\n        ordered = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n        ),\n        bullet = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n        ),\n        list = defaultTextStyle.copy(\n            fontSize = textSizes.base,\n            fontWeight = FontWeight.Normal,\n        ),\n        textLink = TextLinkStyles(\n            style = defaultTextStyle.copy(\n                fontSize = textSizes.base,\n                color = colors.info,\n            ).toSpanStyle(),\n            hoveredStyle = defaultTextStyle.copy(\n                fontSize = textSizes.base,\n                color = colors.info,\n                textDecoration = TextDecoration.Underline\n            ).toSpanStyle()\n        ),\n        table = defaultTextStyle,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/PlatformThemeDefinitions.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.runtime.Composable\nimport com.abdownloadmanager.shared.util.ui.theme.MyShapes\nimport com.abdownloadmanager.shared.util.ui.theme.MySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.TextSizes\nimport io.github.oikvpqya.compose.fastscroller.ScrollbarStyle\n\n@Composable\nexpect fun myPlatformScrollbarStyle(): ScrollbarStyle\n\n@Composable\nexpect fun myPlatformTextSizes(): TextSizes\n\n@Composable\nexpect fun myPlatformShapes(): MyShapes\n\n@Composable\nexpect fun myPlatformSpacing(): MySpacings\n\n@Composable\nexpect fun PlatformDependentProviders(content: @Composable () -> Unit)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/ThemeManager.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.runtime.Stable\nimport androidx.compose.ui.graphics.Color\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\nimport com.abdownloadmanager.shared.util.ui.MyColors\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.flow.combineStateFlows\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.guardedEntry\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlin.collections.filter\nimport kotlin.collections.map\n\nclass ThemeManager(\n    private val scope: CoroutineScope,\n    private val appSettings: ThemeSettingsStorage,\n    private val osThemeDetector: ISystemThemeDetector,\n) {\n    companion object {\n        val defaultThemes = DefaultThemes.getAll()\n        val DefaultDarkTheme = DefaultThemes.getDefaultDark()\n        val DefaultLightTheme = DefaultThemes.getDefaultLight()\n        val DefaultTheme = DefaultDarkTheme\n        val DEFAULT_THEME_ID = DefaultTheme.id\n        val systemThemeInfo = ThemeInfo(\n            id = \"system\",\n            name = Res.string.system.asStringSource(),\n            color = Color.Gray,\n        )\n    }\n\n    private val _availableThemes = MutableStateFlow(emptyList<MyColors>())\n    val availableThemes = _availableThemes.asStateFlow()\n\n    private fun getThemeById(themeId: String): MyColors? {\n        return availableThemes.value.find {\n            it.id == themeId\n        }\n    }\n\n    val selectableThemes = availableThemes.mapStateFlow {\n        buildList {\n            if (osThemeDetector.isSupported) {\n                add(systemThemeInfo)\n            }\n            addAll(it.map {\n                it.toThemeInfo()\n            })\n        }\n    }\n\n    val selectableDarkThemes = availableThemes.mapStateFlow {\n        it.filter { !it.isLight }.map { it.toThemeInfo() }\n    }\n\n    val selectableLightThemes = availableThemes.mapStateFlow {\n        it.filter { it.isLight }.map { it.toThemeInfo() }\n    }\n\n    private val themeIds = selectableThemes.mapStateFlow {\n        it.map { it.id }\n    }\n\n\n    val currentThemeInfo = combineStateFlows(\n        appSettings.theme, selectableThemes\n    ) { themeId, possibleThemes ->\n        possibleThemes.find {\n            it.id == themeId\n        } ?: possibleThemes.find {\n            it.id == DEFAULT_THEME_ID\n        }!!\n    }\n\n    val selectedDarkThemeInfo = combineStateFlows(\n        appSettings.defaultDarkTheme, selectableThemes\n    ) { themeId, possibleThemes ->\n        possibleThemes.find {\n            it.id == themeId\n        } ?: possibleThemes.find {\n            it.id == DefaultDarkTheme.id\n        }!!\n    }\n\n    val selectedLightThemeInfo = combineStateFlows(\n        appSettings.defaultLightTheme, selectableThemes\n    ) { themeId, possibleThemes ->\n        possibleThemes.find {\n            it.id == themeId\n        } ?: possibleThemes.find {\n            it.id == DefaultLightTheme.id\n        }!!\n    }\n\n\n    private var osDarkModeFlow = MutableStateFlow(true)\n\n    val currentThemeColor = combineStateFlows(\n        themeIds,\n        appSettings.theme,\n        appSettings.defaultDarkTheme,\n        appSettings.defaultLightTheme,\n        osDarkModeFlow,\n    ) { themes, themeId, userDefaultDarkThemeId, userDefaultLightThemeId, osThemeIsDark ->\n        val id = if (themeId == systemThemeInfo.id) {\n            if (osThemeIsDark) {\n                userDefaultDarkThemeId\n            } else {\n                userDefaultLightThemeId\n            }\n        } else {\n            themeId\n        }\n        if (themes.contains(id)) {\n            getThemeById(id)!!\n        } else {\n            DefaultTheme\n        }\n    }\n\n    fun setTheme(themeId: String) {\n        synchronized(this) {\n            if (themeId == systemThemeInfo.id) {\n                registerSystemThemeDetector()\n            } else {\n                unRegisterSystemThemeDetector()\n            }\n            if (themeIds.value.contains(themeId)) {\n                appSettings.theme.value = themeId\n            } else {\n                // theme id in setting is invalid update it\n                appSettings.theme.value = DEFAULT_THEME_ID\n            }\n        }\n    }\n\n    fun setDarkTheme(themeId: String) {\n        synchronized(this) {\n            appSettings.defaultDarkTheme.value = if (themeIds.value.contains(themeId)) {\n                themeId\n            } else {\n                // theme id in setting is invalid update it\n                DefaultDarkTheme.id\n            }\n        }\n    }\n\n    fun setLightTheme(themeId: String) {\n        synchronized(this) {\n            appSettings.defaultLightTheme.value = if (themeIds.value.contains(themeId)) {\n                themeId\n            } else {\n                // theme id in setting is invalid update it\n                DefaultLightTheme.id\n            }\n        }\n    }\n\n    private var booted = guardedEntry()\n\n    fun boot() {\n        booted.action {\n            // now we can load custom themes here\n            // loadCustomThemes()\n            //\n            _availableThemes.update {\n                it.plus(defaultThemes)\n            }\n            setTheme(appSettings.theme.value)\n        }\n    }\n\n    private var osUpdateFlowJob: Job? = null\n    private fun registerSystemThemeDetector() {\n        osUpdateFlowJob?.cancel()\n        if (osThemeDetector.isSupported) {\n            // update immediately\n            osDarkModeFlow.value = osThemeDetector.isDark()\n            osUpdateFlowJob = osThemeDetector.systemThemeFlow.onEach { isDark ->\n                osDarkModeFlow.value = isDark\n            }.launchIn(scope)\n        }\n    }\n\n    private fun unRegisterSystemThemeDetector() {\n        osUpdateFlowJob?.cancel()\n        osUpdateFlowJob = null\n    }\n\n}\n\n/**\n * This is for demonstration purposes of a theme\n */\n@Stable\ndata class ThemeInfo(\n    val id: String,\n    val name: StringSource,\n    val color: Color,\n)\n\nprivate fun MyColors.toThemeInfo(): ThemeInfo {\n    return ThemeInfo(\n        id = id,\n        name = name.asStringSource(),\n        color = surface,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/theme/ThemeSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface ThemeSettingsStorage {\n    val theme: MutableStateFlow<String>\n    val defaultDarkTheme: MutableStateFlow<String>\n    val defaultLightTheme: MutableStateFlow<String>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/ActionButton.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.LocalIndication\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.div\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.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.*\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.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\n\n@Composable\nfun ActionButton(\n    text: String,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n    backgroundColor: Brush = SolidColor(myColors.surface),\n    disabledBackgroundColor: Brush = SolidColor(myColors.surface / 0.5f),\n    borderColor: Brush = SolidColor(myColors.onBackground / 10),\n    focusedBorderColor: Brush = SolidColor(myColors.focusedBorderColor),\n    disabledBorderColor: Brush = SolidColor(myColors.onBackground / 10),\n    contentColor: Color = LocalContentColor.current,\n    contentPadding: PaddingValues = PaddingValues(vertical = 6.dp, horizontal = 24.dp),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    start: (@Composable RowScope.() -> Unit)? = null,\n    end: (@Composable RowScope.() -> Unit)? = null,\n) {\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    val shape = myShapes.defaultRounded\n    val borderColor = if (isFocused) focusedBorderColor else borderColor\n    Row(\n        modifier\n            .heightIn(mySpacings.thumbSize)\n            .border(1.dp, if (enabled) borderColor else disabledBorderColor, shape)\n            .clip(shape)\n            .background(if (enabled) backgroundColor else disabledBackgroundColor)\n            .clickable(\n                enabled = enabled,\n                interactionSource = interactionSource,\n                indication = LocalIndication.current,\n                role = Role.Button,\n                onClick = onClick,\n            )\n            .padding(contentPadding),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        WithContentColor(contentColor) {\n            WithContentAlpha(if (enabled) 1f else 0.5f) {\n                start?.let {\n                    it()\n                }\n                Text(\n                    text = text,\n                    modifier = Modifier,\n                    fontSize = myTextSizes.base,\n                    maxLines = 1,\n                    softWrap = false,\n                )\n                end?.let {\n                    it()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/ActionContainer.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\n\n@Composable\nfun ActionContainer(\n    modifier: Modifier,\n    contentPadding: PaddingValues = PaddingValues(\n        horizontal = 16.dp,\n        vertical = 8.dp,\n    ),\n    content: @Composable () -> Unit,\n) {\n    Column(modifier) {\n        Spacer(\n            Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onBackground / 0.15f)\n        )\n        Box(\n            Modifier\n                .fillMaxWidth()\n                .background(myColors.surface / 0.5f)\n                .padding(contentPadding),\n        ) {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/AddUrlButton.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\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.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Composable\nfun AddUrlButton(\n    modifier: Modifier=Modifier,\n    onClick:()->Unit\n) {\n    val shape = myShapes.defaultRounded\n    val addUrlIcon = MyIcons.link\n    val downloadIcon = MyIcons.download\n    Row(\n        modifier\n            .clip(shape)\n            .background(myColors.surface)\n            .clickable(onClick = onClick)\n            .height(32.dp)\n//            .width(120.dp)\n            .padding(horizontal = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n\n        ) {\n        WithContentAlpha(1f) {\n            MyIcon(addUrlIcon, null, Modifier.size(16.dp))\n            Spacer(Modifier.width(10.dp))\n            Text(\n                myStringResource(Res.string.new_download),\n                Modifier,\n                maxLines = 1,\n                fontSize = myTextSizes.sm,\n            )\n        }\n        Spacer(Modifier.width(10.dp))\n        Box(\n            Modifier\n                .clip(myShapes.defaultRounded)\n                .background(\n                    myColors.primaryGradient\n                ).padding(4.dp)\n        ) {\n            MyIcon(\n                downloadIcon,\n                null,\n                Modifier.size(12.dp),\n                tint = myColors.onPrimaryGradient,\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/CheckBox.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.selection.triStateToggleable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.state.ToggleableState\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun CheckBox(\n    value: Boolean,\n    onValueChange: (Boolean) -> Unit,\n    enabled: Boolean = true,\n    modifier: Modifier = Modifier,\n    size: Dp = 18.dp,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    uncheckedAlpha: Float = 0.25f,\n    shape: Shape = RoundedCornerShape(25)\n) {\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    Box(\n        modifier\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .size(size)\n            .clip(shape)\n            .triStateToggleable(\n                state = ToggleableState(value),\n                enabled = enabled,\n                role = Role.Checkbox,\n                interactionSource = interactionSource,\n                indication = null,\n                onClick = { onValueChange(!value) },\n            )\n    ) {\n        val borderColor = if (isFocused) {\n            myColors.focusedBorderColor\n        } else {\n            LocalContentColor.current / uncheckedAlpha\n        }\n        Spacer(\n            Modifier.matchParentSize()\n                .border(1.dp, borderColor, shape)\n        )\n        AnimatedContent(\n            value,\n            transitionSpec = {\n                val tween = tween<Float>(220)\n                fadeIn(tween) togetherWith fadeOut(tween)\n            }\n        ) {\n            val m = Modifier\n                .fillMaxSize()\n                .alpha(animateFloatAsState(if (value) 1f else 0f).value)\n                .background(myColors.primaryGradient)\n            if (it) {\n                MyIcon(\n                    MyIcons.check,\n                    contentDescription = null,\n                    modifier = m,\n                    tint = myColors.onPrimaryGradient,\n                )\n            } else {\n                Spacer(m)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/DashedBorder.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.drawWithCache\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.geometry.isSimple\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.drawscope.clipRect\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.platform.debugInspectorInfo\nimport androidx.compose.ui.unit.Dp\n\n/**\n * Modify element to add border with appearance specified with a [border] and a [shape], pad the\n * content by the [BorderStroke.width] and clip it.\n *\n * @sample androidx.compose.foundation.samples.BorderSample()\n *\n * @param border [BorderStroke] class that specifies border appearance, such as size and color\n * @param shape shape of the border\n */\nfun Modifier.dashedBorder(border: BorderStroke, shape: Shape = RectangleShape, on: Dp, off: Dp) =\n    dashedBorder(width = border.width, brush = border.brush, shape = shape, on, off)\n\n/**\n * Returns a [Modifier] that adds border with appearance specified with [width], [color] and a\n * [shape], pads the content by the [width] and clips it.\n *\n * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass()\n *\n * @param width width of the border. Use [Dp.Hairline] for a hairline border.\n * @param color color to paint the border with\n * @param shape shape of the border\n * @param on the size of the solid part of the dashes\n * @param off the size of the space between dashes\n */\nfun Modifier.dashedBorder(width: Dp, color: Color, shape: Shape = RectangleShape, on: Dp, off: Dp) =\n    dashedBorder(width, SolidColor(color), shape, on, off)\n\n/**\n * Returns a [Modifier] that adds border with appearance specified with [width], [brush] and a\n * [shape], pads the content by the [width] and clips it.\n *\n * @sample androidx.compose.foundation.samples.BorderSampleWithBrush()\n *\n * @param width width of the border. Use [Dp.Hairline] for a hairline border.\n * @param brush brush to paint the border with\n * @param shape shape of the border\n */\nfun Modifier.dashedBorder(width: Dp, brush: Brush, shape: Shape, on: Dp, off: Dp): Modifier =\n    composed(\n        factory = {\n            this.then(\n                Modifier.drawWithCache {\n                    val outline: Outline = shape.createOutline(size, layoutDirection, this)\n                    val borderSize = if (width == Dp.Hairline) 1f else width.toPx()\n\n                    var insetOutline: Outline? = null // outline used for roundrect/generic shapes\n                    var stroke: Stroke? = null // stroke to draw border for all outline types\n                    var pathClip: Path? = null // path to clip roundrect/generic shapes\n                    var inset = 0f // inset to translate before drawing the inset outline\n                    // path to draw generic shapes or roundrects with different corner radii\n                    var insetPath: Path? = null\n                    if (borderSize > 0 && size.minDimension > 0f) {\n                        if (outline is Outline.Rectangle) {\n                            stroke = Stroke(\n                                borderSize, pathEffect = PathEffect.dashPathEffect(\n                                    floatArrayOf(on.toPx(), off.toPx())\n                                )\n                            )\n                        } else {\n                            // Multiplier to apply to the border size to get a stroke width that is\n                            // large enough to cover the corners while not being too large to overly\n                            // square off the internal shape. The resultant shape will be\n                            // clipped to the desired shape. Any value lower will show artifacts in\n                            // the corners of shapes. A value too large will always square off\n                            // the internal shape corners. For example, for a rounded rect border\n                            // a large multiplier will always have squared off edges within the\n                            // inner section of the stroke, however, having a smaller multiplier\n                            // will still keep the rounded effect for the inner section of the\n                            // border\n                            val strokeWidth = 1.2f * borderSize\n                            inset = borderSize - strokeWidth / 2\n                            val insetSize = Size(\n                                size.width - inset * 2,\n                                size.height - inset * 2\n                            )\n                            insetOutline = shape.createOutline(insetSize, layoutDirection, this)\n                            stroke = Stroke(\n                                strokeWidth, pathEffect = PathEffect.dashPathEffect(\n                                    floatArrayOf(on.toPx(), off.toPx())\n                                )\n                            )\n                            pathClip = if (outline is Outline.Rounded) {\n                                Path().apply { addRoundRect(outline.roundRect) }\n                            } else if (outline is Outline.Generic) {\n                                outline.path\n                            } else {\n                                // should not get here because we check for Outline.Rectangle\n                                // above\n                                null\n                            }\n\n                            insetPath =\n                                if (insetOutline is Outline.Rounded &&\n                                    !insetOutline.roundRect.isSimple\n                                ) {\n                                    // Rounded rect with non equal corner radii needs a path\n                                    // to be pre-translated\n                                    Path().apply {\n                                        addRoundRect(insetOutline.roundRect)\n                                        translate(Offset(inset, inset))\n                                    }\n                                } else if (insetOutline is Outline.Generic) {\n                                    // Generic paths must be created and pre-translated\n                                    Path().apply {\n                                        addPath(insetOutline.path, Offset(inset, inset))\n                                    }\n                                } else {\n                                    // Drawing a round rect with equal corner radii without\n                                    // usage of a path\n                                    null\n                                }\n                        }\n                    }\n\n                    onDrawWithContent {\n                        drawContent()\n                        // Only draw the border if a have a valid stroke parameter. If we have\n                        // an invalid border size we will just draw the content\n                        if (stroke != null) {\n                            if (insetOutline != null && pathClip != null) {\n                                val isSimpleRoundRect = insetOutline is Outline.Rounded &&\n                                        insetOutline.roundRect.isSimple\n                                withTransform({\n                                    clipPath(pathClip)\n                                    // we are drawing the round rect not as a path so we must\n                                    // translate ourselves othe\n                                    if (isSimpleRoundRect) {\n                                        translate(inset, inset)\n                                    }\n                                }) {\n                                    if (isSimpleRoundRect) {\n                                        // If we don't have an insetPath then we are drawing\n                                        // a simple round rect with the corner radii all identical\n                                        val rrect = (insetOutline as Outline.Rounded).roundRect\n                                        drawRoundRect(\n                                            brush = brush,\n                                            topLeft = Offset(rrect.left, rrect.top),\n                                            size = Size(rrect.width, rrect.height),\n                                            cornerRadius = rrect.topLeftCornerRadius,\n                                            style = stroke,\n                                            alpha = 0f,\n\n                                        )\n                                    } else if (insetPath != null) {\n                                        drawPath(\n                                            path = insetPath,\n                                            brush = brush,\n                                            style = stroke,\n                                            alpha = 0f,\n                                        )\n                                    }\n                                }\n                                // Clip rect to ensure the stroke does not extend the bounds\n                                // of the composable.\n                                clipRect {\n                                    // Draw a hairline stroke to cover up non-anti-aliased pixels\n                                    // generated from the clip\n                                    if (isSimpleRoundRect) {\n                                        val rrect = (outline as Outline.Rounded).roundRect\n                                        drawRoundRect(\n                                            brush = brush,\n                                            topLeft = Offset(rrect.left, rrect.top),\n                                            size = Size(rrect.width, rrect.height),\n                                            cornerRadius = rrect.topLeftCornerRadius,\n                                            style = Stroke(\n                                                Stroke.HairlineWidth,\n                                                pathEffect = PathEffect.dashPathEffect(\n                                                    floatArrayOf(on.toPx(), off.toPx())\n                                                )\n                                            )\n                                        )\n                                    } else {\n                                        drawPath(\n                                            pathClip, brush = brush, style = Stroke(\n                                                Stroke.HairlineWidth,\n                                                pathEffect = PathEffect.dashPathEffect(\n                                                    floatArrayOf(on.toPx(), off.toPx())\n                                                )\n                                            )\n                                        )\n                                    }\n                                }\n                            } else {\n                                // Rectangular border fast path\n                                val strokeWidth = stroke.width\n                                val halfStrokeWidth = strokeWidth / 2\n                                drawRect(\n                                    brush = brush,\n                                    topLeft = Offset(halfStrokeWidth, halfStrokeWidth),\n                                    size = Size(\n                                        size.width - strokeWidth,\n                                        size.height - strokeWidth\n                                    ),\n                                    style = stroke\n                                )\n                            }\n                        }\n                    }\n                }\n            )\n        },\n        inspectorInfo = debugInspectorInfo {\n            name = \"border\"\n            properties[\"width\"] = width\n            if (brush is SolidColor) {\n                properties[\"color\"] = brush.value\n                value = brush.value\n            } else {\n                properties[\"brush\"] = brush\n            }\n            properties[\"shape\"] = shape\n        }\n    )\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/ExpandableItem.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun ExpandableItem(\n    isExpanded:Boolean,\n    header:@Composable ()->Unit,\n    body: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n){\n    Column(modifier) {\n        header()\n        AnimatedVisibility(isExpanded){\n            body()\n        }\n    }\n}"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Handle.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.draggable\nimport androidx.compose.foundation.gestures.rememberDraggableState\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsDraggedAsState\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport com.abdownloadmanager.shared.ui.modifier.MyPointerHoverIcon\nimport com.abdownloadmanager.shared.ui.modifier.myPointerHoverIcon\n\n@Composable\nfun Handle(\n    modifier: Modifier,\n    orientation: Orientation = Orientation.Horizontal,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    color: Color = myColors.surface,\n    inactiveColor: Color = myColors.surface / 50,\n    onDrag: (Dp) -> Unit,\n) {\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    val isDragging by interactionSource.collectIsDraggedAsState()\n\n    val hoverIcon = when (orientation) {\n        Orientation.Vertical -> MyPointerHoverIcon.VerticalResize\n        Orientation.Horizontal -> MyPointerHoverIcon.HorizontalResize\n    }\n\n    Spacer(\n        modifier\n            .myPointerHoverIcon(hoverIcon, true)\n            .hoverable(interactionSource)\n            .resizeHandle(\n                orientation = orientation,\n                interactionSource = interactionSource,\n                onDrag = onDrag,\n            )\n            .background(\n                animateColorAsState(\n                    if (isHovered || isDragging) color\n                    else inactiveColor\n                ).value\n            )\n    )\n}\n\nfun Modifier.resizeHandle(\n    orientation: Orientation = Orientation.Horizontal,\n    interactionSource: MutableInteractionSource? = null,\n    onDrag: (Dp) -> Unit,\n) = composed {\n    val latestOnDrag by rememberUpdatedState(onDrag)\n    val density = LocalDensity.current\n    val draggableState = rememberDraggableState {\n        density.run {\n            latestOnDrag(it.toDp())\n        }\n    }\n    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl\n    val reverseDirection = orientation == Orientation.Horizontal && isRtl\n    draggable(\n        state = draggableState,\n        orientation = orientation,\n        interactionSource = interactionSource,\n        reverseDirection = reverseDirection\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Help.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\n\n@Composable\nfun Help(\n    content: String,\n    modifier: Modifier = Modifier,\n) {\n    var showHelpContent by remember { mutableStateOf(false) }\n    val onRequestCloseShowHelpContent = {\n        showHelpContent = false\n    }\n    Column(modifier) {\n        MyIcon(\n            MyIcons.question,\n            \"Hint\",\n            Modifier\n                .clip(CircleShape)\n                .clickable {\n                    showHelpContent = !showHelpContent\n                }\n                .border(\n                    1.dp,\n                    if (showHelpContent) myColors.primary\n                    else Color.Transparent,\n                    CircleShape\n                )\n                .background(myColors.surface)\n                .padding(4.dp)\n                .size(12.dp),\n            tint = myColors.onSurface,\n        )\n        if (showHelpContent) {\n            TooltipPopup(\n                onRequestCloseShowHelpContent = onRequestCloseShowHelpContent,\n                content = content,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/IconPick.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\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.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MyDropDown\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\n\n@Composable\nfun IconPick(\n    selectedIcon: IconSource?,\n    icons: List<IconSource>,\n    onSelected: (IconSource) -> Unit,\n    onCancel: () -> Unit,\n) {\n    MyDropDown(\n        onDismissRequest = onCancel,\n        offset = DpOffset(y = 2.dp, x = 0.dp),\n        content = {\n            val shape = myShapes.defaultRounded\n            Box(\n                Modifier\n                    .shadow(24.dp)\n//                .verticalScroll(rememberScrollState())\n                    .clip(shape)\n//                    .width(IntrinsicSize.Max)\n                    .widthIn(120.dp)\n                    .height(220.dp)\n                    .border(1.dp, myColors.surface, shape)\n                    .background(myColors.menuGradientBackground)\n\n            ) {\n                Content(\n                    modifier = Modifier.padding(horizontal = 8.dp, vertical = 0.dp),\n                    selectedIcon = selectedIcon,\n                    icons = icons,\n                    onSelected = onSelected,\n                )\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun Content(\n    modifier: Modifier,\n    selectedIcon: IconSource?,\n    icons: List<IconSource>,\n    onSelected: (IconSource) -> Unit,\n) {\n    val state = rememberLazyListState()\n    val shape = myShapes.defaultRounded\n    Box {\n        LazyColumn(\n            modifier = modifier,\n            state = state,\n            contentPadding = PaddingValues(vertical = 8.dp),\n            content = {\n                items(icons.chunked(6)) { rowItems ->\n                    Row {\n                        for (iconSource in rowItems) {\n                            val isSelected = selectedIcon == iconSource\n                            MyIcon(\n                                iconSource,\n                                null,\n                                Modifier\n                                    .clip(shape)\n                                    .ifThen(isSelected) {\n                                        background(myColors.primary / 0.25f)\n                                    }\n                                    .border(\n                                        1.dp,\n                                        if (isSelected) myColors.primary / 0.25f\n                                        else Color.Transparent,\n                                        shape\n                                    )\n                                    .clickable {\n                                        onSelected(iconSource)\n                                    }\n                                    .padding(8.dp)\n                                    .size(24.dp),\n                            )\n                        }\n                    }\n                }\n//    LazyVerticalGrid(\n//        columns = GridCells.Fixed(6),\n//        content = {\n//            val shape = myShapes.defaultRounded\n//            items(icons) {\n//                MyIcon(\n//                    it,\n//                    null,\n//                    Modifier\n//                        .clip(shape)\n//                        .ifThen(selectedIcon == it) {\n//                            background(myColors.primary / 0.25f)\n//                        }\n//                        .clickable {\n//                            onSelected(it)\n//                        }\n//                        .padding(8.dp)\n//                        .size(24.dp),\n//                )\n//            }\n//        }\n//    )\n            }\n        )\n        AnimatedVisibility(\n            state.canScrollForward,\n            modifier = Modifier.matchParentSize(),\n            enter = fadeIn(),\n            exit = fadeOut(),\n        ) {\n            Spacer(\n                Modifier\n                    .fillMaxSize()\n                    .background(\n                        Brush.verticalGradient(\n                            colorStops = arrayOf(\n                                0f to Color.Transparent,\n                                0.8f to Color.Transparent,\n                                1f to myColors.background,\n                            )\n                        )\n                    )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Language.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.LayoutDirection\nimport com.abdownloadmanager.shared.util.ui.LocalTitleBarDirection\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.compose.localizationmanager.LocalLanguageManager\nimport ir.amirab.util.compose.localizationmanager.LocaleLanguageDirection\n\n@Composable\nfun ProvideLanguageManager(\n    languageManager: LanguageManager,\n    content: @Composable () -> Unit,\n) {\n    val isRtl = languageManager.isRtl.collectAsState().value\n    val languageDirection = if (isRtl) {\n        LayoutDirection.Rtl\n    } else {\n        LayoutDirection.Ltr\n    }\n    CompositionLocalProvider(\n        LocalLanguageManager provides languageManager,\n        LocalLayoutDirection provides languageDirection,\n        LocalTitleBarDirection provides LayoutDirection.Ltr,\n        LocaleLanguageDirection provides languageDirection\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/LinkText.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.LocalTextStyle\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.ifThen\n\n\n@Composable\nfun LinkText(\n    text: String,\n    link: String,\n    modifier: Modifier = Modifier,\n    maxLines: Int = Int.MAX_VALUE,\n    showExternalIndicator: Boolean = true,\n    overflow: TextOverflow = TextOverflow.Clip,\n) {\n    val handler = LocalUriHandler.current\n    val interactionSource = remember { MutableInteractionSource() }\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    Row(\n        modifier\n            .pointerHoverIcon(PointerIcon.Hand)\n            .hoverable(interactionSource)\n            .clickable(\n                interactionSource = interactionSource,\n                indication = null\n            ) {\n                handler.openUri(link)\n            }\n    ) {\n        Text(\n            text = text,\n            style = LocalTextStyle.current\n                .merge(LinkStyle).ifThen(isHovered) {\n                    copy(\n                        textDecoration = TextDecoration.Underline\n                    )\n                },\n            overflow = overflow,\n            maxLines = maxLines,\n        )\n        if (showExternalIndicator) {\n            MyIcon(\n                MyIcons.externalLink,\n                null,\n                Modifier.size(10.dp).alpha(\n                    if (isHovered) 0.75f\n                    else 0.5f\n                )\n            )\n        }\n    }\n}\n\n@Composable\nfun MaybeLinkText(\n    text: String,\n    link: String?,\n    modifier: Modifier = Modifier,\n    overflow: TextOverflow = TextOverflow.Clip,\n    maxLines: Int = Int.MAX_VALUE,\n) {\n    val link = remember(link) {\n        link?.takeIf(HttpUrlUtils::isValidUrl)\n    }\n    if (link == null) {\n        Text(\n            modifier = modifier,\n            text = text,\n            maxLines = maxLines,\n            overflow = overflow,\n        )\n    } else {\n        LinkText(\n            modifier = modifier,\n            text = text,\n            link = link,\n            maxLines = maxLines,\n            overflow = overflow\n        )\n    }\n}\n\nprivate val LinkStyle: TextStyle\n    @Composable\n    get() = TextStyle(\n        color = myColors.info,\n    )\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/LoadingIndicator.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport androidx.annotation.FloatRange\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.progressSemantics\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun LoadingIndicator(\n    modifier: Modifier,\n    sweepAngle: Float = 90f, // angle (length) of indicator arc\n    color: Color = myColors.primary, // color of indicator arc line\n    strokeWidth: Dp = 4.dp,\n) {\n    val transition = rememberInfiniteTransition()\n\n    // define the changing value from 0 to 360.\n    // This is the angle of the beginning of indicator arc\n    // this value will change over time from 0 to 360 and repeat indefinitely.\n    // it changes starting position of the indicator arc and the animation is obtained\n    val currentArcStartAngle by transition.animateValue(\n        0,\n        360,\n        Int.VectorConverter,\n        infiniteRepeatable(\n            animation = tween(\n                durationMillis = 1100,\n                easing = LinearEasing\n            )\n        )\n    )\n\n    IndicatorCanvas(\n        modifier = modifier,\n        currentArcStartAngle = currentArcStartAngle,\n        strokeWidth = strokeWidth,\n        color = SolidColor(color),\n        sweepAngle = sweepAngle,\n    )\n}\n@Composable\nfun LoadingIndicator(\n    modifier: Modifier,\n    color: Color = myColors.primary, // color of indicator arc line\n    strokeWidth: Dp = 4.dp,\n    @FloatRange(0.0,1.0)\n    progress:Float\n) {\n    IndicatorCanvas(\n        modifier = modifier,\n        currentArcStartAngle = 0,\n        sweepAngle = (progress * 360).coerceIn(0f, 360f),\n        strokeWidth = strokeWidth,\n        color = SolidColor(color),\n    )\n}\n\n@Composable\nfun LoadingIndicatorWithBrush(\n    modifier: Modifier,\n    brush: Brush = SolidColor(myColors.primary), // color of indicator arc line\n    strokeWidth: Dp = 4.dp,\n    @FloatRange(0.0, 1.0)\n    progress: Float\n) {\n    IndicatorCanvas(\n        modifier = modifier,\n        currentArcStartAngle = 0,\n        sweepAngle = (progress*360).coerceIn(0f,360f),\n        strokeWidth = strokeWidth,\n        color = brush,\n    )\n}\n@Composable\nfun IndicatorCanvas(\n    modifier: Modifier,\n    currentArcStartAngle: Int,\n    sweepAngle:Float,\n    strokeWidth: Dp,\n    color: Brush,\n) {\n    // define stroke with given width and arc ends type considering device DPI\n    val stroke = with(LocalDensity.current) {\n        Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)\n    }\n    // draw on canvas\n    Canvas(\n        modifier\n            .progressSemantics() // (optional) for Accessibility services\n            .padding(strokeWidth / 2) //padding. otherwise, not the whole circle will fit in the canvas\n    ) {\n        // draw arc with the same stroke\n        drawArc(\n            color,\n            // arc start angle\n            // -90 shifts the start position towards the y-axis\n            startAngle = currentArcStartAngle.toFloat() - 90,\n            sweepAngle = sweepAngle,\n            useCenter = false,\n            style = stroke\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/MainActionButton.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\n\n\n@Composable\nfun PrimaryMainActionButton(\n    text: String,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    val backgroundColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 30\n        }\n    )\n    val borderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors\n    )\n    val disabledBorderColor = Brush.horizontalGradient(\n        myColors.primaryGradientColors.map {\n            it / 50\n        }\n    )\n    ActionButton(\n        text = text,\n        modifier = modifier,\n        enabled = enabled,\n        onClick = onClick,\n        backgroundColor = backgroundColor,\n        disabledBackgroundColor = backgroundColor,\n        borderColor = borderColor,\n        disabledBorderColor = disabledBorderColor,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/MessageDialogType.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\n@Suppress(\"unused\")\nsealed class MessageDialogType {\n    data object Success : MessageDialogType()\n    data object Info : MessageDialogType()\n    data object Error : MessageDialogType()\n    data object Warning : MessageDialogType()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Multiselect.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\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.graphics.Shape\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\n\n@Composable\nfun <T> Multiselect(\n    selections: List<T>,\n    selectedItem: T,\n    onSelectionChange: (T) -> Unit,\n    modifier: Modifier = Modifier,\n    shape: Shape = myShapes.defaultRounded,\n    backgroundColour: Color = myColors.surface,\n    selectedColor: Color = LocalContentColor.current / 10,\n    unselectedAlpha: Float = 0.5f,\n    render: @Composable (T) -> Unit,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .clip(shape)\n            .background(backgroundColour)\n    ) {\n        for (item in selections) {\n            val isSelected = item == selectedItem\n            Box(\n                Modifier\n                    .padding(vertical = 4.dp, horizontal = 4.dp)\n                    .heightIn(mySpacings.thumbSize)\n                    .clip(shape)\n                    .ifThen(isSelected) {\n                        background(selectedColor)\n                    }\n                    .clickable {\n                        onSelectionChange(item)\n                    }\n                    .padding(vertical = 2.dp, horizontal = 4.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                WithContentAlpha(\n                    if (isSelected) {\n                        1f\n                    } else {\n                        unselectedAlpha\n                    }\n                ) {\n                    render(item)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/MyIconButton.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.LocalIndication\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.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.sizeIn\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.modifiers.autoMirror\n\n@Composable\nfun alphaFlicker(): Float {\n    val t = rememberInfiniteTransition()\n    return t.animateFloat(1f, 0f, infiniteRepeatable(tween(1000), repeatMode = RepeatMode.Reverse)).value\n}\n\n@Composable\nfun IconActionButton(\n    icon: IconSource,\n    contentDescription: StringSource,\n    modifier: Modifier = Modifier,\n    indicateActive: Boolean = false,\n    requiresAttention: Boolean = false,\n    enabled: Boolean = true,\n    shape: Shape = myShapes.defaultRounded,\n    backgroundColor: Color = myColors.surface,\n    contentColor: Color = LocalContentColor.current,\n    borderColor: Color = myColors.onBackground / 10,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    automaticMirrorIcon: Boolean = true,\n    iconSize: Dp = mySpacings.iconSize,\n    onClick: () -> Unit,\n) {\n    Tooltip(contentDescription) {\n        WithContentColor(contentColor) {\n            val isFocused by interactionSource.collectIsFocusedAsState()\n            val isActiveOrFocused = indicateActive || isFocused\n            Box(\n                modifier\n                    .sizeIn(mySpacings.thumbSize, mySpacings.thumbSize)\n                    .ifThen(!enabled) {\n                        alpha(0.5f)\n                    }\n                    .border(\n                        1.dp,\n                        borderColor,\n                        shape\n                    )\n                    .ifThen(isActiveOrFocused || requiresAttention) {\n                        border(\n                            1.dp,\n                            myColors.focusedBorderColor / if (isActiveOrFocused) 1f else alphaFlicker(),\n                            shape\n                        )\n                    }\n                    .clip(shape)\n                    .background(backgroundColor)\n                    .clickable(\n                        enabled = enabled,\n                        indication = LocalIndication.current,\n                        interactionSource = interactionSource,\n                        role = Role.Button,\n                        onClick = onClick,\n                    )\n                    .padding(6.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                MyIcon(\n                    icon,\n                    contentDescription.rememberString(),\n                    Modifier\n                        .ifThen(automaticMirrorIcon) {\n                            autoMirror()\n                        }\n                        .size(iconSize),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun TransparentIconActionButton(\n    icon: IconSource,\n    contentDescription: StringSource,\n    modifier: Modifier = Modifier,\n    indicateActive: Boolean = false,\n    requiresAttention: Boolean = false,\n    enabled: Boolean = true,\n    shape: Shape = myShapes.defaultRounded,\n    contentColor: Color = LocalContentColor.current,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    automaticMirrorIcon: Boolean = true,\n    iconSize: Dp = mySpacings.iconSize,\n    onClick: () -> Unit,\n) {\n    IconActionButton(\n        icon = icon,\n        contentDescription = contentDescription,\n        modifier = modifier,\n        indicateActive = indicateActive,\n        requiresAttention = requiresAttention,\n        enabled = enabled,\n        shape = shape,\n        backgroundColor = Color.Transparent,\n        contentColor = contentColor,\n        borderColor = Color.Transparent,\n        interactionSource = interactionSource,\n        automaticMirrorIcon = automaticMirrorIcon,\n        iconSize = iconSize,\n        onClick = onClick,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/MyTextField.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.LocalTextStyle\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.*\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.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\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.alpha\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.graphics.Shape\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.isSpecified\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.takeOrElse\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\n\n@Composable\nfun MyTextField(\n    text: String,\n    onTextChange: (String) -> Unit,\n    placeholder: String,\n    modifier: Modifier,\n    background: Color = myColors.surface,\n    contentColor: Color = myColors.getContentColorFor(background).takeIf { it.isSpecified }\n        ?: LocalContentColor.current,\n    focusedBorderColor: Color = myColors.primary,\n    borderColor: Color = myColors.onBackground / 0.1f,\n    shape: Shape = myShapes.defaultRounded,\n    textPadding: PaddingValues = PaddingValues(8.dp),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    fontSize: TextUnit = TextUnit.Unspecified,\n    enabled: Boolean = true,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    singleLine: Boolean = true,\n    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,\n    minLines: Int = 1,\n    start: @Composable (RowScope.() -> Unit)? = null,\n    end: @Composable (RowScope.() -> Unit)? = null,\n) {\n    val focusRequester = remember { FocusRequester() }\n    val fm = LocalFocusManager.current\n    val isFocused by interactionSource.collectIsFocusedAsState()\n\n    val textSize = fontSize.takeOrElse { LocalTextStyle.current.fontSize }\n    Row(\n        modifier\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .clip(shape)\n            .heightIn(mySpacings.thumbSize)\n            .height(IntrinsicSize.Max)\n//            .height(32.dp)\n            .pointerHoverIcon(\n                if (enabled) PointerIcon.Text\n                else PointerIcon.Default\n            )\n            .onKeyEvent {\n                if (it.key == Key.Escape) {\n                    fm.clearFocus()\n                    true\n                } else false\n            }\n            .clickable(\n                indication = null,\n                interactionSource = null,\n            ) {\n                focusRequester.requestFocus()\n            }\n            .border(\n                1.dp,\n                animateColorAsState(\n                    if (isFocused) focusedBorderColor\n                    else borderColor\n                ).value,\n                shape\n            )\n            .ifThen(background.isSpecified) {\n                background(background)\n            },\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        start?.let {\n            it()\n        }\n\n        BasicTextField(\n            value = text,\n            singleLine = singleLine,\n            maxLines = maxLines,\n            minLines = minLines,\n            onValueChange = onTextChange,\n            interactionSource = interactionSource,\n            enabled = enabled,\n            modifier = Modifier\n                .weight(1f)\n                .padding(textPadding)\n                .focusRequester(focusRequester),\n            textStyle = LocalTextStyle.current.merge(\n                TextStyle(\n                    color = LocalContentColor.current.ifThen(!enabled) {\n                        copy(0.5f)\n                    },\n                    fontSize = fontSize\n                )\n            ),\n            decorationBox = {\n                Box {\n                    androidx.compose.animation.AnimatedVisibility(\n                        text.isEmpty(),\n//                modifier = Modifier.matchParentSize(),\n                        enter = fadeIn(),\n                        exit = fadeOut(),\n                    ) {\n                        Text(\n                            text = placeholder,\n                            maxLines = 1,\n                            color = contentColor / 50,\n                            fontSize = textSize\n                        )\n                    }\n                    it()\n                }\n            },\n            cursorBrush = SolidColor(myColors.primary),\n            keyboardActions = keyboardActions,\n            keyboardOptions = keyboardOptions,\n        )\n        end?.let {\n            it()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/MyTextFieldWithIcons.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\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.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.ifThen\n\n@Composable\nfun MyTextFieldWithIcons(\n    text: String,\n    onTextChange: (String) -> Unit,\n    placeholder: String,\n    modifier: Modifier,\n    errorText: String? = null,\n    enabled: Boolean = true,\n    singleLine: Boolean = true,\n    start: @Composable (() -> Unit)? = null,\n    end: @Composable (() -> Unit)? = null,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    val dividerModifier = Modifier\n        .fillMaxHeight()\n        .padding(vertical = 1.dp)\n        //to not conflict with text-field border\n        .width(1.dp)\n        .background(if (isFocused) myColors.onBackground / 10 else Color.Transparent)\n    Column(modifier) {\n        MyTextField(\n            text = text,\n            onTextChange = onTextChange,\n            placeholder = placeholder,\n            modifier = Modifier.fillMaxWidth(),\n            background = myColors.surface / 50,\n            interactionSource = interactionSource,\n            shape = myShapes.defaultRounded,\n            singleLine = singleLine,\n            enabled = enabled,\n            start = start?.let {\n                {\n                    WithContentAlpha(0.5f) {\n                        it()\n                    }\n                    Spacer(dividerModifier)\n                }\n            },\n            end = end?.let {\n                {\n                    Spacer(dividerModifier)\n                    it()\n                }\n            }\n        )\n        AnimatedVisibility(errorText != null) {\n            if (errorText != null) {\n                Text(\n                    errorText,\n                    Modifier.padding(bottom = 4.dp, start = 4.dp),\n                    fontSize = myTextSizes.sm,\n                    color = myColors.error,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun MyTextFieldIcon(\n    icon: IconSource,\n    enabled: Boolean = true,\n    contentDescription: String? = null,\n    onClick: (() -> Unit)? = null,\n) {\n    MyIcon(\n        icon = icon,\n        contentDescription = contentDescription,\n        modifier = Modifier\n            .fillMaxHeight()\n            .ifThen(onClick != null) {\n                pointerHoverIcon(PointerIcon.Default)\n                    .clickable(enabled = enabled, onClick = onClick)\n            }\n            .wrapContentHeight()\n            .padding(horizontal = 8.dp)\n            .size(mySpacings.iconSize)\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/NavigateableItem.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\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\n\n@Composable\nfun NavigateableItem(\n    isSelected:Boolean,\n    onClick:()->Unit,\n    content:@Composable ()->Unit,\n){\n    val shape = RoundedCornerShape(12.dp)\n    WithContentAlpha(if (isSelected)1f else 0.75f){\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .clip(shape)\n                .let {\n                    if (isSelected) {\n                        val selectionColor = myColors.onBackground\n                        it\n                            .border(\n                                1.dp,\n                                myColors.selectionGradient(0.10f, 0.05f, selectionColor),\n                                shape\n                            )\n                            .background(myColors.selectionGradient(0.15f, 0f, selectionColor))\n                    } else it\n                }\n                .clickable {\n                    onClick()\n                }\n                .padding(8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Notification.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport java.util.UUID\n\nprivate val LocalNotification = compositionLocalOf<NotificationManager> {\n    error(\"LocalNotification not provided yet\")\n}\n\n@Composable\nfun useNotification(): NotificationManager {\n    return LocalNotification.current\n}\n\nsealed interface NotificationType {\n    data class Loading(val percent: Int? = null) : NotificationType\n    data object Success : NotificationType\n    data object Info : NotificationType\n    data object Error : NotificationType\n    data object Warning : NotificationType\n}\n\n@Stable\nclass NotificationModel(\n    val tag: Any,\n    initialTitle: StringSource = \"\".asStringSource(),\n    initialDescription: StringSource = \"\".asStringSource(),\n    initialNotificationType: NotificationType = NotificationType.Info,\n) {\n    var notificationType: NotificationType by mutableStateOf(initialNotificationType)\n    var title: StringSource by mutableStateOf(initialTitle)\n    var description: StringSource by mutableStateOf(initialDescription)\n}\n\n@Composable\nfun ProvideNotificationManager(\n    notificationManager: NotificationManager,\n    content: @Composable () -> Unit\n) {\n    CompositionLocalProvider(\n        LocalNotification provides notificationManager\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun ShowNotification(\n    title: StringSource,\n    description: StringSource,\n    type: NotificationType,\n    tag: Any = currentCompositeKeyHash,\n) {\n    val notification = remember(tag) {\n        NotificationModel(\n            tag = tag,\n            initialTitle = title,\n            initialDescription = description,\n        )\n    }\n    LaunchedEffect(type){\n        notification.notificationType=type\n    }\n    LaunchedEffect(title) {\n        notification.title = title\n    }\n    LaunchedEffect(description) {\n        notification.description = description\n    }\n    val notificationManager = useNotification()\n    LaunchedEffect(Unit) {\n        notificationManager.showNotification(notification)\n    }\n}\n\n@Composable\nfun NotificationArea(\n    modifier: Modifier\n) {\n    val notificationManager = useNotification()\n//    val list = notificationManager.activeNotificationList\n    val activeNotificationList by notificationManager.activeNotificationList.collectAsState()\n    val notificationListToShow by remember {\n        derivedStateOf {\n            activeNotificationList.distinctBy { it.tag }\n        }\n    }\n    LazyColumn (modifier) {\n        itemsIndexed(notificationListToShow) { index, item ->\n            Spacer(Modifier.size(12.dp))\n            RenderNotification(\n                Modifier.animateItem(),\n                item\n            )\n            Spacer(Modifier.size(12.dp))\n        }\n    }\n}\n\n@Composable\nprivate fun RenderNotification(\n    modifier: Modifier,\n    notificationModel: NotificationModel,\n    alignment: Alignment = Alignment.BottomCenter,\n) {\n    val shape = myShapes.defaultRounded\n    Row(modifier\n        .shadow(4.dp, shape)\n        .animateContentSize(alignment = alignment)\n        .height(IntrinsicSize.Max)\n        .fillMaxWidth()\n        .clip(shape)\n        .border(1.dp, myColors.menuBorderColor, shape)\n        .background(myColors.menuGradientBackground, shape)\n        .padding(10.dp)\n    ) {\n        NotificationIcon(\n            Modifier\n                .padding(horizontal = 2.dp)\n                .align(Alignment.CenterVertically),\n            notificationModel\n        )\n        Spacer(Modifier\n            .fillMaxHeight()\n            .padding(horizontal = 8.dp)\n            .padding(vertical = 2.dp)\n            .width(1.dp)\n            .background(myColors.onSurface/20)\n        )\n        Column {\n            NotificationTitle(notificationModel)\n            Spacer(Modifier.size(4.dp))\n            NotificationDescription(notificationModel)\n        }\n    }\n}\n\n@Composable\nprivate fun NotificationDescription(notificationModel: NotificationModel) {\n    WithContentAlpha(0.75f) {\n        Text(\n            text = notificationModel.description.rememberString(),\n            fontSize = myTextSizes.base\n        )\n    }\n}\n\n@Composable\nprivate fun NotificationTitle(notificationModel: NotificationModel) {\n    WithContentAlpha(1f) {\n        Text(\n            text = notificationModel.title.rememberString(),\n            fontSize = myTextSizes.base,\n            fontWeight = FontWeight.Bold,\n        )\n    }\n}\n\n@Composable\nfun LoadingIcon(modifier: Modifier, percent: Int?) {\n    if (percent == null) {\n        LoadingIndicator(\n            modifier = modifier\n        )\n    } else {\n        LoadingIndicator(\n            modifier = modifier,\n            progress = (percent / 100f).coerceIn(0f, 1f)\n        )\n    }\n}\n\n@Composable\nprivate fun InfoIcon(modifier: Modifier, color: Color) {\n    MyIcon(\n        icon = MyIcons.info,\n        contentDescription = null,\n        modifier = modifier,\n        tint = color,\n    )\n}\n\n@Composable\nfun NotificationIcon(\n    modifier: Modifier=Modifier,\n    notificationModel: NotificationModel\n) {\n    val notificationType = notificationModel.notificationType\n    val modifier = modifier.size(24.dp)\n    when (notificationType) {\n        NotificationType.Error -> {\n            InfoIcon(modifier, myColors.error)\n        }\n\n        NotificationType.Info -> {\n            InfoIcon(modifier, myColors.info)\n        }\n\n        NotificationType.Success -> {\n            InfoIcon(modifier, myColors.success)\n        }\n\n        NotificationType.Warning -> {\n            InfoIcon(modifier, myColors.warning)\n        }\n\n        is NotificationType.Loading -> {\n            LoadingIcon(modifier, notificationType.percent)\n        }\n    }\n}\n\n\n@Stable\nclass NotificationManager {\n    private val _activeNotificationList = MutableStateFlow<List<NotificationModel>>(emptyList())\n    val activeNotificationList = _activeNotificationList.asStateFlow()\n\n    suspend fun showNotification(\n        notification: NotificationModel,\n    ) {\n        try {\n            _activeNotificationList.update {\n                it.plus(notification)\n            }\n            awaitCancellation()\n        } finally {\n            _activeNotificationList.update {\n                it.minus(notification)\n            }\n        }\n    }\n\n    suspend fun showNotification(\n        title: StringSource,\n        description: StringSource,\n        delay: Long = -1,\n        type: NotificationType = NotificationType.Info,\n        tag: Any = UUID.randomUUID(),\n    ) {\n        val notification = NotificationModel(\n            tag = tag,\n            initialTitle = title,\n            initialDescription = description,\n            initialNotificationType = type,\n        )\n        coroutineScope {\n            if (delay == -1L) {\n                showNotification(notification)\n            } else {\n                withTimeoutOrNull(delay) {\n                    showNotification(notification)\n                }\n            }\n        }\n    }\n}\n\n/*\nfun main() {\n    application {\n        ABDownloaderTheme(\"dark\") {\n            ProvideNotificationManager {\n                CustomWindow(\n                    rememberWindowState(\n                        size = DpSize(400.dp, 400.dp)\n                    ),\n                    onCloseRequest = this::exitApplication,\n                ) {\n                    val useNotification = useNotification()\n                    LaunchedEffect(Unit) {\n                        delay(1000)\n                        launch {\n                            useNotification.showNotification(\n                                title = \"A title\",\n                                description = \"A brief description\",\n                                delay = 5000,\n                            )\n                        }\n                        delay(1000)\n                        launch {\n                            useNotification.showNotification(\n                                title = \"A second title\",\n                                description = \"A brief description\",\n                                delay = 5000,\n                            )\n                        }\n                    }\n\n                    Box(Modifier.fillMaxSize()) {\n                        NotificationArea(\n                            Modifier\n                                .width(200.dp)\n                                .padding(8.dp)\n                                .align(Alignment.BottomEnd)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}*/\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/NumberTextField.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsDraggedAsState\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.IconSource\n\nprivate val DefaultShape\n    @Composable\n    get() = myShapes.defaultRounded\n\n@Composable\nfun IntTextField(\n    value: Int, onValueChange: (Int) -> Unit,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    range: ClosedRange<Int>,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    keyboardOptions: KeyboardOptions,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    prettify: (Int) -> String = { it.toString() },\n    placeholder: String = \"\",\n    textPadding: PaddingValues = PaddingValues(4.dp),\n    shape: Shape = DefaultShape,\n) {\n    NumberTextField(\n        value = value,\n        onValueChange = onValueChange,\n        enc = {\n            value + it\n        },\n        toValue = {\n            it.toIntOrNull()\n        },\n        prettify = prettify,\n        fromValue = {\n            it.toString()\n        },\n        range = range,\n        modifier = modifier,\n        enabled = enabled,\n        keyboardOptions = keyboardOptions,\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        placeholder = placeholder,\n        textPadding = textPadding,\n        shape = shape,\n    )\n}\n\n@Composable\nfun LongTextField(\n    value: Long,\n    onValueChange: (Long) -> Unit,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    range: ClosedRange<Long>,\n    modifier: Modifier,\n    enabled: Boolean = true,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    textPadding: PaddingValues = PaddingValues(4.dp),\n    placeholder: String = \"\",\n) {\n    NumberTextField(\n        value = value,\n        onValueChange = onValueChange,\n        enc = {\n            value + it\n        },\n        toValue = {\n            it.toLongOrNull()\n        },\n        fromValue = {\n            it.toString()\n        },\n        range = range,\n        modifier = modifier,\n        enabled = enabled,\n        keyboardOptions = keyboardOptions.copy(\n            keyboardType = KeyboardType.Decimal\n        ),\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        placeholder = placeholder,\n        textPadding = textPadding,\n    )\n}\n\n@Composable\nfun DoubleTextField(\n    value: Double,\n    onValueChange: (Double) -> Unit,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    range: ClosedRange<Double>,\n    modifier: Modifier,\n    unit: Double = 0.5,\n    prettify: (Double) -> String = {\n        it.toString()\n    },\n    enabled: Boolean = true,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    placeholder: String = \"\",\n) {\n    NumberTextField(\n        value = value,\n        onValueChange = onValueChange,\n        enc = {\n            value + it * unit\n        },\n        toValue = {\n            it.toDoubleOrNull()\n        },\n        fromValue = prettify,\n        range = range,\n        modifier = modifier,\n        enabled = enabled,\n        keyboardOptions = keyboardOptions.copy(\n            keyboardType = KeyboardType.Decimal\n        ),\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        placeholder = placeholder\n    )\n}\n\n@Composable\nfun FloatTextField(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    range: ClosedRange<Float>,\n    modifier: Modifier,\n    unit: Float = 0.5f,\n    enabled: Boolean = true,\n    prettify: (Float) -> String = { it.toString() },\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    placeholder: String = \"\",\n    textPadding: PaddingValues = PaddingValues(8.dp),\n) {\n    NumberTextField(\n        value = value,\n        onValueChange = onValueChange,\n        enc = {\n            value + it * unit\n        },\n        toValue = {\n            it.toFloat()\n        },\n        fromValue = prettify,\n        range = range,\n        modifier = modifier,\n        enabled = enabled,\n        keyboardOptions = keyboardOptions.copy(\n            keyboardType = KeyboardType.Decimal\n        ),\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        placeholder = placeholder,\n        textPadding = textPadding,\n    )\n}\n\n//a null symbol used by NumberTextField\nprivate val NULL = Any()\n\n@Composable\nfun <T : Comparable<T>> NumberTextField(\n    value: T,\n    onValueChange: (T) -> Unit,\n    enc: (unit: Int) -> T,\n    toValue: (String) -> T?,\n    fromValue: (T) -> String,\n    prettify: (T) -> String = fromValue,\n    range: ClosedRange<T>,\n    modifier: Modifier,\n    enabled: Boolean,\n    keyboardOptions: KeyboardOptions,\n    keyboardActions: KeyboardActions,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    placeholder: String = \"\",\n    textPadding: PaddingValues = PaddingValues(4.dp),\n    shape: Shape = DefaultShape,\n) {\n\n    val value by rememberUpdatedState(value)\n    val isFocused by interactionSource.collectIsFocusedAsState()\n    var haveWrongValue by remember(value) {\n        mutableStateOf(false)\n    }\n    var myText by remember {\n        mutableStateOf(\"\")\n    }\n    var lastEmittedValueByMe by remember {\n        mutableStateOf(NULL as Any?)\n    }\n    // we observe new values here\n    // we want to check who is changing that value\n    // if lastEmittedValueByMe == value then we do that,\n    // so we can stop prettifying that value until focus gone\n    // this logic depends on [set] function [prettify] parameter\n    // if it set lastEmittedByMe to null then it will recall prettify\n    // another purpose of this logic is to handle new value that are coming in the composable\n    LaunchedEffect(value) {\n        if (lastEmittedValueByMe != value) {\n            myText = prettify(value)\n        }\n    }\n    fun set(v: T, prettify: Boolean): Boolean {\n        val isInRange = v in range\n        val valueInRange = if (isInRange) v else v.coerceIn(range)\n        lastEmittedValueByMe = if (prettify || !isInRange) {\n            NULL\n        } else {\n            valueInRange\n        }\n        onValueChange(valueInRange)\n        return isInRange\n    }\n    LaunchedEffect(isFocused, haveWrongValue) {\n        if (!isFocused) {\n            if (haveWrongValue) {\n                set(range.start, true)\n            } else {\n                myText = prettify(value)\n            }\n        }\n    }\n    MyTextField(\n        textPadding = textPadding,\n        shape = shape,\n        modifier = modifier.onKeyEvent {\n            when (it.key) {\n                Key.DirectionUp -> {\n                    set(enc(1), true)\n                    true\n                }\n\n                Key.DirectionDown -> {\n                    set(enc(-1), true)\n                    true\n                }\n\n                else -> {\n                    false\n                }\n            }\n        },\n        placeholder = placeholder,\n        text = myText,\n        onTextChange = {\n            if (it.isBlank()) {\n                myText = \"\"\n                haveWrongValue = true\n                return@MyTextField\n            }\n            val v = toValue(it)\n            if (v != null) {\n                if (v == value) {\n                    //only update text (not prettify until focus lost\n                    myText = it\n                } else {\n                    val wasInRange = set(v, false)\n                    if (wasInRange) {\n                        myText = it\n                    }\n                }\n            }\n        },\n        enabled = enabled,\n        keyboardOptions = keyboardOptions,\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        end = {\n            VerticalDirectionHandle(\n                modifier = Modifier,\n                onValueChange = {\n                    set(it, true)\n                },\n                enc = enc,\n                enabled = enabled,\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun <T : Comparable<T>> VerticalDirectionHandle(\n    modifier: Modifier,\n    onValueChange: (T) -> Unit,\n    enc: (unit: Int) -> T,\n    enabled: Boolean,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n    val isDragging by interactionSource.collectIsDraggedAsState()\n    val isHovered by interactionSource.collectIsHoveredAsState()\n\n    WithContentAlpha(\n        animateFloatAsState(if (isDragging || isHovered) 1f else 0.5f).value\n    ) {\n        Row(\n            modifier\n                .ifThen(enabled) {\n                    hoverable(interactionSource)\n                        .resizeHandle(\n                            Orientation.Vertical, interactionSource\n                        ) {\n                            val times = it.value.toInt()\n                            //we reverse this as Y is top to down\n                            onValueChange(enc(-times))\n                        }\n                }\n                .pointerHoverIcon(PointerIcon.Default),\n        ) {\n            DirectionIcon(\n                MyIcons.down,\n                enabled = enabled,\n                onClick = { onValueChange(enc(-1)) },\n            )\n            DirectionIcon(\n                MyIcons.up,\n                enabled = enabled,\n                onClick = { onValueChange(enc(1)) },\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun DirectionIcon(\n    icon: IconSource,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    MyIcon(\n        icon, null, Modifier\n            .pointerHoverIcon(PointerIcon.Default)\n            .fillMaxHeight()\n            .clickable(enabled = enabled, onClick = onClick)\n            .wrapContentHeight()\n            .padding(horizontal = 4.dp)\n            .size(mySpacings.iconSize)\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Popup.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntRect\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.round\nimport androidx.compose.ui.window.PopupPositionProvider\nimport kotlin.math.roundToInt\n\n\n/**\n * A [PopupPositionProvider] that positions the popup at the given position relative to the anchor.\n *\n * @param positionPx the offset, in pixels, relative to the anchor, to position the popup at.\n * @param offset [DpOffset] to be added to the position of the popup.\n * @param alignment The alignment of the popup relative to desired position.\n * @param windowMargin Defines the area within the window that limits the placement of the popup.\n */\n@ExperimentalComposeUiApi\n@Composable\nfun rememberMyPopupPositionProviderAtPosition(\n    positionPx: Offset,\n    offset: DpOffset = DpOffset.Zero,\n    alignment: Alignment = Alignment.BottomEnd,\n    windowMargin: Dp = 4.dp\n): PopupPositionProvider = with(LocalDensity.current) {\n    val offsetPx = Offset(offset.x.toPx(), offset.y.toPx())\n    val windowMarginPx = windowMargin.roundToPx()\n\n    remember(positionPx, offsetPx, alignment, windowMarginPx) {\n        PopupPositionProviderAtPosition(\n            positionPx = positionPx,\n            isRelativeToAnchor = true,\n            offsetPx = offsetPx,\n            alignment = alignment,\n            windowMarginPx = windowMarginPx\n        )\n    }\n}\n\n/**\n * A [PopupPositionProvider] that positions the popup at the given offsets and alignment.\n *\n * @param positionPx The offset of the popup's location, in pixels.\n * @param isRelativeToAnchor Whether [positionPx] is relative to the anchor bounds passed to\n * [calculatePosition]. If `false`, it is relative to the window.\n * @param offsetPx Extra offset to be added to the position of the popup, in pixels.\n * @param alignment The alignment of the popup relative to desired position.\n * @param windowMarginPx Defines the area within the window that limits the placement of the popup,\n * in pixels.\n */\n@ExperimentalComposeUiApi\nclass PopupPositionProviderAtPosition(\n    val positionPx: Offset,\n    val isRelativeToAnchor: Boolean,\n    val offsetPx: Offset,\n    val alignment: Alignment = Alignment.BottomEnd,\n    val windowMarginPx: Int,\n) : PopupPositionProvider {\n    override fun calculatePosition(\n        anchorBounds: IntRect,\n        windowSize: IntSize,\n        layoutDirection: LayoutDirection,\n        popupContentSize: IntSize\n    ): IntOffset {\n        val anchor = IntRect(\n            offset = positionPx.round() +\n                    (if (isRelativeToAnchor) anchorBounds.topLeft else IntOffset.Zero),\n            size = IntSize.Zero\n        )\n        val tooltipArea = IntRect(\n            IntOffset(\n                anchor.left - popupContentSize.width,\n                anchor.top - popupContentSize.height,\n            ),\n            IntSize(\n                popupContentSize.width * 2,\n                popupContentSize.height * 2\n            )\n        )\n        val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)\n        var x = tooltipArea.left + position.x + offsetPx.x\n        var y = tooltipArea.top + position.y + offsetPx.y\n        if (x + popupContentSize.width > windowSize.width - windowMarginPx) {\n            x -= popupContentSize.width\n        }\n        if (y + popupContentSize.height > windowSize.height - windowMarginPx) {\n            y -= popupContentSize.height + anchor.height\n        }\n        x = x.coerceAtLeast(windowMarginPx.toFloat())\n        y = y.coerceAtLeast(windowMarginPx.toFloat())\n\n        return IntOffset(x.roundToInt(), y.roundToInt())\n    }\n}\n\n/**\n * Provides [PopupPositionProvider] relative to the current component bounds.\n *\n * @param anchor The anchor point relative to the current component bounds.\n * @param alignment The alignment of the popup relative to the [anchor] point.\n * @param offset [DpOffset] to be added to the position of the popup.\n */\n@Composable\nfun rememberMyComponentRectPositionProvider(\n    anchor: Alignment = Alignment.BottomCenter,\n    alignment: Alignment = Alignment.BottomCenter,\n    offset: DpOffset = DpOffset.Zero\n): PopupPositionProvider {\n    val offsetPx = with(LocalDensity.current) {\n        IntOffset(offset.x.roundToPx(), offset.y.roundToPx())\n    }\n    return remember(anchor, alignment, offsetPx) {\n        object : PopupPositionProvider {\n            override fun calculatePosition(\n                anchorBounds: IntRect,\n                windowSize: IntSize,\n                layoutDirection: LayoutDirection,\n                popupContentSize: IntSize\n            ): IntOffset {\n                val anchorPoint = anchor.align(IntSize.Zero, anchorBounds.size, layoutDirection)\n                val tooltipArea = IntRect(\n                    IntOffset(\n                        anchorBounds.left + anchorPoint.x - popupContentSize.width,\n                        anchorBounds.top + anchorPoint.y - popupContentSize.height,\n                    ),\n                    IntSize(\n                        popupContentSize.width * 2,\n                        popupContentSize.height * 2\n                    )\n                )\n                val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)\n                return tooltipArea.topLeft + position + offsetPx\n            }\n        }\n    }\n}\n\n@Composable\nfun rememberMyComponentCustomRectPositionProvider(\n    providedAnchorBounds: IntRect,\n    anchor: Alignment = Alignment.BottomCenter,\n    alignment: Alignment = Alignment.BottomCenter,\n    offset: DpOffset = DpOffset.Zero\n): PopupPositionProvider {\n    val offsetPx = with(LocalDensity.current) {\n        IntOffset(offset.x.roundToPx(), offset.y.roundToPx())\n    }\n    return remember(providedAnchorBounds, anchor, alignment, offsetPx) {\n        object : PopupPositionProvider {\n            override fun calculatePosition(\n                anchorBounds: IntRect,\n                windowSize: IntSize,\n                layoutDirection: LayoutDirection,\n                popupContentSize: IntSize\n            ): IntOffset {\n                val anchorPoint = anchor.align(IntSize.Zero, providedAnchorBounds.size, layoutDirection)\n                val tooltipArea = IntRect(\n                    IntOffset(\n                        providedAnchorBounds.left + anchorPoint.x - popupContentSize.width,\n                        providedAnchorBounds.top + anchorPoint.y - popupContentSize.height,\n                    ),\n                    IntSize(\n                        popupContentSize.width * 2,\n                        popupContentSize.height * 2\n                    )\n                )\n                val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)\n                return tooltipArea.topLeft + position + offsetPx\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/RadioButton.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.selection.triStateToggleable\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.state.ToggleableState\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun RadioButton(\n    value: Boolean,\n    onValueChange: (Boolean) -> Unit,\n    enabled: Boolean = true,\n    modifier: Modifier = Modifier,\n    size: Dp = 18.dp,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    uncheckedAlpha: Float = 0.25f,\n) {\n    val shape = CircleShape\n    Box(\n        modifier\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .size(size)\n            .clip(shape)\n            .triStateToggleable(\n                state = ToggleableState(value),\n                enabled = enabled,\n                role = Role.RadioButton,\n                interactionSource = interactionSource,\n                indication = null,\n                onClick = { onValueChange(!value) },\n            )\n    ) {\n        Spacer(\n            Modifier.matchParentSize()\n                .border(\n                    1.dp,\n                    if (value) {\n                        myColors.primaryGradient\n                    } else {\n                        SolidColor(LocalContentColor.current / uncheckedAlpha)\n                    },\n                    shape\n                )\n        )\n        AnimatedContent(\n            value,\n            transitionSpec = {\n                val tween = tween<Float>(220)\n                fadeIn(tween) togetherWith fadeOut(tween)\n            }\n        ) {\n            val m = Modifier\n                .fillMaxSize()\n                .alpha(animateFloatAsState(if (value) 1f else 0f).value)\n                .padding(4.dp)\n                .clip(shape)\n                .background(myColors.primaryGradient)\n            Spacer(m)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Switch.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.BiasAlignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun Switch(\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n    enabled: Boolean = true,\n    modifier: Modifier = Modifier.width(42.dp).height(24.dp),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n) {\n    Box(\n        modifier\n            .clip(CircleShape)\n            .ifThen(!enabled) {\n                alpha(0.5f)\n            }\n            .background(\n                if (checked) {\n                    myColors.primaryGradient\n                }else {\n                    Brush.linearGradient(listOf(Color.Gray,Color.Gray))\n                }\n            )\n            .toggleable(\n                value = checked,\n                onValueChange = onCheckedChange,\n                enabled = enabled,\n                role = Role.Switch,\n                interactionSource = interactionSource,\n                indication = null\n            )\n            .padding(4.dp)\n            .fillMaxSize()\n    ) {\n        Box(\n            Modifier\n                .fillMaxHeight()\n                .aspectRatio(1f, true)\n                .align(\n                    BiasAlignment(\n                        animateFloatAsState(\n                            if (checked) 1f else -1f\n                        ).value,\n                        0f,\n                    )\n                )\n                .clip(CircleShape)\n                .background(myColors.onPrimaryGradient / animateFloatAsState(if (checked) 1f else 0.5f).value)\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Tabs.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\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.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.mySpacings\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.ifThen\n\n\n@Composable\nfun MyTabRow(content: @Composable RowScope.() -> Unit) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun MyTab(\n    selected: Boolean,\n    onClick: () -> Unit,\n    icon: IconSource,\n    title: StringSource,\n    selectionBackground: Color = myColors.surface,\n) {\n    WithContentAlpha(\n        if (selected) 1f else 0.75f\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier\n                .ifThen(selected) {\n                    background(selectionBackground)\n                }\n                .clickable { onClick() }\n                .heightIn(mySpacings.thumbSize)\n                .padding(horizontal = 12.dp)\n                .padding(vertical = 6.dp)\n\n        ) {\n            MyIcon(icon, null, Modifier.size(16.dp))\n            Spacer(Modifier.width(4.dp))\n            Text(\n                title.rememberString(),\n                maxLines = 1,\n                fontSize = myTextSizes.base,\n                fontWeight = if (selected) {\n                    FontWeight.Bold\n                } else {\n                    FontWeight.Medium\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Text.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.LocalTextStyle\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.isSpecified\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\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.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\n\n@Composable\nfun Text(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign = TextAlign.Unspecified,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    minLines: Int = 1,\n    onTextLayout: ((TextLayoutResult) -> Unit)? = null,\n    style: TextStyle = LocalTextStyle.current\n) {\n    val localContentColor = LocalContentColor.current\n    val localContentAlpha = LocalContentAlpha.current\n    val overrideColorOrUnspecified: Color = if (color.isSpecified) {\n        color\n    } else if (style.color.isSpecified) {\n        style.color\n    } else {\n        localContentColor.copy(localContentAlpha)\n    }\n\n    BasicText(\n        text = text,\n        modifier = modifier,\n        style = style.merge(\n            fontSize = fontSize,\n            fontWeight = fontWeight,\n            textAlign = textAlign,\n            lineHeight = lineHeight,\n            fontFamily = fontFamily,\n            textDecoration = textDecoration,\n            fontStyle = fontStyle,\n            letterSpacing = letterSpacing\n        ),\n        onTextLayout = onTextLayout,\n        overflow = overflow,\n        softWrap = softWrap,\n        maxLines = maxLines,\n        minLines = minLines,\n        color = { overrideColorOrUnspecified }\n    )\n}\n\n@Composable\nfun Text(\n    text: AnnotatedString,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign = TextAlign.Unspecified,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    minLines: Int = 1,\n    inlineContent: Map<String, InlineTextContent> = mapOf(),\n    onTextLayout: (TextLayoutResult) -> Unit = {},\n    style: TextStyle = LocalTextStyle.current\n) {\n    val localContentColor = LocalContentColor.current\n    val localContentAlpha = LocalContentAlpha.current\n    val overrideColorOrUnspecified = if (color.isSpecified) {\n        color\n    } else if (style.color.isSpecified) {\n        style.color\n    } else {\n        localContentColor.copy(localContentAlpha)\n    }\n\n    BasicText(\n        text = text,\n        modifier = modifier,\n        style = style.merge(\n            fontSize = fontSize,\n            fontWeight = fontWeight,\n            textAlign = textAlign,\n            lineHeight = lineHeight,\n            fontFamily = fontFamily,\n            textDecoration = textDecoration,\n            fontStyle = fontStyle,\n            letterSpacing = letterSpacing\n        ),\n        onTextLayout = onTextLayout,\n        overflow = overflow,\n        softWrap = softWrap,\n        maxLines = maxLines,\n        minLines = minLines,\n        inlineContent = inlineContent,\n        color = { overrideColorOrUnspecified }\n    )\n}\n\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/Tooltip.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.BasicTooltipState\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.MutatePriority\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.delay\n\nprivate const val TooltipDelay = 500L\n\n@Composable\nfun Tooltip(\n    tooltip: StringSource,\n    delayUntilShow: Long = TooltipDelay,\n    anchor: Alignment = Alignment.TopCenter,\n    alignment: Alignment = Alignment.TopCenter,\n    content: @Composable () -> Unit,\n) {\n    val showHint = remember { mutableStateOf(false) }\n    Column(\n        modifier = Modifier\n            .detectTooltip(showHint)\n    ) {\n        if (showHint.value) {\n            DelayedTooltipPopup(\n                onRequestCloseShowHelpContent = {\n                    showHint.value = false\n                },\n                content = tooltip.rememberString(),\n                delay = delayUntilShow,\n                anchor = anchor,\n                alignment = alignment\n            )\n        }\n        content()\n    }\n}\n\n@Composable\nfun TooltipPopup(\n    onRequestCloseShowHelpContent: () -> Unit,\n    content: String,\n    anchor: Alignment = Alignment.TopCenter,\n    alignment: Alignment = Alignment.TopCenter\n) {\n    Popup(\n        popupPositionProvider = rememberMyComponentRectPositionProvider(\n            anchor = anchor,\n            alignment = alignment,\n        ),\n        onDismissRequest = onRequestCloseShowHelpContent\n    ) {\n        val shape = myShapes.defaultRounded\n        Box(\n            Modifier\n                .padding(vertical = 4.dp)\n                .widthIn(max = 240.dp)\n                .shadow(4.dp, shape)\n                .clip(shape)\n                .border(1.dp, myColors.onSurface / 0.1f, shape)\n                .background(myColors.surface)\n                .padding(8.dp)\n        ) {\n            WithContentColor(myColors.onSurface) {\n                Text(\n                    content,\n                    fontSize = myTextSizes.base,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun DelayedTooltipPopup(\n    onRequestCloseShowHelpContent: () -> Unit,\n    content: String,\n    delay: Long = TooltipDelay,\n    anchor: Alignment = Alignment.TopCenter,\n    alignment: Alignment = Alignment.TopCenter,\n) {\n    var showPopup by remember { mutableStateOf(false) }\n    LaunchedEffect(Unit) {\n        delay(delay)\n        showPopup = true\n    }\n    if (showPopup) {\n        TooltipPopup(\n            onRequestCloseShowHelpContent = onRequestCloseShowHelpContent,\n            content = content,\n            anchor = anchor,\n            alignment = alignment,\n        )\n    }\n}\n\nexpect fun Modifier.detectTooltip(\n    state: MutableState<Boolean>\n): Modifier\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/DropDown.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.shared.ui.widget.rememberMyComponentRectPositionProvider\n\n@Composable\nfun MyDropDown(\n    onDismissRequest: () -> Unit,\n    offset: DpOffset = DpOffset.Zero,\n    anchor: Alignment = Alignment.BottomStart,\n    alignment: Alignment = Alignment.BottomEnd,\n    focusable: Boolean = true,\n    content: @Composable () -> Unit,\n) {\n    val positionProvider = rememberMyComponentRectPositionProvider(\n        offset = offset,\n        anchor = anchor,\n        alignment = alignment,\n    )\n    Popup(\n        popupPositionProvider = positionProvider,\n        onDismissRequest = onDismissRequest,\n        properties = PopupProperties(focusable = focusable),\n        content = {\n            content()\n        })\n}\n\n@Composable\nfun SiblingDropDown(\n    onDismissRequest: () -> Unit,\n    offset: DpOffset = DpOffset.Zero,\n    content: @Composable () -> Unit,\n) {\n    val positionProvider = rememberMyComponentRectPositionProvider(\n        anchor = Alignment.TopEnd,\n        alignment = Alignment.BottomEnd,\n        offset = offset,\n    )\n    Popup(\n        popupPositionProvider = positionProvider,\n        onDismissRequest = onDismissRequest,\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/MenuColumn.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.myColors\n\n@Composable\nfun MenuColumn(\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    val shape = LocalMenuBoxClip.current\n    Column(\n        Modifier.Companion\n            .shadow(24.dp)\n//                .verticalScroll(rememberScrollState())\n            .clip(shape)\n            .width(IntrinsicSize.Max)\n            .widthIn(120.dp)\n            .border(1.dp, myColors.surface, shape)\n            .background(myColors.menuGradientBackground)\n            .padding(horizontal = 0.dp, vertical = 0.dp)\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/Option.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.compose.action.MenuItem\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.layout.*\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\n@Composable\nexpect fun ShowOptionsInPopup(\n    menu: MenuItem.SubMenu,\n    onDismissRequest: () -> Unit,\n)\n\n/**\n * this is only used by expect actual ShowOptionsInPopup if their actual implementations need other style remove it here\n */\n@Composable\ninternal fun RenderOptions(\n    menu: MenuItem.SubMenu,\n    onDismissRequest: () -> Unit\n) {\n    SubMenu(menu,onDismissRequest) {\n        Column(\n            Modifier\n                .width(200.dp)\n        ) {\n            val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)\n            val title by menu.title.collectAsState()\n            Text(\n                title.rememberString(),\n                Modifier\n                    .then(itemPadding)\n                    .basicMarquee(\n                        iterations = Int.MAX_VALUE,\n                        initialDelayMillis = 0\n                    ),\n                fontSize = myTextSizes.base,\n                maxLines = 1,\n                overflow = TextOverflow.Clip,\n            )\n            Spacer(Modifier\n                .fillMaxWidth()\n                .height(1.dp)\n                .background(myColors.onSurface/5))\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/SiblingMenuPositionProvider.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.unit.*\nimport androidx.compose.ui.window.PopupPositionProvider\n\n@Immutable\ninternal class SiblingMenuPositionProvider : PopupPositionProvider {\n    override fun calculatePosition(\n        anchorBounds: IntRect,\n        windowSize: IntSize,\n        layoutDirection: LayoutDirection,\n        popupContentSize: IntSize,\n    ): IntOffset {\n        return anchorBounds.topRight\n    }\n}"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/SubMenu.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\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.layout.widthIn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.compositionLocalOf\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.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport com.abdownloadmanager.shared.util.LocalShortCutManager\nimport com.abdownloadmanager.shared.util.PlatformKeyStroke\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.ProvideTextStyle\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport ir.amirab.util.compose.action.MenuItem\nimport ir.amirab.util.compose.modifiers.autoMirror\nimport ir.amirab.util.ifThen\n\nenum class MenuDisabledItemBehavior {\n    Filter,\n    LowerOpacity,\n}\n\nval LocalMenuDisabledItemBehavior = compositionLocalOf {\n    MenuDisabledItemBehavior.LowerOpacity\n}\nval LocalMenuBoxClip = compositionLocalOf<Shape> {\n    RoundedCornerShape(6.dp)\n}\n\n/**\n * render a menu\n */\n@Composable\nfun SubMenu(\n    subMenu: MenuItem.SubMenu,\n    onRequestClose: () -> Unit,\n    header: (@Composable () -> Unit)? = null,\n) {\n    SubMenu(\n        subMenu = subMenu.items.collectAsState().value,\n        header = header,\n        onRequestClose = onRequestClose,\n    )\n}\n@Composable\nfun SubMenu(\n    subMenu: List<MenuItem>,\n    onRequestClose: () -> Unit,\n    header: (@Composable () -> Unit)? = null,\n) {\n    var openedItem: MenuItem.SubMenu? by remember {\n        mutableStateOf(null)\n    }\n    var lastHoveredItem by remember {\n        mutableStateOf(null as MenuItem?)\n    }\n\n    WithContentColor(myColors.onMenuColor) {\n        val shape = LocalMenuBoxClip.current\n        Column(\n            Modifier\n                .shadow(24.dp)\n//                .verticalScroll(rememberScrollState())\n                .clip(shape)\n                .width(IntrinsicSize.Max)\n                .widthIn(120.dp)\n                .border(1.dp, myColors.surface, shape)\n                .background(myColors.menuGradientBackground)\n                .padding(horizontal = 0.dp, vertical = 0.dp)\n        ) {\n            header?.invoke()\n            for (menuItem in subMenu) {\n                val interactionSource = remember { MutableInteractionSource() }\n                val isHovered by interactionSource.collectIsHoveredAsState()\n                LaunchedEffect(isHovered) {\n                    if (isHovered) {\n//                        println(\"last overed item is ${menuItem.hashCode()}\")\n//                        println(\"last overed item is ${(menuItem as? MenuItem.SubMenu)?.title?.value}\")\n                        lastHoveredItem = menuItem\n                    }\n                }\n                RenderMenuItem(\n                    menuItem = menuItem,\n                    openedItem = openedItem,\n                    onRequestCLose = onRequestClose,\n                    isSelected = openedItem == menuItem,\n                    onRequestOpenItem = {\n                        openedItem = it\n                    },\n                    isHovered = lastHoveredItem == menuItem,\n                    modifier = Modifier.hoverable(interactionSource)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ReactableItem(\n    item: MenuItem.ReadableItem,\n    onClick: () -> Unit,\n    isSelected: Boolean,\n    modifier: Modifier = Modifier,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    extraContent: @Composable () -> Unit = {},\n) {\n    val iconModifier = Modifier.size(16.dp)\n    val title by item.title.collectAsState()\n    val icon by item.icon.collectAsState()\n    val itemPadding = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    val isEnabled = (item as? MenuItem.HasEnable)\n        ?.isEnabled\n        ?.collectAsState()\n        ?.value ?: true\n    Row(\n        modifier\n            .ifThen(!isEnabled) { alpha(0.5f) }\n            .hoverable(interactionSource)\n            .background(\n                when {\n                    (isHovered && isEnabled) || isSelected -> {\n                        myColors.surface\n                    }\n\n                    else -> {\n                        Color.Transparent\n                    }\n                }\n            )\n            .clickable(enabled = isEnabled) {\n                onClick()\n            }\n            .then(itemPadding)\n            .fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        icon.let { icon ->\n            if (icon != null) {\n                Spacer(Modifier.width(4.dp))\n                MyIcon(icon, null, iconModifier)\n                Spacer(Modifier.width(8.dp))\n            } else {\n                Spacer(iconModifier)\n            }\n        }\n        Text(\n            title.rememberString(),\n            Modifier.weight(1f),\n            fontSize = myTextSizes.base,\n            softWrap = false,\n            maxLines = 1,\n        )\n        Spacer(Modifier.width(16.dp))\n        extraContent()\n    }\n}\n\n@Composable\nprivate fun RenderMenuItem(\n    menuItem: MenuItem,\n    openedItem: MenuItem.SubMenu?,\n    onRequestCLose: () -> Unit,\n    isSelected: Boolean,\n    isHovered: Boolean,\n    modifier: Modifier = Modifier,\n    onRequestOpenItem: (MenuItem.SubMenu?) -> Unit,\n) {\n//    val isEnabled by menuItem.isEnabled.collectAsState()\n    LaunchedEffect(isHovered, menuItem) {\n        if (isHovered) {\n            if (menuItem is MenuItem.SubMenu) {\n                onRequestOpenItem(menuItem)\n            } else {\n                onRequestOpenItem(null)\n            }\n        }\n    }\n    Row(\n        modifier\n            .fillMaxWidth()\n    ) {\n        when (menuItem) {\n            MenuItem.Separator -> {\n                Spacer(\n                    Modifier\n                        .fillMaxWidth()\n                        .height(1.dp)\n                        .background(myColors.onSurface / 5)\n                )\n            }\n\n            is MenuItem.SingleItem -> {\n                RenderSingleItem(\n                    item = menuItem,\n                    isSelected = isSelected,\n                    onRequestClose = onRequestCLose,\n                )\n            }\n\n            is MenuItem.SubMenu -> {\n                RenderSubMenuItem(\n                    menuItem = menuItem,\n                    isSelected = isSelected,\n                    onRequestCLose = onRequestCLose,\n                    openedItem = openedItem,\n                    onRequestOpenItem = onRequestOpenItem,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RenderSubMenuItem(\n    menuItem: MenuItem.SubMenu,\n    isSelected: Boolean,\n    openedItem: MenuItem.SubMenu?,\n    onRequestOpenItem: (MenuItem.SubMenu?) -> Unit,\n    onRequestCLose: () -> Unit,\n) {\n    ReactableItem(\n        item = menuItem,\n        onClick = {\n            onRequestOpenItem(menuItem)\n        },\n        isSelected = isSelected,\n        extraContent = {\n            MyIcon(\n                MyIcons.next,\n                null,\n                Modifier\n                    .size(16.dp)\n                    .autoMirror(),\n            )\n        })\n    if (openedItem == menuItem) {\n        SiblingDropDown(\n            onDismissRequest = {\n                onRequestOpenItem(null)\n            }\n        ) {\n            SubMenu(menuItem, onRequestCLose)\n        }\n    }\n}\n\n@Composable\nprivate fun RenderSingleItem(\n    onRequestClose: () -> Unit,\n    isSelected: Boolean,\n    item: MenuItem.SingleItem,\n) {\n    val isEnabled by item.isEnabled.collectAsState()\n    if (!isEnabled && LocalMenuDisabledItemBehavior.current == MenuDisabledItemBehavior.Filter) {\n        return\n    }\n\n    val shortcutManager = LocalShortCutManager.current\n    val shortcutStroke = remember(shortcutManager, item) {\n        shortcutManager?.getShortCutOf(item)\n    }\n    val onClick = {\n        if (item.shouldDismissOnClick) {\n            onRequestClose()\n        }\n        item.onClick()\n    }\n    ReactableItem(\n        item = item,\n        onClick = onClick,\n        isSelected = isSelected,\n        extraContent = {\n            if (shortcutStroke != null) {\n                RenderShortcutStroke(shortcutStroke)\n            }\n        }\n    )\n\n}\n\n@Composable\nprivate fun RenderShortcutStroke(shortcutStroke: PlatformKeyStroke) {\n    val modifiers = remember(shortcutStroke) {\n        buildList {\n            addAll(shortcutStroke.getModifiers())\n            add(shortcutStroke.getKeyText())\n        }\n    }\n    ProvideTextStyle(\n        TextStyle(\n            fontSize = myTextSizes.xs,\n        )\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(1.dp)\n        ) {\n            val shape = RoundedCornerShape(10)\n            WithContentColor(myColors.onBackground) {\n                Text(\n                    modifiers.joinToString(\"+\"),\n                    Modifier\n                        .clip(shape)\n                        .background(myColors.onBackground / 5)\n                        .padding(2.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/WithContextMenu.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport ir.amirab.util.compose.action.MenuItem\n\n@Composable\nexpect fun WithContextMenu(\n    menuProvider: () -> List<MenuItem>,\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/sort/ComparatorProvider.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.sort\n\ninterface ComparatorProvider<T> {\n    fun comparator(): Comparator<T>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/sort/Sort.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.sort\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Sort<Cell : ComparatorProvider<*>>(\n    val cell: Cell,\n    private val isDescending: Boolean,\n) {\n    fun isAscending() = !isDescending\n    fun isDescending() = isDescending\n    fun reverse(): Sort<Cell> {\n        return copy(isDescending = !isDescending)\n    }\n\n    companion object {\n        const val DEFAULT_IS_DESCENDING: Boolean = true\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/sort/SortIndicatorMode.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.sort\n\nenum class SortIndicatorMode {\n    None,\n    Ascending,\n    Descending,\n}\n\nfun SortIndicatorMode.isAscending(): Boolean {\n    return when (this) {\n        SortIndicatorMode.Ascending -> true\n        else -> false\n    }\n}\n\nfun SortIndicatorMode.isDescending(): Boolean {\n    return when (this) {\n        SortIndicatorMode.Descending -> true\n        else -> false\n    }\n}\n\nfun SortIndicatorMode.next(): SortIndicatorMode {\n    return when (this) {\n        SortIndicatorMode.None -> SortIndicatorMode.Descending\n        SortIndicatorMode.Ascending -> SortIndicatorMode.Descending\n        SortIndicatorMode.Descending -> SortIndicatorMode.Ascending\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/ui/widget/sort/sorted.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.sort\n\nimport ir.amirab.util.ifThen\n\nfun <ITEM, TComparatorProvider : ComparatorProvider<ITEM>> Sort<TComparatorProvider>.sorted(\n    list: List<ITEM>\n): List<ITEM> {\n    return list\n        .sortedWith(\n            cell\n                .comparator()\n                .ifThen(isDescending()) { reversed() }\n        )\n}\n\nfun <T : Sort<*>> T.toSortIndicatorMode(): SortIndicatorMode {\n    return if (isDescending()) {\n        SortIndicatorMode.Descending\n    } else {\n        SortIndicatorMode.Ascending\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/updater/UpdateDownloaderViaDownloadSystem.kt",
    "content": "package com.abdownloadmanager.shared.updater\n\nimport com.abdownloadmanager.UpdateDownloadLocationProvider\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport com.abdownloadmanager.updateapplier.UpdateDownloader\nimport com.abdownloadmanager.updatechecker.UpdateSource\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.downloaditem.EmptyContext\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadItem\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport java.io.File\n\nclass UpdateDownloaderViaDownloadSystem(\n    private val downloadSystem: DownloadSystem,\n    private val updateDownloadLocationProvider: UpdateDownloadLocationProvider,\n) : UpdateDownloader() {\n    override suspend fun downloadUpdateFile(updateDirectDownloadLink: UpdateSource.DirectDownloadLink): File {\n        val updateDownloadsFolder = updateDownloadLocationProvider.getSaveLocation().path\n        val updateDownloads = downloadSystem.getDownloadItemsByFolder(updateDownloadsFolder)\n        val pausedDownload = updateDownloads.find {\n            it.name == updateDirectDownloadLink.name\n        }\n        // at the moment if the download was finished but removed from the filesystem\n        // download will not be restarted automatically\n        val requireRestartDownload = pausedDownload?.getFullPath()?.exists()?.not() ?: false\n        val id = pausedDownload?.id\n            ?: downloadSystem.addDownload(\n                newDownload = NewDownloadItemProps(\n                    downloadItem = HttpDownloadItem(\n                        id = -1,\n                        link = updateDirectDownloadLink.link,\n                        folder = updateDownloadsFolder,\n                        name = updateDirectDownloadLink.name,\n                    ),\n                    onDuplicateStrategy = OnDuplicateStrategy.AddNumbered,\n                    extraConfig = null,\n                    context = EmptyContext,\n                ),\n                queueId = null,\n                categoryId = null,\n            )\n        coroutineScope {\n            if (requireRestartDownload) {\n                downloadSystem.reset(id)\n            }\n            val waiter = async {\n                downloadSystem.downloadMonitor.waitForDownloadToFinishOrCancel(id)\n            }\n            downloadSystem.manualResume(id, EmptyContext)\n            waiter.await()\n        }\n        // we recheck download info maybe some dude change the file name!\n        val downloadedItem = downloadSystem.getDownloadItemById(id)\n        requireNotNull(downloadedItem) {\n            \"Download is removed!\"\n        }\n        return downloadSystem.getDownloadFile(downloadedItem)\n    }\n\n    override suspend fun removeUpdateFiles(updateDirectDownloadLink: UpdateSource.DirectDownloadLink) {\n        val id = downloadSystem\n            .getDownloadItemsByFolder(updateDownloadLocationProvider.getSaveLocation().path)\n            .find { it.name == updateDirectDownloadLink.name }?.id\n        id?.let {\n            downloadSystem.removeDownload(id, true, EmptyContext)\n        }\n    }\n\n    override suspend fun removeAllUpdateFiles() {\n        val ids = downloadSystem\n            .getDownloadItemsByFolder(updateDownloadLocationProvider.getSaveLocation().path)\n            .map { it.id }\n        for (id in ids) {\n            downloadSystem.removeDownload(\n                id = id, alsoRemoveFile = true, EmptyContext\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/AppHostNameVerifier.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport kotlinx.coroutines.flow.StateFlow\nimport javax.net.ssl.HostnameVerifier\nimport javax.net.ssl.SSLSession\n\nclass AppHostNameVerifier(\n    private val delegateHostnameVerifier: HostnameVerifier,\n    private val ignoreHostNameVerification: StateFlow<Boolean>,\n) : HostnameVerifier {\n    override fun verify(hostname: String?, session: SSLSession?): Boolean {\n        if (ignoreHostNameVerification.value) {\n            return true\n        }\n        return delegateHostnameVerifier.verify(hostname, session)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/AppSSLFactoryProvider.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport kotlinx.coroutines.flow.StateFlow\nimport okhttp3.internal.platform.Platform\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.SSLSocketFactory\nimport javax.net.ssl.X509TrustManager\n\n/**\n * at the moment we simply use okhttp ssl factory provider with a toggleable trust manager to ignore ssl certificates\n */\nclass AppSSLFactoryProvider(\n    private val ignoreSSLCertificates: StateFlow<Boolean>,\n) {\n    val trustManager: X509TrustManager by lazy {\n        ToggleableTrustManager(\n            trustManager = Platform.get().platformTrustManager(),\n            shouldCheck = { !ignoreSSLCertificates.value }\n        )\n    }\n\n    fun createSSLSocketFactory(): SSLSocketFactory {\n        return Platform.get().newSslSocketFactory(\n            trustManager = trustManager,\n        )\n    }\n}\n\n\nprivate class ToggleableTrustManager(\n    private val trustManager: X509TrustManager,\n    private val shouldCheck: () -> Boolean,\n) : X509TrustManager {\n    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {\n        if (shouldCheck()) {\n            trustManager.checkClientTrusted(chain, authType)\n        }\n    }\n\n    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {\n        if (shouldCheck()) {\n            trustManager.checkServerTrusted(chain, authType)\n        }\n    }\n\n    override fun getAcceptedIssuers(): Array<X509Certificate> {\n        return trustManager.acceptedIssuers\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/AppVersion.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.BuildConfig\nimport io.github.z4kn4fein.semver.Version\n\nobject AppVersion {\n    private val currentVersion = Version.parse(BuildConfig.APP_VERSION)\n    fun get() = currentVersion\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/AutoStartManager.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport ir.amirab.util.startup.AbstractStartupManager\n\nobject AutoStartManager : KoinComponent {\n    private val startManager by inject<AbstractStartupManager>()\n    fun startOnBoot(boolean: Boolean) {\n//        println(\"start Manager is ${startManager}\")\n        if (boolean) {\n            startManager.install()\n        } else {\n            startManager.uninstall()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/BaseComponent.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.arkivanov.decompose.ComponentContext\nimport com.arkivanov.essenty.lifecycle.Lifecycle\nimport com.arkivanov.essenty.lifecycle.coroutines.coroutineScope\nimport com.arkivanov.essenty.lifecycle.coroutines.withLifecycle\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.Flow\n\nabstract class BaseComponent(\n    componentContext: ComponentContext\n) : ComponentContext by componentContext {\n    val scope = coroutineScope(\n        SupervisorJob() + Dispatchers.Main\n    )\n\n    fun <T> Flow<T>.withResumedLifecycle(): Flow<T> {\n        return withLifecycle(\n            lifecycle = lifecycle,\n            minActiveState = Lifecycle.State.STARTED,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/BaseConstants.kt",
    "content": "package com.abdownloadmanager.shared.util\n\ninterface BaseConstants {\n    val appName: String\n    val appDisplayName: String\n    val packageName: String\n    val dataDirName: String\n    val projectWebsite: String\n    val projectSourceCode: String\n    val donateLink: String\n    val projectTranslations: String\n    val projectGithubOwner: String\n    val projectGithubRepo: String\n    val browserIntegrations: List<BrowserIntegrationModel>\n    val telegramGroupUrl: String\n    val telegramChannelUrl: String\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/BaseSettings.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.datastore.core.DataStore\nimport arrow.optics.Lens\nimport ir.amirab.util.flow.mapTwoWayStateFlow\nimport ir.amirab.util.config.MapConfig\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.runBlocking\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nabstract class BaseStorage<T> : KoinComponent {\n    val scope: CoroutineScope by inject()\n\n    protected abstract val inMemoryState: MutableStateFlow<T>\n    protected abstract suspend fun saveData(data: T)\n\n    val data get() = inMemoryState\n\n    fun <K> from(lens: Lens<T, K>): MutableStateFlow<K> {\n        return inMemoryState.mapTwoWayStateFlow(lens)\n    }\n\n    /**\n     * call this on upper implementations where [inMemoryState] and [saveData] are implemented\n     */\n    protected fun startPersistData() {\n        inMemoryState\n            //first\n            .drop(1)\n            .debounce(500)\n            .onEach { s ->\n                saveData(s)\n            }.launchIn(scope)\n    }\n}\n\nabstract class ConfigBaseSettingsByMapConfig<T>(\n    private val dataStore: DataStore<MapConfig>,\n    private val lens: Lens<MapConfig, T>,\n) : BaseStorage<T>(), KoinComponent {\n    private val lastFileState = dataStore.data.let {\n        runBlocking { it.stateIn(scope) }\n    }\n\n    override val inMemoryState = MutableStateFlow(\n        lens.get(lastFileState.value)\n    )\n\n    override suspend fun saveData(data: T) {\n        dataStore.updateData {\n            val newData = lens.set(MapConfig(), data)\n            newData\n        }\n    }\n\n    init {\n        startPersistData()\n    }\n}\n\nabstract class ConfigBaseSettingsByJson<T>(\n    private val dataStore: DataStore<T>,\n) : BaseStorage<T>(), KoinComponent {\n    private val lastFileState = dataStore.data.let {\n        runBlocking { it.stateIn(scope) }\n    }\n\n    override val inMemoryState = MutableStateFlow(lastFileState.value)\n    override suspend fun saveData(data: T) {\n        dataStore.updateData { data }\n    }\n\n    init {\n        startPersistData()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/BottomSheet.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.animation.*\nimport androidx.compose.animation.core.updateTransition\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalFocusManager\nimport com.abdownloadmanager.shared.util.ui.widget.MPBackHandler\nimport ir.amirab.util.compose.modifiers.hijackClick\nimport ir.amirab.util.compose.modifiers.silentClickable\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.map\n\n@Suppress(\"NAME_SHADOWING\")\n@Composable\nfun ResponsiveDialog(\n    state: ResponsiveDialogState,\n    onDismiss: () -> Unit,\n    modifier: Modifier = Modifier,\n    enter: EnterTransition = slideInVertically { it },\n    exit: ExitTransition = slideOutVertically { it },\n    content: @Composable ResponsiveDialogScope.() -> Unit\n) {\n\n    // I don't know why but if I don't wrap these to rememberUpdateState\n    // sometimes recomposition not works for lambda that placed in host\n    val modifier by rememberUpdatedState(modifier)\n    val onDismiss by rememberUpdatedState(onDismiss)\n    val content by rememberUpdatedState(content)\n    val enter by rememberUpdatedState(enter)\n    val exit by rememberUpdatedState(exit)\n    if (state.targetIsOpened || state.currentIsOpened) {\n        PlaceInHost {\n            val focusManager = LocalFocusManager.current\n            LaunchedEffect(Unit) {\n                focusManager.clearFocus()\n            }\n            CustomSheet(modifier, state, onDismiss, enter, exit, content)\n        }\n    }\n}\n\n@Stable\nclass ResponsiveDialogState(\n    isOpened: Boolean\n) {\n    var targetIsOpened by mutableStateOf(isOpened)\n        internal set\n    var currentIsOpened by mutableStateOf(isOpened)\n        internal set\n\n    val isFullyVisible by derivedStateOf {\n        val targetIsOpened = targetIsOpened\n        val currentIsOpened = currentIsOpened\n        targetIsOpened == currentIsOpened && targetIsOpened\n    }\n\n    val isFullyInvisible by derivedStateOf {\n        val targetIsOpened = targetIsOpened\n        val currentIsOpened = currentIsOpened\n        targetIsOpened == currentIsOpened && !targetIsOpened\n    }\n\n    val onFullyInvisibleFlow = snapshotFlow { isFullyInvisible }\n        .filter { it }.map { }\n\n    val isIdle by derivedStateOf {\n        targetIsOpened == currentIsOpened\n    }\n\n    fun show() {\n        targetIsOpened = true\n    }\n\n    fun hide() {\n        targetIsOpened = false\n    }\n}\n\n@Composable\nfun ResponsiveDialogState.OnFullyDismissed(\n    onDismiss: () -> Unit,\n) {\n    val state = this\n    val onDismiss by rememberUpdatedState(onDismiss)\n    LaunchedEffect(state) {\n        val isFullyInvisibleAtFirst = state.isFullyInvisible\n        val dropValue = if (isFullyInvisibleAtFirst) 1 else 0\n        state.onFullyInvisibleFlow\n            .drop(dropValue)\n            .collect { onDismiss() }\n    }\n}\n\n@Composable\nfun rememberResponsiveDialogState(isOpened: Boolean): ResponsiveDialogState {\n    return remember {\n        ResponsiveDialogState(isOpened)\n    }\n}\ninterface ResponsiveDialogScope {\n    val isTopEndFree: Boolean\n    val isTopStartFree: Boolean\n    val isBottomStartFree: Boolean\n    val isBottomEndFree: Boolean\n}\n\n@Immutable\nprivate data class ResponsiveDialogScopeImpl(\n    override val isTopStartFree: Boolean,\n    override val isTopEndFree: Boolean,\n    override val isBottomStartFree: Boolean,\n    override val isBottomEndFree: Boolean,\n) : ResponsiveDialogScope\n\n@Composable\nprivate fun CustomSheet(\n    modifier: Modifier,\n    state: ResponsiveDialogState,\n    onDismiss: () -> Unit,\n    enter: EnterTransition = slideInVertically { it },\n    exit: ExitTransition = slideOutVertically { it },\n    content: @Composable (ResponsiveDialogScope.() -> Unit)\n) {\n    val originalTransition = updateTransition(state.targetIsOpened, \"originalTransition\")\n\n    // it should be animated for the first time!\n    var isVisible by remember { mutableStateOf(false) }\n    val transition = updateTransition(isVisible, \"transition\")\n    // the reason I use || is to prevent state from closing too early\n    state.currentIsOpened = originalTransition.currentState || transition.currentState\n    LaunchedEffect(originalTransition.targetState) {\n        isVisible = originalTransition.targetState\n    }\n    val responsiveSize = rememberResponsiveWidth()\n    Box(\n        modifier.fillMaxSize(),\n    ) {\n        transition.AnimatedVisibility(\n            visible = { it },\n            enter = fadeIn(),\n            exit = fadeOut()\n        ) {\n            Box(\n                Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = 0.5f))\n                    .silentClickable(onClick = onDismiss)\n            )\n        }\n        val widthFraction: Float\n        val alignment: Alignment\n        when (responsiveSize) {\n            ResponsiveTarget.Phone -> {\n                widthFraction = 1f\n                alignment = Alignment.BottomCenter\n            }\n\n            ResponsiveTarget.Tablet -> {\n                widthFraction = 0.75f\n                alignment = Alignment.Center\n            }\n\n            ResponsiveTarget.Desktop -> {\n                widthFraction = 0.5f\n                alignment = Alignment.Center\n            }\n        }\n\n        val responsiveDialogScope = ResponsiveDialogScopeImpl(\n            isTopStartFree = true,\n            isTopEndFree = true,\n            isBottomStartFree = alignment == Alignment.Center,\n            isBottomEndFree = alignment == Alignment.Center,\n        )\n\n        transition.AnimatedVisibility(\n            { it },\n            enter = enter,\n            exit = exit,\n            modifier = Modifier\n                .fillMaxWidth(widthFraction)\n                .align(alignment)\n                .hijackClick()\n        ) {\n            MPBackHandler(onBack = onDismiss)\n            Box(Modifier) {\n                content(responsiveDialogScope)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/BrowserIntegrationModel.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.util.compose.IconSource\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\n\nsealed class BrowserType(\n    val code:String\n){\n    data object Firefox : BrowserType(\"firefox\")\n    data object Chrome : BrowserType(\"chrome\")\n    data object Opera : BrowserType(\"opera\")\n    data object Edge : BrowserType(\"edge\")\n}\nfun BrowserType.getName():String{\n    return when(this){\n        BrowserType.Chrome -> \"Google Chrome\"\n        BrowserType.Edge -> \"Microsoft Edge\"\n        BrowserType.Firefox -> \"Mozilla Firefox\"\n        BrowserType.Opera -> \"Opera\"\n    }\n}\nfun BrowserType.getIcon(): IconSource {\n    return when(this){\n        BrowserType.Chrome -> MyIcons.browserGoogleChrome\n        BrowserType.Edge -> MyIcons.browserMicrosoftEdge\n        BrowserType.Firefox -> MyIcons.browserMozillaFirefox\n        BrowserType.Opera -> MyIcons.browserOpera\n    }\n}\n@Immutable\ndata class BrowserIntegrationModel(\n    val type: BrowserType,\n    val url:String,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ClipboardUtil.kt",
    "content": "package com.abdownloadmanager.shared.util\n\n\nexpect object ClipboardUtil {\n    fun read(): String?\n    fun copy(text: String)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ColorUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.ui.graphics.Color\n\noperator fun Color.div(percent: Int): Color = run {\n    require(percent in 0..100)\n    div(percent.toFloat() / 100)\n}\n\noperator fun Color.div(percent: Float): Color = run {\n    require(percent in 0f..1f)\n    copy(alpha = percent)\n}\n\nfun Color.lighter(amount: Float = 0.1f): Color {\n    return toHsl().apply { laminate += amount }.toColor()\n}\n\nfun Color.darker(amount: Float = 0.1f): Color {\n    return toHsl().apply { laminate -= amount }.toColor()\n}\n\n\n@JvmInline\nvalue class HSLColor(val hsl: FloatArray) {\n    constructor(hue: Float, saturation: Float, laminate: Float) : this(\n        floatArrayOf(hue, saturation, laminate)\n    )\n\n    constructor(hue: Int, saturation: Float, laminate: Float) : this(\n        floatArrayOf(hue / 360f, saturation, laminate)\n    )\n\n    fun copy(): HSLColor {\n        return HSLColor(hsl.copyOf())\n    }\n\n    override fun toString(): String {\n        return \"\"\"HSLColor( hue=$hue , saturation=$saturation , laminate=$laminate )\"\"\"\n    }\n\n    fun getHueInt(): Int {\n        return (hue * 360).toInt()\n    }\n\n    fun setHue(value: Int) {\n        val h = value.coerceIn(0..360)\n        hue = h / 360f\n    }\n\n    var hue\n        get() = hsl[0]\n        set(value) {\n            hsl[0] = value.coerceIn(0f, 1f)\n        }\n    var saturation\n        get() = hsl[1]\n        set(value) {\n            hsl[1] = value.coerceIn(0f, 1f)\n        }\n    var laminate\n        get() = hsl[2]\n        set(value) {\n            hsl[2] = value.coerceIn(0f, 1f)\n        }\n}\n\n\nfun Color.toHsl(): HSLColor {\n    val hsl = FloatArray(3)\n    val color = this\n\n    val r = color.red\n    val g = color.green\n    val b = color.blue\n\n    val max = maxOf(r, g, b)\n    val min = minOf(r, g, b)\n    hsl[2] = (max + min) / 2\n\n    if (max == min) {\n        hsl[1] = 0f\n        hsl[0] = hsl[1]\n\n    } else {\n        val d = max - min\n\n        hsl[1] = if (hsl[2] > 0.5f) d / (2f - max - min) else d / (max + min)\n        when (max) {\n            r -> hsl[0] = (g - b) / d + (if (g < b) 6 else 0)\n            g -> hsl[0] = (b - r) / d + 2\n            b -> hsl[0] = (r - g) / d + 4\n        }\n        hsl[0] /= 6f\n    }\n    return HSLColor(hsl)\n}\n\nfun HSLColor.toColor(): Color {\n    val hsl: FloatArray = this.hsl\n    val r: Float\n    val g: Float\n    val b: Float\n\n    val h = hsl[0]\n    val s = hsl[1]\n    val l = hsl[2]\n\n    if (s == 0f) {\n        b = l\n        g = b\n        r = g\n    } else {\n        val q = if (l < 0.5f) l * (1 + s) else l + s - l * s\n        val p = 2 * l - q\n        r = hue2rgb(p, q, h + 1f / 3)\n        g = hue2rgb(p, q, h)\n        b = hue2rgb(p, q, h - 1f / 3)\n    }\n\n    return Color((r * 255).toInt(), (g * 255).toInt(), (b * 255).toInt())\n}\n\nprivate fun hue2rgb(p: Float, q: Float, t: Float): Float {\n    var valueT = t\n    if (valueT < 0) valueT += 1f\n    if (valueT > 1) valueT -= 1f\n    if (valueT < 1f / 6) return p + (q - p) * 6f * valueT\n    if (valueT < 1f / 2) return q\n    return if (valueT < 2f / 3) p + (q - p) * (2f / 3 - valueT) * 6f else p\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ContainsShortcuts.kt",
    "content": "package com.abdownloadmanager.shared.util\n\ninterface ContainsShortcuts {\n    val shortcutManager: ShortcutManager\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/CoroutineUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.job\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\nfun newScopeBasedOn(\n    scope: CoroutineScope,\n    extraContext: CoroutineContext = EmptyCoroutineContext\n): CoroutineScope {\n    return CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext.job) + extraContext)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DefinedPaths.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport okio.Path\nimport java.io.File\nimport kotlin.io.resolve\n\nabstract class DefinedPaths(\n    val dataDir: Path,\n) {\n    val configDir: Path = dataDir.resolve(\"config\")\n    val systemDir: Path = dataDir.resolve(\"system\")\n    val updateDir: Path = systemDir.resolve(\"update\")\n    val logDir: Path = systemDir.resolve(\"log\")\n    val pagesStateDir: Path = configDir.resolve(\"pages\")\n    val optionsDir: Path = configDir.resolve(\"options\")\n    val downloadDbDir: Path = configDir.resolve(\"download_db\")\n    val downloadListDir = downloadDbDir.resolve(\"downloadlist\")\n    val extraDownloadSettings: Path = downloadDbDir.resolve(\"extra_download_settings\")\n    val extraQueueSettings: Path = downloadDbDir.resolve(\"extra_queue_settings\")\n    val categoriesDir: Path = downloadDbDir.resolve(\"categories\")\n    val categoriesFile: Path = categoriesDir.resolve(\"categories.json\")\n\n    val partsDir: Path = downloadDbDir.resolve(\"parts\")\n    val updateDownloadLocation: Path = updateDir.resolve(\"downloads\")\n\n    val downloadDataDir: Path = systemDir.resolve(\"downloadData\")\n    val queuesDir: Path = downloadDbDir.resolve(\"queues\")\n\n    val proxySettingsFile: Path = optionsDir.resolve(\"proxySettings.json\")\n    val appSettingsFile: Path = configDir.resolve(\"appSettings.json\")\n    val perHostSettingsFile: Path = optionsDir.resolve(\"perHostSettings.json\")\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DesktopDiskStat.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.downloader.utils.IDiskStat\n\nexpect class PlatformDiskStat : IDiskStat\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DownloadFoldersRegistry.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport okio.Path\nimport java.io.File\n\n/**\n * this is used to boot when file permission are granted!\n */\nclass DownloadFoldersRegistry {\n    private val foldersToCreate = mutableListOf<File>()\n    fun boot() {\n//        println(\"folder registery is $this\")\n        foldersToCreate.forEach {\n            it.mkdirs()\n        }\n    }\n\n    override fun toString(): String {\n        return foldersToCreate.map {\n            it.absolutePath\n        }.joinToString(\"\\n\").let {\n            \"DownloadFoldersRegistry(\\nlist=$it\\n)\"\n        }\n    }\n    fun registerAndGet(folder: Path): File {\n        val file = folder.toFile()\n        foldersToCreate.add(file)\n        return file\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DownloadItemOpener.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\ninterface DownloadItemOpener {\n    suspend fun openDownloadItem(id:Long)\n    suspend fun openDownloadItem(downloadItem: IDownloadItem)\n\n    suspend fun openDownloadItemFolder(id:Long)\n    suspend fun openDownloadItemFolder(downloadItem: IDownloadItem)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DownloadSystem.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.storage.IExtraDownloadSettingsStorage\nimport com.abdownloadmanager.shared.storage.IExtraQueueSettingsStorage\nimport com.abdownloadmanager.shared.util.category.CategoryItemWithId\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.CategorySelectionMode\nimport com.abdownloadmanager.shared.util.ondownloadcompletion.OnDownloadCompletionActionRunner\nimport com.abdownloadmanager.shared.util.onqueuecompletion.OnQueueEventActionRunner\nimport ir.amirab.downloader.DownloadManager\nimport ir.amirab.downloader.NewDownloadItemProps\nimport ir.amirab.downloader.db.IDownloadListDb\nimport ir.amirab.downloader.downloaditem.*\nimport ir.amirab.downloader.downloaditem.contexts.ResumedBy\nimport ir.amirab.downloader.downloaditem.contexts.StoppedBy\nimport ir.amirab.downloader.downloaditem.contexts.User\nimport ir.amirab.downloader.downloaditem.DownloadStatus\nimport ir.amirab.downloader.monitor.IDownloadItemState\nimport ir.amirab.downloader.monitor.IDownloadMonitor\nimport ir.amirab.downloader.monitor.ProcessingDownloadItemState\nimport ir.amirab.downloader.monitor.isDownloadActiveFlow\nimport ir.amirab.downloader.queue.ManualDownloadQueue\nimport ir.amirab.downloader.queue.QueueManager\nimport ir.amirab.downloader.utils.OnDuplicateStrategy\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport java.io.File\n\n/**\n * a facade for download manager library\n * all the download manager features should be accessed and controlled here\n */\nclass DownloadSystem(\n    val downloadManager: DownloadManager,\n    val queueManager: QueueManager,\n    val manualDownloadQueue: ManualDownloadQueue,\n    val categoryManager: CategoryManager,\n    val downloadMonitor: IDownloadMonitor,\n    val onDownloadCompletionActionRunner: OnDownloadCompletionActionRunner,\n    val onQueueEventActionRunner: OnQueueEventActionRunner,\n    private val scope: CoroutineScope,\n    private val downloadListDB: IDownloadListDb,\n    private val extraQueueSettingsStorage: IExtraQueueSettingsStorage<*>,\n    private val extraDownloadSettingsStorage: IExtraDownloadSettingsStorage<*>,\n    private val foldersRegistry: DownloadFoldersRegistry,\n) {\n    private val booted = MutableStateFlow(false)\n\n    val downloadEvents = downloadManager.listOfJobsEvents\n\n    suspend fun boot() {\n        if (booted.value) return\n        foldersRegistry.boot()\n        queueManager.boot()\n        downloadManager.boot()\n        categoryManager.boot()\n        manualDownloadQueue.boot()\n        onDownloadCompletionActionRunner.startListening()\n        onQueueEventActionRunner.startListening()\n        booted.update { true }\n    }\n\n    suspend fun addDownload(\n        newItemsToAdd: List<NewDownloadItemProps>,\n        queueId: Long? = null,\n        categorySelectionMode: CategorySelectionMode? = null,\n    ): List<Long> {\n        val createdIds = newItemsToAdd.map {\n            downloadManager.addDownload(it)\n        }\n        createdIds.also { ids ->\n            queueId?.let {\n                queueManager.addToQueue(\n                    it, ids\n                )\n            }\n        }\n        categorySelectionMode?.let {\n            when (it) {\n                CategorySelectionMode.Auto -> {\n                    categoryManager.autoAddItemsToCategoriesBasedOnFileNames(\n                        createdIds.mapIndexed { index: Int, id: Long ->\n                            val downloadItem = newItemsToAdd[index].downloadItem\n                            CategoryItemWithId(\n                                id = id,\n                                fileName = downloadItem.name,\n                                url = downloadItem.link,\n                            )\n                        }\n                    )\n                }\n\n                is CategorySelectionMode.Fixed -> {\n                    categoryManager.addItemsToCategory(\n                        it.categoryId,\n                        createdIds,\n                    )\n                }\n            }\n        }\n        return createdIds\n    }\n\n    suspend fun addDownload(\n        newDownload: NewDownloadItemProps,\n        queueId: Long?,\n        categoryId: Long?,\n    ): Long {\n        val downloadId = downloadManager.addDownload(newDownload)\n        queueId?.let {\n            queueManager.addToQueue(queueId, downloadId)\n        }\n        categoryId?.let {\n            categoryManager.addItemsToCategory(\n                categoryId = categoryId,\n                itemIds = listOf(downloadId)\n            )\n        }\n        return downloadId\n    }\n\n    suspend fun removeDownload(\n        id: Long,\n        alsoRemoveFile: Boolean,\n        context: DownloadItemContext,\n    ) {\n        downloadManager.deleteDownload(\n            id = id,\n            alsoRemoveFile = {\n                alsoRemoveFile\n            },\n            context = context\n        )\n        categoryManager.removeItemInCategories(listOf(id))\n        extraDownloadSettingsStorage.deleteExtraDownloadItemSettings(id)\n    }\n\n    suspend fun userManualResume(id: Long): Boolean {\n        manualDownloadQueue.resume(id)\n        return true\n    }\n\n    suspend fun manualResume(id: Long, context: DownloadItemContext): Boolean {\n        // it won't go though headless queue\n        // to respect the max concurrent limits\n        downloadManager.resume(id, context)\n        return true\n    }\n\n    suspend fun reset(id: Long): Boolean {\n        downloadManager.reset(id)\n        return true\n    }\n\n    suspend fun manualPause(id: Long): Boolean {\n        manualDownloadQueue.pause(id)\n        return true\n    }\n\n    suspend fun startQueue(\n        queueId: Long,\n    ) {\n        val queue = queueManager.getQueue(queueId)\n        if (queue.isQueueActive) {\n            return\n        }\n//      going to start\n        queue.start()\n    }\n\n    suspend fun stopAnything() {\n        queueManager.getAll().forEach {\n            it.stop()\n        }\n        manualDownloadQueue.clearQueue()\n        downloadManager.stopAll()\n    }\n\n    suspend fun stopQueue(\n        queueId: Long,\n    ) {\n        queueManager.getQueue(queueId)\n            .stop()\n    }\n\n    suspend fun getDownloadItemById(id: Long): IDownloadItem? {\n        return downloadListDB.getById(id) ?: return null\n    }\n\n    suspend fun getDownloadItemByLink(link: String): List<IDownloadItem> {\n        return downloadListDB.getAll().filter {\n            it.link == link\n        }\n    }\n\n    suspend fun getDownloadItemsBy(selector: (IDownloadItem) -> Boolean): List<IDownloadItem> {\n        return downloadListDB.getAll().filter(selector)\n    }\n\n    suspend fun getOrCreateDownloadByLink(\n        downloadItem: IDownloadItem,\n    ): Long {\n        val items = getDownloadItemByLink(downloadItem.link)\n        if (items.isNotEmpty()) {\n            val completedFound = items.find { it.status == DownloadStatus.Completed }\n            if (completedFound != null) {\n                return completedFound.id\n            }\n            val id = items.sortedByDescending { it.dateAdded }.first().id\n            return id\n        }\n        val id = addDownload(\n            newDownload = NewDownloadItemProps(\n                downloadItem = downloadItem,\n                onDuplicateStrategy = OnDuplicateStrategy.AddNumbered,\n                extraConfig = null,\n                context = EmptyContext,\n            ),\n            queueId = null,\n            categoryId = null,\n        )\n        return id\n    }\n\n    fun getDownloadFile(downloadItem: IDownloadItem): File {\n        return downloadManager.calculateOutputFile(downloadItem)\n    }\n\n    fun getDownloadItemByPath(path: String): IDownloadItemState? {\n        return downloadMonitor.downloadListFlow.value.find {\n            it.getFullPath().path == path\n        }\n    }\n\n    fun getDownloadItemsByFolder(folder: String): List<IDownloadItemState> {\n        return downloadMonitor.downloadListFlow.value.filter {\n            it.folder == folder\n        }\n    }\n\n\n    suspend fun getFilePathById(id: Long): File? {\n        val item = getDownloadItemById(id) ?: return null\n        return downloadManager.calculateOutputFile(item)\n    }\n\n    fun addQueue(name: String) {\n        scope.launch {\n            queueManager.addQueue(name)\n        }\n    }\n\n    fun getAllDownloadIds(): List<Long> {\n        return getUnfinishedDownloadIds() + getFinishedDownloadIds()\n    }\n\n    fun getFinishedDownloadIds(): List<Long> {\n        return downloadMonitor.completedDownloadListFlow.value.map {\n            it.id\n        }\n    }\n\n    fun getUnfinishedDownloadIds(): List<Long> {\n        return downloadMonitor.activeDownloadListFlow.value.map {\n            it.id\n        }\n    }\n\n    fun isDownloadMissingFileOrHaveNotProgress(downloadItem: IDownloadItemState): Boolean {\n        val missingFileBypass = if (downloadItem is ProcessingDownloadItemState) {\n            // some downloads not started yet so there is no file belong to them, so we shouldn't remove them\n            downloadItem.hasProgress\n        } else {\n            // finished downloads can be removed\n            true\n        }\n        return missingFileBypass && !downloadItem.getFullPath().exists()\n    }\n\n    fun getListOfDownloadThatMissingFileOrHaveNotProgress(): List<IDownloadItemState> {\n        val downloads = downloadMonitor.downloadListFlow.value\n        return downloads.filter {\n            isDownloadMissingFileOrHaveNotProgress(it)\n        }\n    }\n\n    fun getAllRegisteredDownloadFiles(): List<File> {\n        return downloadMonitor.run {\n            activeDownloadListFlow.value + completedDownloadListFlow.value\n        }.map {\n            File(it.folder, it.name)\n        }\n    }\n\n    suspend fun isDownloadActive(id: Long): Boolean {\n        return downloadMonitor.isDownloadActiveFlow(id).value\n    }\n\n    suspend fun editDownload(\n        id: Long,\n        applyUpdate: (IDownloadItem) -> Unit,\n        downloadJobExtraConfig: DownloadJobExtraConfig?\n    ) {\n        val wasActive = isDownloadActive(id)\n        if (wasActive) {\n            manualPause(id)\n        }\n        downloadManager.updateDownloadItem(\n            id = id,\n            downloadJobExtraConfig = downloadJobExtraConfig,\n            updater = applyUpdate,\n        )\n        if (wasActive) {\n            userManualResume(id)\n        }\n    }\n\n    suspend fun deleteQueue(queueId: Long) {\n        queueManager.deleteQueue(queueId)\n        extraQueueSettingsStorage.deleteExtraQueueSettings(queueId)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/DurationUtil.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport kotlin.math.roundToInt\n\n/**\n * @param duration duration in seconds\n */\nfun convertDurationToHumanReadable(duration: Double): StringSource {\n    // omit fractional section\n    val duration = duration.roundToInt()\n    val seconds = duration % 60\n    val minutes = (duration / 60) % 60\n    val hours = duration / 3600\n    return if (hours > 0) {\n        String.format(\"%02d:%02d:%02d\", hours, minutes, seconds)\n    } else {\n        String.format(\"%02d:%02d\", minutes, seconds)\n    }.asStringSource()\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ExceptionToString.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nfun exceptionToString(exception: Exception): String {\n    return exception.message?:exception::class.qualifiedName?:\"Unknown Error\"\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/FileIconProvider.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport com.abdownloadmanager.shared.util.category.CategoryManager\nimport com.abdownloadmanager.shared.util.category.DefaultCategories\nimport com.abdownloadmanager.shared.util.category.iconSource\nimport com.abdownloadmanager.shared.util.ui.IMyIcons\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.IconSource\n\n\ninterface FileIconProvider {\n    fun getIcon(fileName: String): IconSource\n\n    /**\n     * Automatically update icon if other dependencies changed\n     */\n    @Composable\n    fun rememberIcon(fileName: String): IconSource\n}\n\nclass FileIconProviderUsingCategoryIcons(\n    private val defaultCategories: DefaultCategories,\n    private val categoryManager: CategoryManager,\n    private val icons: IMyIcons,\n    private val iconResolver: IIconResolver,\n) : FileIconProvider {\n    override fun getIcon(fileName: String): IconSource {\n        return fromDefaultCategories(fileName)\n            ?: fromUserDefinedCategories(fileName)\n            ?: icons.file\n    }\n\n    @Composable\n    override fun rememberIcon(fileName: String): IconSource {\n        val fromDefault = remember(fileName) {\n            fromDefaultCategories(fileName)\n        }\n        if (fromDefault != null) {\n            return fromDefault\n        }\n        val categories by categoryManager.categoriesFlow.collectAsState()\n        val fromCategories = remember(fileName, categories) {\n            fromUserDefinedCategories(fileName)\n        }\n        if (fromCategories != null) {\n            return fromCategories\n        }\n        return icons.file\n    }\n\n    private fun fromDefaultCategories(fileName: String): IconSource? {\n        return defaultCategories\n            .getCategoryOfFileName(fileName)?.iconSource(iconResolver)\n    }\n\n    private fun fromUserDefinedCategories(fileName: String): IconSource? {\n        return categoryManager\n            .getCategoryOfFileName(fileName)?.iconSource(iconResolver)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/FilenameFixer.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.util.ifThen\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isWindows\n\n/**\n * This utility class removes characters that are not supported by the current OS.\n *\n * If additional modifications are required, it may be better to create a separate class for each platform.\n */\nobject FilenameFixer {\n    private const val DEFAULT_REPLACEMENT_CHAR = \"_\"\n    private val illegalChars by lazy {\n        when (Platform.getCurrentPlatform()) {\n            Platform.Desktop.Windows -> setOf('<', '>', ':', '\"', '/', '\\\\', '|', '?', '*')\n            Platform.Desktop.MacOS -> setOf(':')\n            Platform.Desktop.Linux,\n            Platform.Android,\n                -> setOf('/')\n        }\n    }\n\n    fun fix(name: String): String {\n        return buildString {\n            name.forEach { char ->\n                append(\n                    if (char in illegalChars) {\n                        DEFAULT_REPLACEMENT_CHAR\n                    } else {\n                        char\n                    }\n                )\n            }\n        }\n            .ifThen(Platform.isWindows()) {\n                trimEnd(' ', '.')\n            }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/HashUtil.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.downloader.utils.calcPercent\nimport java.io.File\nimport java.io.InputStream\nimport java.security.MessageDigest\n\nsealed class FileChecksumAlgorithm(\n    val algorithm: String,\n) {\n    data object MD5 : FileChecksumAlgorithm(\"MD5\")\n    data object SHA1 : FileChecksumAlgorithm(\"SHA-1\")\n    data object SHA256 : FileChecksumAlgorithm(\"SHA-256\")\n    data object SHA512 : FileChecksumAlgorithm(\"SHA-512\")\n\n    companion object {\n        fun default() = SHA256\n        fun all() = listOf(\n            MD5,\n            SHA1,\n            SHA256,\n            SHA512,\n        )\n    }\n}\n\ndata class FileChecksum(\n    val algorithm: String,\n    val value: String,\n) {\n\n    override fun toString(): String {\n        return \"$algorithm:$value\"\n    }\n\n    companion object {\n        fun fromString(string: String): FileChecksum {\n            val segments = string.split(\":\")\n            require(segments.size == 2) {\n                \"Invalid checksum string: $string it should be in format algorithm:value\"\n            }\n            return FileChecksum(\n                algorithm = segments[0],\n                value = segments[1],\n            )\n        }\n\n        fun fromNullableString(string: String?): FileChecksum? {\n            return string?.let {\n                fromString(it)\n            }\n        }\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as FileChecksum\n        return algorithm.equals(other.algorithm, true) && value.equals(other.value, true)\n    }\n\n    override fun hashCode(): Int {\n        var result = algorithm.hashCode()\n        result = 31 * result + value.hashCode()\n        return result\n    }\n}\n\nobject HashUtil {\n    fun hash(\n        algorithm: String,\n        inputStream: InputStream,\n        size: Long,\n        onNewPercent: (Int) -> Unit,\n    ): String {\n        val messageDigest = MessageDigest.getInstance(algorithm)\n        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)\n        var processedBytes = 0L\n        var lastPercent = 0\n        while (true) {\n            val readCount = inputStream.read(buffer)\n            if (readCount == -1) {\n                break\n            }\n            messageDigest.update(buffer, 0, readCount)\n            processedBytes += readCount\n            val newPercent = calcPercent(processedBytes, size)\n            if (newPercent != lastPercent) {\n                onNewPercent(newPercent)\n                lastPercent = newPercent\n            }\n        }\n        return messageDigest\n            .digest()\n            .joinToString(\"\") {\n                \"%02x\".format(it)\n            }\n    }\n\n    fun fileHash(\n        algorithm: String,\n        file: File,\n        onNewPercent: (Int) -> Unit\n    ): String {\n        val fileSize = file.length()\n        return file.inputStream().use {\n            hash(\n                algorithm = algorithm,\n                inputStream = it,\n                size = fileSize,\n                onNewPercent = onNewPercent\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/IPUtils.kt",
    "content": "@file:OptIn(ExperimentalUnsignedTypes::class)\n@file:Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\n\npackage com.abdownloadmanager.shared.util\n\nimport java.net.NetworkInterface\n\nfun getSubnet(): List<SubnetAddress> {\n    return runCatching {\n        NetworkInterface.getNetworkInterfaces()\n            .toList()\n            .flatMap { networkInterface ->\n                networkInterface.interfaceAddresses\n            }.filter {\n                !it.address.isLinkLocalAddress\n                        && !it.address.isLoopbackAddress\n                        && it.networkPrefixLength > 0\n            }.mapNotNull {\n                val ip = ByteIp(it.address.address)\n                SubnetAddress(\n                    ip,\n                    it.networkPrefixLength\n                )\n            }\n    }.getOrElse {\n        emptyList()\n    }\n}\n\nabstract class Ip {\n    abstract val intIp: UInt\n    abstract val byteIp: UByteArray\n\n    fun toByteIp() = ByteIp(byteIp)\n    fun toIntIp() = IntIp(intIp)\n    override fun toString(): String {\n        return byteIp.map { it }.joinToString(\".\")\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other === this) return true\n        if (other is Ip) {\n            return other.intIp == intIp\n        }\n        return false\n    }\n\n    override fun hashCode(): Int {\n        return intIp.hashCode()\n    }\n}\n\nclass IntIp(\n    ip: UInt\n) : Ip() {\n    override val intIp by lazy { ip }\n\n    override val byteIp by lazy {\n        UByteArray(4) {\n            val bitCount = (3 - it) * 8\n            (ip shr bitCount).toUByte()\n        }\n    }\n}\n\n@JvmName(\"ByteIpFromByteArrayVararg\")\nfun ByteIp(ip: ByteArray): ByteIp {\n    return ByteIp(\n        ip.map { it.toUByte() }.toUByteArray()\n    )\n}\n\n@JvmName(\"ByteIpFromByteVararg\")\nfun ByteIp(vararg ip: Byte): ByteIp {\n    return ByteIp(ip)\n}\n\n@JvmName(\"ByteIpFromUByteVararg\")\nfun ByteIp(vararg ip: UByte): ByteIp {\n    return ByteIp(ip)\n}\n\n@JvmName(\"ByteIpFromIntVararg\")\nfun ByteIp(vararg ip: Int): ByteIp {\n    return ByteIp(\n        ip.map {\n            it.toUByte()\n        }.toUByteArray()\n    )\n}\n\nclass ByteIp(\n    ip: UByteArray\n) : Ip() {\n    init {\n        require(ip.size == 4)\n    }\n\n    override val intIp: UInt by lazy {\n        ip.foldIndexed(0u) { index, acc, byte ->\n            val int = byte.toUInt()\n            val bitCount = ((3 - index) * 8)\n            val shifted = int shl bitCount\n            acc.or(shifted)\n        }\n    }\n    override val byteIp by lazy { ip }\n}\n\n\nclass SubnetAddress(\n    val ip: Ip,\n    val prefix: Short\n) {\n\n\n    override fun toString(): String {\n        return \"$network/$prefix\"\n    }\n\n\n    val mask: UInt by lazy {\n        val full = UInt.MAX_VALUE\n        full shl (32 - prefix)\n    }\n    val network: Ip by lazy {\n        IntIp(ip.intIp and mask)\n    }\n\n    val broadcast: Ip by lazy {\n        IntIp(network.intIp or mask.inv())\n    }\n\n    private val subnetIntRange = ((network.intIp + 1u) until broadcast.intIp)\n    val subnetRangeSize by lazy {\n        (subnetIntRange.last - subnetIntRange.first)\n    }\n\n    val subnetIpRange by lazy {\n        subnetIntRange.asSequence().map {\n            IntIp(it)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/InstanceCheckUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nfun <T> T.isAnyOf(vararg conditions: (T) -> Boolean): Boolean {\n    return conditions.any {\n        it(this)\n    }\n}\n\nfun <T> T.isAllOf(vararg conditions: (T) -> Boolean): Boolean {\n    return conditions.all {\n        it(this)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/LimitationsInUI.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nobject ThreadCountLimitation {\n    const val MAX_ALLOWED_THREAD_COUNT = 256\n    const val MAX_NORMAL_VALUE = 32\n}\nobject MaximumDownloadRetriesLimitation {\n    const val MAX_ALLOWED_RETRIES = 1024\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/Platform.kt",
    "content": "package com.abdownloadmanager.shared.util\n\n//expect object Platform {\n//    val type: OSInfo.OSType\n//}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/PlatformKeyStroke.kt",
    "content": "package com.abdownloadmanager.shared.util\n\ninterface PlatformKeyStroke {\n    val keyCode: Int\n\n    fun getModifiers(): List<String>\n    fun getKeyText(): String\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/PlatformThemeDetector.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\n\nexpect class PlatformThemeDetector : ISystemThemeDetector\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/PopUpContainer.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.*\nimport java.util.*\n\nprivate val LocalBottomSheetContainer =\n    staticCompositionLocalOf<MutableList<Pair<Any, @Composable () -> Unit>>> { error(\"not initialized yet\") }\n\n@Composable\nfun PlaceInHost(\n    item: @Composable () -> Unit\n) {\n    val key = remember {\n        UUID.randomUUID()\n    }\n    val container = LocalBottomSheetContainer.current\n    DisposableEffect(key) {\n        val item = key to item\n        container.add(item)\n        onDispose {\n            container.remove(item)\n        }\n    }\n}\n\n\n@Composable\nfun PopUpContainer(\n    content: @Composable () -> Unit\n) {\n    val items = remember {\n        mutableStateListOf<Pair<Any, @Composable () -> Unit>>()\n    }\n    CompositionLocalProvider(\n        LocalBottomSheetContainer provides items,\n        content = {\n            content()\n            items.forEach { (key, content) ->\n                key(key) {\n                    content()\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/RememberDotLoading.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\n\n@Composable\nfun rememberDotLoading(): String {\n    val transition = rememberInfiniteTransition()\n    val count by transition.animateValue(\n        1,\n        4,\n        Int.VectorConverter,\n        infiniteRepeatable(\n            tween(2500, easing = LinearEasing),\n            repeatMode = RepeatMode.Restart\n        )\n    )\n    return buildString {\n        for (i in 1..3) {\n            if (i <= count) {\n                append(\".\")\n            } else {\n                append(\" \")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/Responsive.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.BoxWithConstraintsScope\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\ninterface ResponsiveSize {\n    val maxHeight: Dp\n    val maxWidth: Dp\n}\n\n@Immutable\ndata class WindowSize(\n    override val maxHeight: Dp,\n    override val maxWidth: Dp,\n) : ResponsiveSize\n\n@Immutable\ndata class ContainerSize(\n    override val maxHeight: Dp,\n    override val maxWidth: Dp,\n    private val containerName: String\n) : ResponsiveSize\n\n@Composable\nfun provideContainerSize(\n    maxHeight: Dp,\n    maxWidth: Dp,\n    name: String? = null\n): ProvidedValue<ContainerSize> {\n    return LocalContainerSize provides ContainerSize(\n        maxHeight,\n        maxWidth,\n        name ?: \"not specified container\"\n    )\n}\n\n@Composable\nfun provideWindowSize(\n    maxHeight: Dp,\n    maxWidth: Dp,\n): ProvidedValue<WindowSize> {\n    return LocalWindowSize provides WindowSize(maxHeight, maxWidth)\n}\n\n@Composable\nfun ResponsiveBox(content: @Composable BoxWithConstraintsScope.() -> Unit) {\n    BoxWithConstraints {\n        CompositionLocalProvider(\n            provideContainerSize(maxHeight, maxWidth),\n        ) {\n            content()\n        }\n    }\n}\n\nenum class ResponsiveTarget {\n    Phone,\n    Tablet,\n    Desktop,\n}\n\n@Composable\nfun rememberResponsiveWidth(): ResponsiveTarget {\n    val width = LocalContainerSize.current.run {\n        minOf(maxWidth, maxHeight)\n    }\n    return when (width) {\n        in 0.dp..599.dp -> ResponsiveTarget.Phone\n        in 600.dp..1199.dp -> ResponsiveTarget.Tablet\n        else -> ResponsiveTarget.Desktop\n    }\n}\n\n\nval LocalWindowSize = compositionLocalOf<WindowSize> { error(\"not initialized\") }\nval LocalContainerSize = compositionLocalOf<ContainerSize> { error(\"not initialized\") }\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/SharedConstants.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.BuildConfig\nimport com.abdownloadmanager.shared.util.BaseConstants\nimport com.abdownloadmanager.shared.util.BrowserIntegrationModel\nimport com.abdownloadmanager.shared.util.BrowserType\n\nobject SharedConstants : BaseConstants {\n    override val appName: String = BuildConfig.APP_NAME\n    override val appDisplayName: String = BuildConfig.APP_DISPLAY_NAME\n    override val packageName: String = BuildConfig.PACKAGE_NAME\n    override val dataDirName: String = BuildConfig.DATA_DIR_NAME\n    override val projectWebsite: String = BuildConfig.PROJECT_WEBSITE\n    override val projectTranslations: String = BuildConfig.PROJECT_TRANSLATIONS\n    override val projectSourceCode: String = BuildConfig.PROJECT_SOURCE_CODE\n    override val donateLink: String = BuildConfig.DONATE_LINK\n    override val projectGithubOwner: String = BuildConfig.PROJECT_GITHUB_OWNER\n    override val projectGithubRepo: String = BuildConfig.PROJECT_GITHUB_REPO\n    override val browserIntegrations: List<BrowserIntegrationModel> = listOf(\n        BrowserIntegrationModel(\n            BrowserType.Chrome, BuildConfig.INTEGRATION_CHROME_LINK\n        ),\n        BrowserIntegrationModel(\n            BrowserType.Firefox, BuildConfig.INTEGRATION_FIREFOX_LINK\n        )\n    )\n    override val telegramChannelUrl: String = BuildConfig.TELEGRAM_CHANNEL\n    override val telegramGroupUrl: String = BuildConfig.TELEGRAM_GROUP\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ShortcutManager.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.input.key.key\nimport javax.swing.KeyStroke\n\nabstract class ShortcutManager {\n    private val shortcuts = mutableMapOf<PlatformKeyStroke, () -> Unit>()\n    fun register(keyStroke: PlatformKeyStroke, action: () -> Unit) {\n        shortcuts[keyStroke] = action\n    }\n\n    abstract fun stringToKeyStroke(keyStrokeString: String): PlatformKeyStroke\n    abstract fun getKeyStrokeFromEvent(s: KeyEvent): PlatformKeyStroke?\n    abstract fun getKeyStrokeFromKeyCode(keyCode: Int): PlatformKeyStroke?\n\n    infix fun String.to(action: () -> Unit) {\n        register(stringToKeyStroke(this), action)\n    }\n\n    infix fun Int.to(action: () -> Unit) {\n        getKeyStrokeFromKeyCode(this)?.let {\n            register(it, action)\n        }\n    }\n\n    fun executeShortcut(\n        keyStroke: PlatformKeyStroke,\n    ): Boolean {\n        val action = shortcuts[keyStroke] ?: return false\n        runCatching {\n            action()\n        }\n        return true\n    }\n\n    fun getShortCutOf(action: () -> Unit): PlatformKeyStroke? {\n        return shortcuts.firstNotNullOfOrNull {\n            if (it.value == action) {\n                it.key\n            } else null\n        }\n    }\n\n    fun handle(event: KeyEvent): Boolean {\n        val keyStroke = getKeyStrokeFromEvent(event) ?: return false\n        return executeShortcut(keyStroke)\n    }\n}\n\nval LocalShortCutManager = compositionLocalOf {\n    null as ShortcutManager?\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ShouldValidate.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport kotlinx.coroutines.flow.StateFlow\n\ninterface ShouldValidate {\n    val valid: StateFlow<Boolean>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/SizeAndSpeedUnitProvider.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.util.datasize.ConvertSizeConfig\nimport kotlinx.coroutines.flow.StateFlow\n\ninterface SizeAndSpeedUnitProvider {\n    val sizeUnit: StateFlow<ConvertSizeConfig>\n    val speedUnit: StateFlow<ConvertSizeConfig>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/SizeUtil.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.util.datasize.CommonSizeConvertConfigs\nimport ir.amirab.util.datasize.ConvertSizeConfig\nimport ir.amirab.util.datasize.SizeWithUnit\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSource\nimport ir.amirab.util.datasize.*\nimport ir.amirab.util.datasize.SizeWithUnit.Companion.DefaultFormat\nimport ir.amirab.util.datasize.SizeWithUnit.Companion.SmallFormat\nimport java.text.NumberFormat\n\nval LocalSpeedUnit = compositionLocalOf {\n    CommonSizeConvertConfigs.BinaryBytes\n}\nval LocalSizeUnit = compositionLocalOf {\n    CommonSizeConvertConfigs.BinaryBytes\n}\n\n@Composable\nfun ProvideSizeAndSpeedUnit(\n    sizeUnitConfig: ConvertSizeConfig,\n    speedUnitConfig: ConvertSizeConfig,\n    content: @Composable () -> Unit,\n) {\n    CompositionLocalProvider(\n        LocalSpeedUnit provides speedUnitConfig,\n        LocalSizeUnit provides sizeUnitConfig,\n        content = content\n    )\n}\n\n\n// they are used for ui\n// size == -1 means that its unknown\n\nfun convertPositiveBytesToSizeUnit(\n    size: Long,\n    target: ConvertSizeConfig,\n): SizeWithUnit? {\n    if (size < 0) return null\n    return SizeConverter.bytesToSize(\n        bytes = size,\n        target = target,\n    )\n}\n\nfun convertPositiveBytesToHumanReadable(\n    size: Long,\n    target: ConvertSizeConfig,\n    asCompactAsPossible: Boolean = false,\n): String? {\n    val format = if (asCompactAsPossible) SmallFormat else DefaultFormat\n    return convertPositiveBytesToSizeUnit(size, target)\n        ?.let {\n            buildString {\n                append(it.formatedValue(format))\n                if (!asCompactAsPossible) {\n                    append(\" \")\n                }\n                append(it.unit.toString())\n            }\n        }\n}\n\nfun convertPositiveSizeToHumanReadable(\n    size: Long,\n    target: ConvertSizeConfig,\n    asCompactAsPossible: Boolean = false,\n): StringSource {\n    return convertPositiveBytesToHumanReadable(size, target, asCompactAsPossible)\n        ?.asStringSource()\n        ?: Res.string.unknown.asStringSource()\n}\n\nfun convertPositiveSpeedToHumanReadable(size: Long, target: ConvertSizeConfig, perUnit: String = \"s\"): String {\n    return convertPositiveBytesToHumanReadable(size, target)\n        ?.let { \"$it/$perUnit\" }\n        ?: \"-\"\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/StateUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.*\nimport ir.amirab.util.flow.DerivedStateFlow\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\n\n@Composable\nfun <T> MutableStateFlow<T>.collectAsModifiableState(): MutableState<T> {\n    val upStream = this\n    val state = remember(this) {\n        mutableStateOf(upStream.value)\n    }\n    LaunchedEffect(this) {\n        upStream.onEach {\n            state.value = it\n        }.launchIn(this)\n        snapshotFlow { state.value }\n            .onEach { each ->\n                upStream.update { each }\n            }.launchIn(this)\n    }\n    return state\n}\n\nfun <T> MutableStateFlow<T>.asMutableState(\n    scope: CoroutineScope,\n): MutableState<T> {\n    val upStream = this\n    val state = mutableStateOf(upStream.value)\n    upStream.onEach {\n        state.value = it\n    }.launchIn(scope)\n    snapshotFlow { state.value }\n        .onEach { each ->\n            upStream.update { each }\n        }.launchIn(scope)\n    return state\n}\nfun <T> StateFlow<T>.asState(\n    scope: CoroutineScope,\n): State<T> {\n    val upStream = this\n    val state = mutableStateOf(upStream.value)\n    upStream.onEach {\n        state.value = it\n    }.launchIn(scope)\n    return state\n}\nfun <T> Flow<T>.asState(\n    scope: CoroutineScope,\n    initialValue:T,\n): MutableState<T> {\n    val upStream = this\n    val state = mutableStateOf(initialValue)\n    upStream.onEach {\n        state.value = it\n    }.launchIn(scope)\n    return state\n}\n\ninline fun <T>MutableState<T>.asState(): State<T> {\n    return this as State<T>\n}\nfun <T> State<T>.asStateFlow(): StateFlow<T> {\n    val getValue = { value }\n    return DerivedStateFlow(\n        getValue = getValue,\n        flow = snapshotFlow(getValue)\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/StringUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nfun String.takeOrAppendDots(takeCount: Int): String {\n    val take = take(takeCount)\n    if (length<=takeCount){\n        return take\n    }else{\n        return \"$take…\"\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/SystemDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport java.io.File\n\nabstract class SystemDownloadLocationProvider {\n\n    fun getDownloadLocation(): File {\n        return runCatching { getCurrentDownloadLocation() }\n            .onFailure { it.printStackTrace() }\n            .getOrNull() ?: getCommonDownloadLocation()\n    }\n\n    /**\n     * it should be a fixed path!\n     * this meant to be used as fallback\n     * - if the OS doesn't provide api to get download location dynamically\n     * - or the [getCurrentDownloadLocation] fails for some reason\n     * So, do your best to not throw exception here otherwise the [getDownloadLocation] will crash too!\n     */\n    protected abstract fun getCommonDownloadLocation(): File\n    protected abstract fun getCurrentDownloadLocation(): File?\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/TimeUtil.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.*\nimport com.abdownloadmanager.resources.Res\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.compose.asStringSourceWithARgs\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.datetime.DateTimePeriod\nimport kotlinx.datetime.LocalDateTime\nimport kotlin.math.absoluteValue\n\nfun formatTime(\n    value: Long,\n    forceUseHour: Boolean = false\n): String? {\n    if (value < 0) {\n        return null\n    }\n    //10_000_000\n    var remaining = value\n    //10_000\n    val _SEC = 1000\n    val _MIN = 60 * _SEC\n    val _HOUR = 60 * _MIN\n    val hour = remaining / _HOUR\n    remaining %= _HOUR\n    val min = remaining / _MIN\n    remaining %= _MIN\n    val sec = remaining / _SEC\n    val padded: (Long) -> String = {\n        \"$it\".padStart(2, '0')\n    }\n    return buildString {\n        if (hour == 0L || forceUseHour) {\n            append(hour)\n            append(\":\")\n        }\n        append(padded(min))\n        append(\":\")\n        append(padded(sec))\n    }\n}\n\nfun prettifyRelativeTime(\n    duration: DateTimePeriod,\n    count: Int = 1,\n    names: TimeNames = DefaultTimeNames,\n): String {\n    val years = duration.years.absoluteValue\n    val months = duration.months.absoluteValue\n    val days = duration.days.absoluteValue\n    val hours = duration.hours.absoluteValue\n    val minutes = duration.minutes.absoluteValue\n    val seconds = duration.seconds.absoluteValue\n\n    val isLater = arrayOf(\n        years,\n        months,\n        days,\n        hours,\n        minutes,\n        seconds,\n    ).any { it < 0 }\n    return prettifyRelativeTime(\n        years = years,\n        months = months,\n        days = days,\n        hours = hours,\n        minutes = minutes,\n        seconds = seconds,\n        isLater = isLater,\n        count = count,\n        names = names\n    )\n}\n\nfun prettifyRelativeTime(\n    years: Int = 0,\n    months: Int = 0,\n    days: Int = 0,\n    hours: Int = 0,\n    minutes: Int = 0,\n    seconds: Int = 0,\n    count: Int = 1,\n    names: TimeNames = DefaultTimeNames,\n    isLater: Boolean,\n): String {\n    val relativeTime = relativeTime(\n        years = years,\n        months = months,\n        days = days,\n        hours = hours,\n        minutes = minutes,\n        seconds = seconds,\n        count = count,\n        names = names,\n    )\n    return (if (isLater) {\n        names.left(relativeTime)\n    } else {\n        names.ago(relativeTime)\n    }).getString()\n}\n\nprivate fun relativeTime(\n    years: Int,\n    months: Int,\n    days: Int,\n    hours: Int,\n    minutes: Int,\n    seconds: Int,\n    count: Int = 6,\n    names: TimeNames = DefaultTimeNames,\n): String {\n    require(count > 0)\n    var used = 0\n    val relativeTime = buildString {\n        if (years > 0) {\n            used++\n            append(names.years(years).getString())\n        }\n        if (used == count) return@buildString\n        if (months > 0) {\n            if (used > 0) {\n                append(\" \")\n            }\n            used++\n            append(names.months(months).getString())\n        }\n        if (used == count) return@buildString\n        if (days > 0) {\n            if (used > 0) {\n                append(\" \")\n            }\n            used++\n            append(names.days(days).getString())\n        }\n        if (used == count) return@buildString\n        if (hours > 0) {\n            if (used > 0) {\n                append(\" \")\n            }\n            used++\n            append(names.hours(hours).getString())\n        }\n        if (used == count) return@buildString\n        if (minutes > 0) {\n            if (used > 0) {\n                append(\" \")\n            }\n            used++\n            append(names.minutes(minutes).getString())\n        }\n        if (used == count) return@buildString\n        if (seconds > 0) {\n            if (used > 0) {\n                append(\" \")\n            }\n            used++\n            append(names.seconds(seconds).getString())\n        }\n        if (used == count) return@buildString\n        if (used == 0) {\n            append(names.seconds(0).getString())\n        }\n    }\n    return relativeTime\n}\n\n\n@Composable\nfun rememberTimeFormatedValue(value: Long): String? {\n    return remember(value) {\n        formatTime(value)\n    }\n}\n\n@Composable\nfun rememberTimeEllapsed(value: Long): Long {\n    var result by remember { mutableStateOf(0L) }\n    LaunchedEffect(value) {\n        while (isActive) {\n            result = if (value < 0) {\n                -1\n            } else {\n                System.currentTimeMillis() - value\n            }\n            delay(1_000)\n        }\n    }\n    return result\n}\n\nfun convertTimeRemainingToHumanReadable(\n    totalSecs: Long,\n    timeNames: TimeNames = TimeNames.SimpleNames,\n): String {\n    val hours = totalSecs / 3600;\n    val minutes = (totalSecs % 3600) / 60;\n    val seconds = totalSecs % 60;\n    return prettifyRelativeTime(\n        hours = hours.toInt(),\n        minutes = minutes.toInt(),\n        seconds = seconds.toInt(),\n        isLater = true,\n        count = 3,\n        names = timeNames,\n    )\n}\n\n@Stable\ninterface TimeNames {\n    fun years(years: Int): StringSource\n    fun months(months: Int): StringSource\n    fun days(days: Int): StringSource\n    fun hours(hours: Int): StringSource\n    fun minutes(minutes: Int): StringSource\n    fun seconds(seconds: Int): StringSource\n    fun ago(time: String): StringSource\n    fun left(time: String): StringSource\n\n\n    @Stable\n    object SimpleNames : TimeNames {\n        override fun years(years: Int): StringSource = Res.string.relative_time_long_years.asStringSourceWithARgs(\n            Res.string.relative_time_long_years_createArgs(years = years.toString())\n        )\n\n        override fun months(months: Int): StringSource = Res.string.relative_time_long_months.asStringSourceWithARgs(\n            Res.string.relative_time_long_months_createArgs(months = months.toString())\n        )\n\n        override fun days(days: Int): StringSource =\n            Res.string.relative_time_long_days.asStringSourceWithARgs(Res.string.relative_time_long_days_createArgs(days = days.toString()))\n\n        override fun hours(hours: Int): StringSource = Res.string.relative_time_long_hours.asStringSourceWithARgs(\n            Res.string.relative_time_long_hours_createArgs(hours = hours.toString())\n        )\n\n        override fun minutes(minutes: Int): StringSource =\n            Res.string.relative_time_long_minutes.asStringSourceWithARgs(\n                Res.string.relative_time_long_minutes_createArgs(minutes = minutes.toString())\n            )\n\n        override fun seconds(seconds: Int): StringSource =\n            Res.string.relative_time_long_seconds.asStringSourceWithARgs(\n                Res.string.relative_time_long_seconds_createArgs(seconds = seconds.toString())\n            )\n\n        override fun left(time: String): StringSource =\n            Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))\n\n        override fun ago(time: String): StringSource =\n            Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))\n    }\n\n    object ShortNames : TimeNames {\n        override fun years(years: Int): StringSource = Res.string.relative_time_short_years.asStringSourceWithARgs(\n            Res.string.relative_time_short_years_createArgs(years = years.toString())\n        )\n\n        override fun months(months: Int): StringSource = Res.string.relative_time_short_months.asStringSourceWithARgs(\n            Res.string.relative_time_short_months_createArgs(months = months.toString())\n        )\n\n        override fun days(days: Int): StringSource = Res.string.relative_time_short_days.asStringSourceWithARgs(\n            Res.string.relative_time_short_days_createArgs(days = days.toString())\n        )\n\n        override fun hours(hours: Int): StringSource = Res.string.relative_time_short_hours.asStringSourceWithARgs(\n            Res.string.relative_time_short_hours_createArgs(hours = hours.toString())\n        )\n\n        override fun minutes(minutes: Int): StringSource =\n            Res.string.relative_time_short_minutes.asStringSourceWithARgs(\n                Res.string.relative_time_short_minutes_createArgs(minutes = minutes.toString())\n            )\n\n        override fun seconds(seconds: Int): StringSource =\n            Res.string.relative_time_short_seconds.asStringSourceWithARgs(\n                Res.string.relative_time_short_seconds_createArgs(seconds = seconds.toString())\n            )\n\n        override fun left(time: String): StringSource =\n            Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))\n\n        override fun ago(time: String): StringSource =\n            Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))\n    }\n}\n\nprivate val DefaultTimeNames = TimeNames.SimpleNames\n\nobject MyDateAndTimeFormats {\n    val fullDateTime = LocalDateTime.Format {\n        year()\n        chars(\"/\")\n        monthNumber()\n        chars(\"/\")\n        day()\n        chars(\" \")\n        hour()\n        chars(\":\")\n        minute()\n        chars(\":\")\n        second()\n    }\n    val fullDateTimeWithoutYearAndSeconds = LocalDateTime.Format {\n        monthNumber()\n        chars(\"/\")\n        day()\n        chars(\" \")\n        hour()\n        chars(\":\")\n        minute()\n    }\n}\n\nval LocalUseRelativeDateTime = staticCompositionLocalOf<Boolean> {\n    true\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/UiConstants.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nconst val DOUBLE_CLICK_DELAY = 500L\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/UserAgentProviderFromSettings.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.storage.BaseAppSettingsStorage\nimport ir.amirab.downloader.connection.UserAgentProvider\n\nclass UserAgentProviderFromSettings(\n    private val appSettingsStorage: BaseAppSettingsStorage\n) : UserAgentProvider {\n    override fun getUserAgent(): String? {\n        return appSettingsStorage.userAgent.value.takeIf { it.isNotBlank() }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ValueUtils.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport com.arkivanov.decompose.router.slot.ChildSlot\nimport com.arkivanov.decompose.value.Value\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\nfun <T : Any> Value<T>.subscribeAsStateFlow(): StateFlow<T> {\n    val stateFlow = MutableStateFlow(this.value)\n    subscribe {\n        stateFlow.value = it\n    }\n    return stateFlow\n}\n\n@Composable\nfun <C : Any, T : Any> StateFlow<ChildSlot<C, T>>.rememberChild(): T? {\n    return collectAsState().value.child?.instance\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/appinfo/PreviousVersion.kt",
    "content": "package com.abdownloadmanager.shared.util.appinfo\n\nimport io.github.z4kn4fein.semver.Version\nimport java.io.File\n\nclass PreviousVersion(\n    systemPath: File,\n    private val currentVersion: Version,\n) {\n    private val versionFile = File(systemPath, \".version\")\n    private var previousVersion: Version? = null\n    fun get(): Version? {\n        return previousVersion\n    }\n\n    fun boot() {\n        previousVersion = kotlin.runCatching {\n            // maybe versionFile is null but we catch it\n            val versionString = versionFile.readText()\n            Version.parse(versionString)\n        }.getOrNull()\n        kotlin.runCatching {\n            versionFile.parentFile.mkdirs()\n            versionFile.writeText(currentVersion.toString())\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/autoremove/RemovedDownloadsFromDiskTracker.kt",
    "content": "package com.abdownloadmanager.shared.util.autoremove\n\nimport com.abdownloadmanager.shared.util.DownloadSystem\nimport io.github.irgaly.kfswatch.KfsDirectoryWatcher\nimport io.github.irgaly.kfswatch.KfsEvent\nimport ir.amirab.downloader.downloaditem.contexts.CanPerformRemove\nimport ir.amirab.downloader.downloaditem.contexts.RemovedBy\nimport ir.amirab.downloader.monitor.*\nimport ir.amirab.util.flow.withPrevious\nimport ir.amirab.util.ifThen\nimport ir.amirab.util.osfileutil.FileUtils\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isWindows\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport java.io.File\n\n\nclass RemovedDownloadsFromDiskTracker(\n    private val downloadMonitor: IDownloadMonitor,\n    private val scope: CoroutineScope,\n    private val downloadSystem: DownloadSystem,\n) {\n    private fun createWatcher(\n        scope: CoroutineScope,\n    ): KfsDirectoryWatcher {\n        return KfsDirectoryWatcher(\n            scope = scope,\n        )\n    }\n\n    @Volatile\n    private var stopped = true\n\n    //download item ids that should be checked for existence after a delay\n    private var itemsToCheck = MutableStateFlow(emptySet<Long>())\n    private var activeJob: Job? = null\n\n    fun start() {\n        stopped = false\n\n        // it seems that if we watch a folder in a removable storage\n        // then it can't be ejected! so we have to ignore it\n        // we know that it happens in Windows!\n        val shouldFilterRemovableStorages = Platform.isWindows()\n\n        activeJob = scope.launch {\n            val watcher = createWatcher(this)\n            watcher\n                .onEventFlow\n                .filter { it.event == KfsEvent.Delete }\n                .onEach {\n                    val fullPath = File(it.targetDirectory, it.path).path\n                    onPathRemoved(fullPath)\n                }\n                .launchIn(this)\n\n            downloadMonitor.downloadListFlow\n                .map { it.map { it.folder }.distinct() }\n                .distinctUntilChanged()\n                .changes()\n                .onEach { changes ->\n                    val groups = changes\n                        .groupBy { it.second }\n                    groups[Change.Removed]\n                        ?.takeIf { it.isNotEmpty() }\n                        ?.map { it.first }\n                        ?.toTypedArray()\n                        ?.let {\n                            watcher.remove(*it)\n                        }\n                    groups[Change.Added]\n                        ?.takeIf { it.isNotEmpty() }\n                        ?.map { it.first }\n                        ?.ifThen(shouldFilterRemovableStorages) {\n                            filter {\n                                !FileUtils.isRemovableStorage(it)\n                            }\n                        }\n                        ?.toTypedArray()\n                        ?.let {\n                            watcher.add(*it)\n                        }\n                }\n                .launchIn(this)\n            itemsToCheck\n                .debounce(500)\n                .filter { it.isNotEmpty() }\n                .onEach { downloadItems ->\n                    checkAndRemoveThisItems(downloadItems)\n                    itemsToCheck.update { it.subtract(downloadItems) }\n                }.launchIn(this)\n        }\n    }\n\n    suspend fun stop() {\n        activeJob?.cancelAndJoin()\n        activeJob = null\n        itemsToCheck.update { emptySet() }\n        stopped = true\n    }\n\n    suspend fun removeDownloadsThatFilesAreMissing() {\n        checkAndRemoveThisItems(\n            downloadSystem.getListOfDownloadThatMissingFileOrHaveNotProgress()\n                .map { it.id }\n                .toSet()\n        )\n    }\n\n    private suspend fun checkAndRemoveThisItems(ids: Set<Long>) {\n        for (id in ids) {\n            val downloadItem = downloadSystem.getDownloadItemById(id) ?: continue\n            val file = downloadSystem.getDownloadFile(downloadItem)\n            if (!file.exists()) {\n                downloadSystem.removeDownload(\n                    id = downloadItem.id,\n                    alsoRemoveFile = false, // it is already deleted!\n                    context = RemovedBy(AutoRemoveOption)\n                )\n            }\n        }\n    }\n\n    /**\n     * find the corespounding download and schedule for  remove that\n     * I will add a delay for that (maybe it's a temporary file remove for example when renaming download item)\n     */\n    private fun onPathRemoved(path: String) {\n        if (stopped) return\n        val item = downloadSystem.getDownloadItemByPath(path) ?: return\n        itemsToCheck.update { it.plus(item.id) }\n    }\n}\n\nprivate sealed interface Change {\n    data object Added : Change\n    data object Removed : Change\n    data object NotChange : Change\n}\n\n\nprivate fun <T> Flow<List<T>>.changes(): Flow<List<Pair<T, Change>>> {\n    return withPrevious { previous, current ->\n        if (previous == null) {\n            current.map { it to Change.Added }\n        } else {\n            diffOf(previous, current)\n        }\n    }\n}\n\nprivate fun <T> diffOf(\n    a: Collection<T>, b: Collection<T>,\n): List<Pair<T, Change>> {\n    val output = ArrayList<Pair<T, Change>>(maxOf(a.size, b.size))\n    val aSet = a.toSet()\n    val remainingBItems = b.toMutableSet()\n    // find removed items in b\n    for (i in aSet) {\n        if (i in remainingBItems) {\n            output.add(i to Change.NotChange)\n            remainingBItems.remove(i)\n        } else {\n            output.add(i to Change.Removed)\n        }\n    }\n    //  remaining b's are added!\n    output.addAll(remainingBItems.map { it to Change.Added })\n    return output\n}\n\ndata object AutoRemoveOption : CanPerformRemove\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/Category.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.LocalIconFromUriResolver\nimport ir.amirab.util.wildcardMatch\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * @param path\n * this is a default download path for this category\n * @param icon\n * can be used by [IconSource]\n */\n@Immutable\n@Serializable\ndata class Category(\n    @SerialName(\"id\")\n    val id: Long,\n    @SerialName(\"name\")\n    val name: String,\n    @SerialName(\"icon\")\n    val icon: String,\n    @SerialName(\"path\")\n    // don't directly use this check for usePath first! see [getDownloadPath()]\n    val path: String,\n    @SerialName(\"usePath\")\n    val usePath: Boolean = true,\n    @SerialName(\"acceptedFileTypes\")\n    val acceptedFileTypes: List<String> = emptyList(),\n    // this is optional if nothing provided it means that every url is acceptable\n    @SerialName(\"acceptedUrlPatterns\")\n    val acceptedUrlPatterns: List<String> = emptyList(),\n    @SerialName(\"items\")\n    val items: List<Long> = emptyList(),\n) {\n    val hasFileTypes = acceptedFileTypes.isNotEmpty()\n    val hasUrlPattern = acceptedUrlPatterns.isNotEmpty()\n    private val filterCount = run {\n        var count = 0\n        if (hasFileTypes) count++\n        if (hasUrlPattern) count++\n        count\n    }\n    val hasFilters = filterCount > 0\n\n    fun acceptFileName(fileName: String): Boolean {\n        if (!hasFileTypes) {\n            return true\n        }\n        return acceptedFileTypes.any { ext ->\n            fileName.endsWith(\n                suffix = \".$ext\",\n                ignoreCase = true\n            )\n        }\n    }\n\n    fun withExtraItems(newItems: List<Long>): Category {\n        return copy(\n            items = items.plus(newItems).distinct()\n        )\n    }\n\n    fun getDownloadPath(): String? {\n        return if (usePath) path else null\n    }\n\n    fun acceptUrl(url: String): Boolean {\n        if (!hasUrlPattern) {\n            return true\n        }\n        return acceptedUrlPatterns.any {\n            wildcardMatch(\n                pattern = it,\n                input = url\n            )\n        }\n    }\n}\n\nfun Category.iconSource(\n    iconResolver: IIconResolver,\n): IconSource? {\n    return iconResolver.resolve(icon)\n}\n\n@Composable\nfun Category.rememberIconPainter(): IconSource? {\n    val iconResolver = LocalIconFromUriResolver.current\n    return remember(icon) {\n        iconResolver.resolve(icon)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategoryFileStorage.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport ir.amirab.downloader.db.TransactionalFileSaver\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.io.File\n\nclass CategoryFileStorage(\n    val file: File,\n    val fileSaver: TransactionalFileSaver,\n) : CategoryStorage {\n    val lock = Mutex()\n    override suspend fun setCategories(categories: List<Category>) {\n        lock.withLock {\n            fileSaver.writeObject(file, categories)\n        }\n    }\n\n    override suspend fun getCategories(): List<Category> {\n        return fileSaver.readObject(file) ?: emptyList()\n    }\n\n    override suspend fun isCategoriesSet(): Boolean {\n        return file.exists()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategoryItem.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport androidx.compose.runtime.Immutable\n\ninterface ICategoryItem {\n    val fileName: String\n    val url: String\n}\n\n@Immutable\ndata class CategoryItem(\n    override val fileName: String,\n    override val url: String,\n) : ICategoryItem\n\n@Immutable\ndata class CategoryItemWithId(\n    val id: Long,\n    override val fileName: String,\n    override val url: String,\n) : ICategoryItem\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategoryManager.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport ir.amirab.util.ifThen\nimport ir.amirab.util.shifted\nimport ir.amirab.util.suspendGuardedEntry\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.FlowPreview\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\nclass CategoryManager(\n    private val categoryStorage: CategoryStorage,\n    private val scope: CoroutineScope,\n    private val defaultCategoriesFactory: DefaultCategories,\n    private val categoryItemProvider: ICategoryItemProvider,\n) {\n    private val _categories = MutableStateFlow<List<Category>>(emptyList())\n    val categoriesFlow = _categories.asStateFlow()\n\n    private var booted = suspendGuardedEntry()\n\n    @OptIn(FlowPreview::class)\n    suspend fun boot() {\n        booted.action {\n            if (categoryStorage.isCategoriesSet()) {\n                _categories.value = categoryStorage\n                    .getCategories()\n            } else {\n                reset()\n            }\n            _categories\n                .sample(500)\n                .onEach { categoryStorage.setCategories(it) }\n                .launchIn(scope)\n        }\n    }\n\n    suspend fun reset() {\n        val newCategories = defaultCategoriesFactory.getDefaultCategories()\n        setCategories(newCategories)\n        withContext(Dispatchers.IO) {\n            newCategories.forEach {\n                prepareCategory(it)\n            }\n            autoAddItemsToCategoriesBasedOnFileNames(\n                categoryItemProvider\n                    .getAll()\n            )\n        }\n    }\n\n    fun getCategories(): List<Category> {\n        return _categories.value\n    }\n\n    fun setCategories(categories: List<Category>) {\n        _categories.update { categories }\n    }\n\n    fun getCategoryById(id: Long): Category? {\n        return getCategories()\n            .firstOrNull { it.id == id }\n    }\n\n    fun getCategoryOfType(extension: String): Category? {\n        return getCategories().firstOrNull { c ->\n            c.acceptedFileTypes.any {\n                it.equals(extension, true)\n            }\n        }\n    }\n\n    fun getCategoryOfFileName(fileName: String): Category? {\n        return getCategories()\n            .firstOrNull {\n                it.acceptFileName(fileName)\n            }\n    }\n\n    fun getCategoryOf(categoryItem: ICategoryItem): Category? {\n        val url = categoryItem.url\n        val fileName = categoryItem.fileName\n        return getCategories()\n            .filter { it.hasFilters }\n            .filter {\n                it.acceptFileName(fileName) && it.acceptUrl(url)\n            }\n            .sortedWith(\n                // first prioritize categories which has url pattern filters\n                compareByDescending<Category> { it.hasUrlPattern }\n                    // then prioritize categories with fewer url patterns count\n                    .thenBy { it.acceptedUrlPatterns.size }\n                    // second prioritize categories which has file types\n                    .thenByDescending { it.hasFileTypes }\n                    // then prioritize categories with fewer file type count\n                    .thenBy { it.acceptedFileTypes.size }\n            )\n            .firstOrNull()\n    }\n\n    fun getCategoryOfItem(id: Long): Category? {\n        return getCategories()\n            .firstOrNull {\n                it.items.contains(id)\n            }\n    }\n\n    fun deleteCategory(category: Category) {\n        deleteCategory(category.id)\n    }\n\n    fun deleteCategory(categoryId: Long) {\n        _categories.update {\n            it.filter {\n                it.id != categoryId\n            }\n        }\n    }\n\n    fun addCustomCategory(category: Category) {\n        require(category.id == -1L)\n        val categories = getCategories()\n        val newId = (\n                categories\n                    .maxOfOrNull { it.id }\n                    ?.coerceAtLeast(DEFAULT_CATEGORY_END_ID)\n                    ?: DEFAULT_CATEGORY_END_ID\n                ) + 1\n        val newCategory = category.copy(\n            id = newId\n        )\n        setCategories(\n            categories.plus(\n                newCategory\n            )\n        )\n        prepareCategory(newCategory)\n    }\n\n    private fun createDirectoryIfNecessary(category: Category) {\n        kotlin.runCatching {\n            val folder = category\n                .getDownloadPath()\n                ?.let(::File)\n                ?.canonicalFile\n                ?: return\n            if (!folder.exists()) {\n                folder.mkdirs()\n            }\n        }\n    }\n\n    private fun prepareCategory(newCategory: Category) {\n        createDirectoryIfNecessary(newCategory)\n    }\n\n    fun updateCategory(categoryToUpdate: Category) {\n        _categories.update {\n            it.updatedItem(\n                categoryId = categoryToUpdate.id,\n                update = { categoryToUpdate }\n            )\n        }\n    }\n\n    fun updateCategory(id: Long, categoryToUpdate: (Category) -> Category) {\n        _categories.update {\n            it.updatedItem(id, categoryToUpdate)\n        }\n    }\n\n\n    fun addItemsToCategory(categoryId: Long, itemIds: List<Long>) {\n        _categories.update { previousCategories ->\n            previousCategories\n                .removedItemIds(itemIds)\n                .updatedItem(categoryId) {\n                    it.withExtraItems(itemIds)\n                }\n        }\n    }\n\n    fun removeItemInCategories(idsToRemove: List<Long>) {\n        _categories.update {\n            it.removedItemIds(idsToRemove)\n        }\n    }\n\n    fun isDefaultCategory(category: Category): Boolean {\n        return category.id in 0..DEFAULT_CATEGORY_END_ID\n    }\n\n    fun autoAddItemsToCategoriesBasedOnFileNames(\n        unCategorizedItems: List<CategoryItemWithId>,\n    ) {\n        val newItemsMap = mutableMapOf<Long, MutableList<Long>>()\n        var count = 0\n        for (item in unCategorizedItems) {\n            val categoryToUpdate = getCategoryOf(item) ?: continue\n            newItemsMap\n                .getOrPut(categoryToUpdate.id) { mutableListOf() }\n                .add(item.id)\n            count++\n        }\n        for ((categoryId, itemsToAdd) in newItemsMap) {\n            updateCategory(categoryId) {\n                it.withExtraItems(itemsToAdd)\n            }\n        }\n    }\n\n    fun isThisPathBelongsToACategory(folder: String): Boolean {\n        return getCategories()\n            .mapNotNull { it.getDownloadPath() }.contains(folder)\n    }\n\n    @Suppress(\"NAME_SHADOWING\")\n    fun updateCategoryFoldersBasedOnDefaultDownloadFolder(\n        previousDownloadFolder: String,\n        currentDownloadFolder: String,\n    ) {\n        val previousDownloadFolder = File(previousDownloadFolder).absoluteFile\n        val currentDownloadFolder = File(currentDownloadFolder).absoluteFile\n        for (category in getCategories()) {\n            val categoryPath = File(category.path).absoluteFile\n            if (categoryPath.startsWith(previousDownloadFolder)) {\n                val relativePath = categoryPath.relativeTo(previousDownloadFolder)\n                updateCategory(category.id) {\n                    it.copy(\n                        path = currentDownloadFolder.resolve(relativePath).absolutePath\n                    )\n                }\n            }\n        }\n    }\n\n    fun reorderCategory(\n        index: Int,\n        delta: Int,\n    ) {\n        _categories.update { categories ->\n            categories.ifThen(index in categories.indices && (index + delta) in categories.indices) {\n                shifted(index, delta)\n            }\n        }\n    }\n\n    companion object {\n        /**\n         * Reserved ids for default categories\n         * this is too big BTW as we only use 5 for now\n         * maybe we need more or extra hidden categories that users can enable (maybe ?)\n         */\n        const val DEFAULT_CATEGORY_END_ID = 100L\n    }\n}\n\nprivate fun List<Category>.removedItemIds(itemIds: List<Long>): List<Category> {\n    return map {\n        it.copy(\n            items = it.items.filter { itemId ->\n                itemId !in itemIds\n            }\n        )\n    }\n}\n\nprivate inline fun List<Category>.updatedItem(categoryId: Long, update: (Category) -> Category): List<Category> {\n    return map {\n        if (it.id == categoryId) {\n            update(it)\n        } else it\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategoryManagerExtensions.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\n\n@Composable\nfun CategoryManager.rememberCategoryOf(\n    itemId: Long,\n): Category? {\n    val categories by categoriesFlow.collectAsState()\n    return remember(itemId, categories) {\n        categories.firstOrNull {\n            it.items.contains(itemId)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategorySelectionMode.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\nsealed interface CategorySelectionMode {\n    data class Fixed(val categoryId: Long) : CategorySelectionMode\n    data object Auto : CategorySelectionMode\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/CategoryStorage.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\ninterface CategoryStorage {\n    suspend fun setCategories(categories: List<Category>)\n    suspend fun getCategories(): List<Category>\n    suspend fun isCategoriesSet(): Boolean\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/DefaultCategories.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport com.abdownloadmanager.shared.util.ui.IMyIcons\nimport ir.amirab.util.compose.IconSource\nimport java.io.File\n\nclass DefaultCategories(\n    private val icons: IMyIcons,\n    private val getDefaultDownloadFolder: () -> String,\n) {\n\n    fun getCategoryOfFileName(name: String): Category? {\n        return getDefaultCategories()\n            .firstOrNull { it.acceptFileName(name) }\n    }\n\n    fun getDefaultCategories(): List<Category> {\n        fun IconSource.toUri(): String {\n            return requireNotNull(uri) {\n                \"It seems that we use an icon that does not have uri\"\n            }\n        }\n\n        fun relative(path: String): String {\n            return File(getDefaultDownloadFolder(), path).path\n        }\n\n        val compressed = Category(\n            id = 0,\n            name = \"Compressed\",\n            path = relative(\"Compressed\"),\n            icon = icons.zipFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"zip\",\n                \"rar\",\n                \"7z\",\n                \"tar\",\n                \"gz\",\n                \"bz2\",\n                \"xz\",\n                \"iso\",\n                \"dmg\",\n                \"tgz\",\n            ),\n        )\n\n        val programs = Category(\n            id = 1,\n            name = \"Programs\",\n            path = relative(\"Programs\"),\n            icon = icons.applicationFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"apk\",\n                \"exe\",\n                \"msi\",\n                \"bat\",\n                \"sh\",\n                \"jar\",\n                \"app\",\n                \"deb\",\n                \"rpm\",\n                \"bin\",\n            ),\n        )\n        val videos = Category(\n            id = 2,\n            name = \"Videos\",\n            path = relative(\"Videos\"),\n            icon = icons.videoFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"mp4\",\n                \"avi\",\n                \"mkv\",\n                \"mov\",\n                \"wmv\",\n                \"flv\",\n                \"webm\",\n                \"m4v\",\n                \"3gp\",\n                \"mpeg\",\n                \"ts\",\n            ),\n        )\n\n        val music = Category(\n            id = 3,\n            name = \"Music\",\n            path = relative(\"Music\"),\n            icon = icons.musicFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"mp3\",\n                \"wav\",\n                \"aac\",\n                \"flac\",\n                \"ogg\",\n                \"aiff\",\n                \"wma\",\n                \"m4a\",\n            ),\n        )\n\n        val pictures = Category(\n            id = 4,\n            name = \"Pictures\",\n            path = relative(\"Pictures\"),\n            icon = icons.pictureFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"jpg\",\n                \"jpeg\",\n                \"png\",\n                \"gif\",\n                \"bmp\",\n                \"tiff\",\n                \"tif\",\n                \"svg\",\n                \"webp\",\n                \"heic\",\n                \"ico\",\n                \"raw\",\n                \"psd\",\n            ),\n        )\n        val documents = Category(\n            id = 5,\n            name = \"Documents\",\n            path = relative(\"Documents\"),\n            icon = icons.documentFile.toUri(),\n            acceptedFileTypes = listOf(\n                \"doc\",\n                \"docx\",\n                \"pdf\",\n                \"txt\",\n                \"rtf\",\n                \"odt\",\n                \"xls\",\n                \"xlsx\",\n                \"ppt\",\n                \"pptx\",\n                \"csv\",\n                \"epub\",\n                \"pages\",\n            ),\n        )\n        return listOf(\n            compressed,\n            programs,\n            videos,\n            music,\n            pictures,\n            documents,\n        )\n    }\n\n    fun isDefault(categories: List<Category>): Boolean {\n        return getDefaultCategories() == categories.map {\n            it.copy(items = emptyList())\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/DownloadManagerCategoryItemProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nimport ir.amirab.downloader.DownloadManager\n\nclass DownloadManagerCategoryItemProvider(\n    private val dowManager: DownloadManager,\n) : ICategoryItemProvider {\n    override suspend fun getAll(): List<CategoryItemWithId> {\n        return dowManager.getDownloadList().map {\n            CategoryItemWithId(\n                id = it.id,\n                fileName = it.name,\n                url = it.link\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/ICategoryItemProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\ninterface ICategoryItemProvider {\n    suspend fun getAll(): List<CategoryItemWithId>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/category/InMemoryCategoryStorage.kt",
    "content": "package com.abdownloadmanager.shared.util.category\n\nclass InMemoryCategoryStorage : CategoryStorage {\n    private var categories = emptyList<Category>()\n\n    override suspend fun setCategories(categories: List<Category>) {\n        this.categories = categories\n    }\n\n    override suspend fun getCategories(): List<Category> {\n        return categories\n    }\n\n    override suspend fun isCategoriesSet(): Boolean {\n        return true\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/PlatformDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport com.abdownloadmanager.shared.util.SystemDownloadLocationProvider\n\nobject PlatformDownloadLocationProvider {\n    val instance: SystemDownloadLocationProvider by lazy {\n        getPlatformDownloadLocationProvider()\n    }\n}\n\nexpect fun getPlatformDownloadLocationProvider(): SystemDownloadLocationProvider\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/extractors/Extractor.kt",
    "content": "package com.abdownloadmanager.shared.util.extractors\n\ninterface Extractor<in Input,out Output>{\n    fun extract(input:Input):Output\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/extractors/linkextractor/DownloadCredentialExtractor.kt",
    "content": "package com.abdownloadmanager.shared.util.extractors.linkextractor\n\nimport com.abdownloadmanager.shared.downloaderinui.DownloaderInUiRegistry\nimport com.abdownloadmanager.shared.util.extractors.Extractor\nimport ir.amirab.downloader.downloaditem.IDownloadCredentials\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\n\ninterface DownloadCredentialExtractor<T> : Extractor<T, List<IDownloadCredentials>> {\n    override fun extract(input: T): List<IDownloadCredentials>\n}\n\nobject DownloadCredentialFromStringExtractor :\n    DownloadCredentialExtractor<String>, KoinComponent {\n    val downloaderInUiRegistry: DownloaderInUiRegistry by inject()\n    override fun extract(input: String): List<IDownloadCredentials> {\n        return StringUrlExtractor.extract(input)\n            .mapNotNull {\n                downloaderInUiRegistry\n                    .bestMatchForThisLink(it)\n                    ?.createMinimumCredentials(it)\n            }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/extractors/linkextractor/DownloadCredentialsFromCurl.kt",
    "content": "package com.abdownloadmanager.shared.util.extractors.linkextractor\n\nimport ir.amirab.downloader.downloaditem.http.HttpDownloadCredentials\n\nobject DownloadCredentialsFromCurl : DownloadCredentialExtractor<String> {\n    override fun extract(input: String): List<HttpDownloadCredentials> {\n        val curlCommands = input.split(\"\\n\").filter { it.trim().startsWith(\"curl\") }\n        return curlCommands.map { command ->\n            val urlRegex = \"\"\"curl\\s+\"([^\"]+)\"\"\"\".toRegex()\n            val headerRegex = \"\"\"-H\\s+\"([^\"]+)\"\"\"\".toRegex()\n            val urlMatch = urlRegex.find(command)\n            val headerMatches = headerRegex.findAll(command)\n            val url = urlMatch?.groupValues?.get(1) ?: \"\"\n            val headers = headerMatches.mapNotNull { match ->\n                val header = match.groupValues[1]\n                val (key, value) = header.split(\":\", limit = 2)\n                key.trim() to value.trim()\n            }.toMap()\n            val usernamePasswordRegex = \"\"\"(?:-u|--user)\\s+([^:]+):(.*)\"\"\".toRegex()\n            val usernamePasswordMatch = usernamePasswordRegex.find(command)\n            val username = usernamePasswordMatch?.groupValues?.get(1)?.trim()\n            val password = usernamePasswordMatch?.groupValues?.get(2)?.trim()\n            HttpDownloadCredentials(\n                link = url,\n                headers = headers,\n                username = username,\n                password = password\n            )\n        }\n    }\n\n    fun generateCurlCommands(credentialsList: List<HttpDownloadCredentials>): List<String> {\n        return credentialsList.map { credentials ->\n            val curlCommand = StringBuilder(\"curl \\\"${credentials.link}\\\"\")\n            credentials.headers?.forEach { (headerName, headerValue) ->\n                curlCommand.append(\" -H \\\"${headerName}: ${headerValue}\\\"\")\n            }\n            if (credentials.username != null) {\n                if (credentials.password != null) {\n                    curlCommand.append(\" -u \\\"${credentials.username}:${credentials.password}\\\"\")\n                } else {\n                    curlCommand.append(\" -u \\\"${credentials.username}\\\"\")\n                }\n            }\n            curlCommand.toString()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/extractors/linkextractor/URLExtractors.kt",
    "content": "package com.abdownloadmanager.shared.util.extractors.linkextractor\n\nimport com.abdownloadmanager.shared.util.extractors.Extractor\nimport ir.amirab.util.HttpUrlUtils\n\nobject StringUrlExtractor : Extractor<String, List<String>> {\n    private val urlRegex by lazy { Regex(\"\"\"\\b(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]\"\"\") }\n    override fun extract(input: String): List<String> {\n        // maybe each line is a link\n        val linksInEachLines = byEachLine(input)\n        if (linksInEachLines.isNotEmpty()) {\n            return linksInEachLines\n        }\n        // try to find links by regex\n        return byRegex(input)\n    }\n\n    private fun byEachLine(input: String): List<String> {\n        return input\n            .lineSequence()\n            .map { it.trim() }\n            .filter { HttpUrlUtils.isValidUrl(it) }\n            .toList()\n    }\n\n    private fun byRegex(input: String): List<String> {\n        return urlRegex.findAll(input)\n            .map { it.value }\n            .filter { HttpUrlUtils.isValidUrl(it) }\n            .toList()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/mvi/ContainsEffects.kt",
    "content": "package com.abdownloadmanager.shared.util.mvi\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\ninterface ContainsEffects<Effect> {\n    val effects: SharedFlow<Effect>\n    fun sendEffect(effect:Effect)\n}\n\nprivate class SupportsEffects<Effect>: ContainsEffects<Effect> {\n    private val _effects = MutableSharedFlow<Effect>(\n        extraBufferCapacity = 64,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n    override val effects: SharedFlow<Effect> = _effects\n    override fun sendEffect(effect:Effect){\n        _effects.tryEmit(effect)\n    }\n}\n\nfun <Effect> supportEffects(): ContainsEffects<Effect> = SupportsEffects()\n\n@Composable\nfun <T> HandleEffects(\n    effectContainer: ContainsEffects<T>,\n    handle:(T)->Unit\n){\n    LaunchedEffect(effectContainer){\n        effectContainer.effects.onEach {\n            handle(it)\n        }.launchIn(this)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/mvi/ContainsScreenState.kt",
    "content": "package com.abdownloadmanager.shared.util.mvi\n\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.*\n\ninterface ContainsScreenState<ScreenState> {\n    val state: StateFlow<ScreenState>\n    fun setState(state: ScreenState)\n    fun setState(updater: (ScreenState) -> ScreenState)\n}\n\nclass SupportsScreenState<ScreenState>(\n    initialState: ScreenState\n) : ContainsScreenState<ScreenState> {\n    private val _state = MutableStateFlow<ScreenState>(initialState)\n    override val state = _state.asStateFlow()\n\n    override fun setState(updater: (ScreenState) -> ScreenState) {\n        _state.update(updater)\n    }\n\n    override fun setState(state: ScreenState) {\n        setState { state }\n    }\n}\n\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/mvi/EventHandler.kt",
    "content": "package com.abdownloadmanager.shared.util.mvi\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.BufferOverflow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\n\ninterface SupportsEvents<Event> : IEventHandlerAndReceiver<Event> {\n    fun getEventHandler(): IEventHandlerAndReceiver<Event>\n    override fun sendEvent(event: Event) {\n        getEventHandler().sendEvent(event)\n    }\n}\n\nfun <Event> eventHandler(\n    scope: CoroutineScope,\n    handler: suspend (handle: Event) -> Unit,\n) = object : EventHandler<Event>(scope) {\n    override suspend fun handleEvent(event: Event) {\n        handler(event)\n    }\n}\n\n\ninterface IEventReceiver<Event> {\n    fun sendEvent(event: Event)\n}\n\ninterface IEventHandler<Event>{\n    suspend fun handleEvent(event: Event)\n}\n\ninterface IEventHandlerAndReceiver<Event> : IEventReceiver<Event>, IEventHandler<Event>\n\nabstract class EventHandler<Event>(\n    private val scope: CoroutineScope,\n) : IEventHandlerAndReceiver<Event> {\n    private val _eventsFlow = MutableSharedFlow<Event>(\n        extraBufferCapacity = 64,\n        onBufferOverflow = BufferOverflow.DROP_OLDEST\n    )\n\n    init {\n        _eventsFlow.onEach {\n            handleEvent(it)\n        }.launchIn(scope)\n    }\n\n    override fun sendEvent(event: Event) {\n        _eventsFlow.tryEmit(event)\n    }\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ondownloadcompletion/OnDownloadCompletionAction.kt",
    "content": "package com.abdownloadmanager.shared.util.ondownloadcompletion\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\ninterface OnDownloadCompletionAction {\n    suspend fun onDownloadCompleted(downloadItem: IDownloadItem)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ondownloadcompletion/OnDownloadCompletionActionProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.ondownloadcompletion\n\nimport ir.amirab.downloader.downloaditem.IDownloadItem\n\ninterface OnDownloadCompletionActionProvider {\n    suspend fun getOnDownloadCompletionAction(downloadItem: IDownloadItem): List<OnDownloadCompletionAction>\n}\n\nclass NoOpOnDownloadCompletionActionProvider : OnDownloadCompletionActionProvider {\n    override suspend fun getOnDownloadCompletionAction(downloadItem: IDownloadItem): List<OnDownloadCompletionAction> {\n        return emptyList()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ondownloadcompletion/OnDownloadCompletionActionRunner.kt",
    "content": "package com.abdownloadmanager.shared.util.ondownloadcompletion\n\nimport ir.amirab.downloader.DownloadManagerEvents\nimport ir.amirab.downloader.DownloadManagerMinimalControl\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.filterIsInstance\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass OnDownloadCompletionActionRunner(\n    private val downloadManagerMinimalControl: DownloadManagerMinimalControl,\n    private val scope: CoroutineScope,\n    private val onDownloadCompletionActionProvider: OnDownloadCompletionActionProvider,\n) {\n    private var job: Job? = null\n\n    /**\n     * Starts listening to download completion events and executes the corresponding actions.\n     */\n    @Synchronized\n    fun startListening() {\n        job?.cancel()\n        job = downloadManagerMinimalControl.listOfJobsEvents\n            .filterIsInstance<DownloadManagerEvents.OnJobCompleted>()\n            .onEach {\n                val downloadItem = it.downloadItem\n                onDownloadCompletionActionProvider\n                    .getOnDownloadCompletionAction(it.downloadItem)\n                    .forEach { completionAction ->\n                        runCatching {\n                            completionAction.onDownloadCompleted(downloadItem)\n                        }.onFailure { e ->\n                            e.printStackTrace()\n                        }\n                    }\n            }\n            .launchIn(scope)\n    }\n\n    fun stopListening() {\n        job?.cancel()\n        job = null\n    }\n}\n\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/onqueuecompletion/OnQueueCompletionActionProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.onqueuecompletion\n\ninterface OnQueueCompletionActionProvider {\n    suspend fun getOnQueueEventActions(queueId: Long): List<OnQueueEventAction>\n}\n\nclass NoopOnQueueCompletionActionProvider : OnQueueCompletionActionProvider {\n    override suspend fun getOnQueueEventActions(queueId: Long): List<OnQueueEventAction> {\n        return emptyList()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/onqueuecompletion/OnQueueEventAction.kt",
    "content": "package com.abdownloadmanager.shared.util.onqueuecompletion\n\ninterface OnQueueEventAction {\n    suspend fun onQueueCompleted(queueId: Long)\n    suspend fun onQueueEndTimeReached(queueId: Long)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/onqueuecompletion/OnQueueEventActionRunner.kt",
    "content": "package com.abdownloadmanager.shared.util.onqueuecompletion\n\nimport ir.amirab.downloader.queue.QueueEvent\nimport ir.amirab.downloader.queue.QueueManager\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.onEach\n\nclass OnQueueEventActionRunner(\n    private val queueManager: QueueManager,\n    private val scope: CoroutineScope,\n    private val onQueueCompletionActionProvider: OnQueueCompletionActionProvider,\n) {\n    private var job: Job? = null\n\n    /**\n     * Starts listening to queue events and executes the corresponding actions.\n     */\n    @Synchronized\n    fun startListening() {\n        job?.cancel()\n        job = queueManager.queueEvents\n            .onEach {\n                when (it) {\n                    is QueueEvent.OnQueueBecomesEmpty -> {\n                        val actions = onQueueCompletionActionProvider.getOnQueueEventActions(it.queueId)\n                        actions.forEach { action ->\n                            action.onQueueCompleted(it.queueId)\n                        }\n                    }\n\n                    is QueueEvent.QueueEndTimeReached -> {\n                        if (!it.wasActive) {\n                            return@onEach\n                        }\n                        val actions = onQueueCompletionActionProvider.getOnQueueEventActions(it.queueId)\n                        actions.forEach { action ->\n                            action.onQueueEndTimeReached(it.queueId)\n                        }\n                    }\n\n                    is QueueEvent.OnQueueStartTimeReached -> {\n                        // nothing\n                    }\n                }\n            }\n            .launchIn(scope)\n    }\n\n    fun stopListening() {\n        job?.cancel()\n        job = null\n    }\n}\n\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/perhostsettings/PerHostSettingsItem.kt",
    "content": "package com.abdownloadmanager.shared.util.perhostsettings\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PerHostSettingsItem(\n    val host: String,\n    val username: String? = null,\n    val password: String? = null,\n    val userAgent: String? = null,\n    val threadCount: Int? = null,\n    val speedLimit: Long? = null,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/perhostsettings/PerHostSettingsManager.kt",
    "content": "package com.abdownloadmanager.shared.util.perhostsettings\n\nimport ir.amirab.util.wildcardMatch\nimport kotlinx.coroutines.flow.update\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nclass PerHostSettingsManager(\n    private val storage: IPerHostSettingsStorage\n) {\n\n    fun getStorageData(): List<PerHostSettingsItem> {\n        return storage.perHostSettingsFlow.value\n    }\n\n    fun getSettingsForHost(host: String): PerHostSettingsItem? {\n        return getStorageData()\n            // hosts that doesn't have wildcards should be checked first\n            .sortedBy {\n                it.host.count { char -> char == '*' }\n            }\n            .firstOrNull {\n                wildcardMatch(it.host, host)\n            }\n    }\n\n    fun setSettingsData(hostSettingsList: List<PerHostSettingsItem>) {\n        storage.perHostSettingsFlow.update {\n            hostSettingsList\n                .filter { it.host.isNotBlank() }\n                .distinctBy { it.host }\n        }\n    }\n}\n\nfun PerHostSettingsManager.getSettingsForURL(url: String): PerHostSettingsItem? {\n    return url\n        .toHttpUrlOrNull()\n        ?.host\n        ?.let(::getSettingsForHost)\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/perhostsettings/PerHostSettingsStorage.kt",
    "content": "package com.abdownloadmanager.shared.util.perhostsettings\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface IPerHostSettingsStorage {\n    val perHostSettingsFlow: MutableStateFlow<List<PerHostSettingsItem>>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/proxy/IProxyStorage.kt",
    "content": "package com.abdownloadmanager.shared.util.proxy\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface IProxyStorage {\n    val proxyDataFlow: MutableStateFlow<ProxyData>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/proxy/Proxy.kt",
    "content": "package com.abdownloadmanager.shared.util.proxy\n\nimport ir.amirab.downloader.connection.proxy.Proxy\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isAndroid\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ProxyRules(\n    val excludeURLPatterns: List<String>,\n)\n\n@Serializable\ndata class ProxyWithRules(\n    val proxy: Proxy,\n    val rules: ProxyRules,\n)\n\n@Serializable\ndata class PACProxy(\n    val uri: String,// an uri to get script path of the PAC\n) {\n    companion object {\n        fun default() = PACProxy(\"http://localhost/some.pac\")\n    }\n}\n\nenum class ProxyMode {\n    @SerialName(\"direct\")\n    Direct,\n\n    @SerialName(\"system\")\n    UseSystem,\n\n    @SerialName(\"manual\")\n    Manual,\n\n    @SerialName(\"pac\")\n    Pac;\n\n    companion object {\n        fun usableValues(): List<ProxyMode> {\n            return if (Platform.isAndroid()) {\n                listOf(\n                    Direct,\n                    Manual,\n                )\n            } else {\n                listOf(\n                    Direct,\n                    UseSystem,\n                    Pac,\n                    Manual,\n                )\n            }\n        }\n    }\n}\n\n// for persisting in storage\n@Serializable\ndata class ProxyData(\n    val proxyMode: ProxyMode,\n    //manual proxy config\n    val proxyWithRules: ProxyWithRules,\n    //configuration script config\n    val pac: PACProxy,\n) {\n    companion object {\n        fun default() = ProxyData(\n            proxyMode = ProxyMode.Direct,\n            proxyWithRules = ProxyWithRules(\n                proxy = Proxy.default(),\n                rules = ProxyRules(emptyList())\n            ),\n            pac = PACProxy.default()\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/proxy/ProxyManager.kt",
    "content": "package com.abdownloadmanager.shared.util.proxy\n\nimport ir.amirab.downloader.connection.proxy.Proxy\nimport ir.amirab.downloader.connection.proxy.ProxyStrategy\nimport ir.amirab.downloader.connection.proxy.ProxyStrategyProvider\nimport ir.amirab.downloader.connection.proxy.ProxyType\nimport ir.amirab.util.HttpUrlUtils\nimport ir.amirab.util.wildcardMatch\nimport java.net.Authenticator\nimport java.net.PasswordAuthentication\n\nclass ProxyManager(\n    val storage: IProxyStorage,\n) : ProxyStrategyProvider {\n    val proxyData = storage.proxyDataFlow\n\n    init {\n        val mySocksProxyAuthenticator = MySocksProxyAuthenticator { proxyData.value.proxyWithRules.proxy }\n        Authenticator.setDefault(mySocksProxyAuthenticator)\n    }\n\n    /**\n     * I don't like this it's better to improve this later\n     */\n    private fun getProxyModeForThisURL(url: String): ProxyStrategy {\n        val usingProxy = proxyData.value\n        return when (usingProxy.proxyMode) {\n            ProxyMode.Direct -> ProxyStrategy.Direct\n            ProxyMode.UseSystem -> ProxyStrategy.UseSystem\n            ProxyMode.Manual -> {\n                val proxyWithRules = usingProxy.proxyWithRules\n                if (shouldUseProxyFor(url, proxyWithRules.rules)) {\n                    ProxyStrategy.ManualProxy(proxyWithRules.proxy)\n                } else {\n                    ProxyStrategy.Direct\n                }\n            }\n            ProxyMode.Pac -> {\n                val pacURI = usingProxy.pac.uri\n                if (HttpUrlUtils.isValidUrl(pacURI)) {\n                    ProxyStrategy.ByScript(pacURI)\n                } else {\n                    ProxyStrategy.Direct\n                }\n            }\n        }\n    }\n\n    private fun shouldUseProxyFor(\n        url: String,\n        rules: ProxyRules,\n    ): Boolean {\n        val isInExcludeList = rules.excludeURLPatterns.any {\n            wildcardMatch(it, url)\n        }\n        return !isInExcludeList\n    }\n\n    override fun getProxyStrategyFor(url: String): ProxyStrategy {\n        return getProxyModeForThisURL(url)\n    }\n}\n\n/**\n * this is used for socks proxy authentication\n */\nprivate class MySocksProxyAuthenticator(\n    val currentProxy: () -> Proxy,\n) : Authenticator() {\n    override fun getPasswordAuthentication(): PasswordAuthentication? {\n        val proxy = currentProxy()\n        if (proxy.type == ProxyType.SOCKS && requestingPrompt == \"SOCKS authentication\") {\n            if (proxy.host == requestingHost && proxy.port == requestingPort) {\n                if (proxy.username != null) {\n                    return PasswordAuthentication(\n                        proxy.username,\n                        proxy.password.orEmpty().toCharArray(),\n                    )\n                }\n            }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/BaseMyColors.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport ir.amirab.util.compose.IIconResolver\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.contants.ICON_PROTOCOL\n\nabstract class BaseMyColors : IMyIcons, IIconResolver {\n    val iconMap = mutableMapOf<String, IconSource>()\n    fun ImageVector.asIconSource(\n        name: String,\n        requiredTint: Boolean = true,\n    ): IconSource {\n        val uri = \"$ICON_PROTOCOL:$name\"\n        return IconSource.VectorIconSource(this, requiredTint, uri)\n            .asIconSource()\n    }\n\n//    fun String.asIconSource(\n//        path: String,\n//        requiredTint: Boolean = true\n//    ): IconSource = apply {\n//        val uri = \"$RESOURCE_PROTOCOL:$path?tint=${requiredTint}\"\n//        return IconSource.ResourceIconSource(this, requiredTint, uri).asSource()\n//    }\n\n    fun IconSource.asIconSource(): IconSource = apply {\n        uri?.let {\n            iconMap[it] = this\n        }\n    }\n\n    override fun resolve(uri: String): IconSource? {\n        return iconMap[uri]\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/IMyIcons.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport ir.amirab.util.compose.IconSource\n\ninterface IMyIcons {\n    val appIcon: IconSource\n\n    val settings: IconSource\n    val flag: IconSource\n    val fast: IconSource\n    val search: IconSource\n    val info: IconSource\n    val check: IconSource\n    val link: IconSource\n    val download: IconSource\n    val permission: IconSource\n    val windowMinimize: IconSource\n    val windowFloating: IconSource\n    val windowMaximize: IconSource\n    val windowClose: IconSource\n    val exit: IconSource\n    val edit: IconSource\n    val undo: IconSource\n//    val menu: IconSource\n//    val menuClose: IconSource\n\n    val openSource: IconSource\n    val telegram: IconSource\n    val speaker: IconSource\n    val group: IconSource\n\n    //browser icons\n    val browserMozillaFirefox: IconSource\n    val browserGoogleChrome: IconSource\n    val browserMicrosoftEdge: IconSource\n    val browserOpera: IconSource\n\n    val next: IconSource\n    val back: IconSource\n    val up: IconSource\n    val down: IconSource\n    val activeCount: IconSource\n    val speed: IconSource\n    val resume: IconSource\n    val pause: IconSource\n    val stop: IconSource\n    val queue: IconSource\n    val queueStart: IconSource\n    val queueStop: IconSource\n    val remove: IconSource\n    val clear: IconSource\n    val add: IconSource\n    val minus: IconSource\n    val paste: IconSource\n    val copy: IconSource\n    val refresh: IconSource\n    val editFolder: IconSource\n    val share: IconSource\n    val file: IconSource\n    val folder: IconSource\n    val fileOpen: IconSource\n    val folderOpen: IconSource\n    val pictureFile: IconSource\n    val musicFile: IconSource\n    val zipFile: IconSource\n    val videoFile: IconSource\n    val applicationFile: IconSource\n    val documentFile: IconSource\n    val otherFile: IconSource\n    val lock: IconSource\n    val question: IconSource\n    val grip: IconSource\n    val sortUp: IconSource\n    val sortDown: IconSource\n    val verticalDirection: IconSource\n    val appearance: IconSource\n    val downloadEngine: IconSource\n    val browserIntegration: IconSource\n    val network: IconSource\n    val language: IconSource\n    val externalLink: IconSource\n\n    val earth: IconSource\n    val hearth: IconSource\n    val dragAndDrop: IconSource\n\n    val selectAll: IconSource\n    val selectInside: IconSource\n    val selectInvert: IconSource\n\n    val menu: IconSource\n\n    val close: IconSource\n\n    val data: IconSource\n    val alphabet: IconSource\n    val clock: IconSource\n}\n\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/LocalIsDebugMode.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nprivate val LocalIsDebugMode = staticCompositionLocalOf { false }\n\n@Composable\nfun ProvideDebugInfo(\n    debug:Boolean,\n    content:@Composable ()->Unit\n){\n    CompositionLocalProvider(\n        LocalIsDebugMode provides debug,\n        content\n    )\n}\n\n@Composable\nfun useIsInDebugMode(): Boolean {\n    return LocalIsDebugMode.current\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/LocalProviders.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.structuralEqualityPolicy\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\n\nval LocalContentColor = compositionLocalOf { Color.Black }\nval LocalContentAlpha = compositionLocalOf { 1f }\n\nval LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle() }\n\n@Composable\nfun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {\n    val mergedStyle = LocalTextStyle.current.merge(value)\n    CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)\n}\n\n@Composable\nfun WithContentAlpha(\n    newAlpha: Float,\n    content: @Composable () -> Unit\n) {\n    CompositionLocalProvider(\n        LocalContentAlpha provides newAlpha,\n        content = content\n    )\n}\n\n@Composable\nfun WithContentColor(\n    newColor: Color,\n    content: @Composable () -> Unit\n) {\n    CompositionLocalProvider(\n        LocalContentColor provides newColor,\n        content = content\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/LocalTitleBarDirection.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.LayoutDirection\n\nval LocalTitleBarDirection = staticCompositionLocalOf<LayoutDirection> {\n    error(\"TitleBarDirection not provided\")\n}\n\n@Composable\nfun WithTitleBarDirection(\n    content: @Composable () -> Unit,\n) {\n    CompositionLocalProvider(\n        LocalLayoutDirection provides LocalTitleBarDirection.current\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/MyColors.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.animation.animateColor\nimport com.abdownloadmanager.shared.util.darker\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.FiniteAnimationSpec\nimport androidx.compose.animation.core.Transition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.core.updateTransition\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\n\nval LocalMyColors = compositionLocalOf<MyColors> { error(\"LocalMyColors not provided\") }\n\nval myColors\n    @Composable\n    get() = LocalMyColors.current\n\n@Immutable\ndata class MyColors(\n    val id: String,\n    val name: String,\n\n\n    val primary: Color,\n    val primaryVariant: Color = primary,\n    val onPrimary: Color,\n    val secondary: Color,\n    val secondaryVariant: Color = secondary,\n    val onSecondary: Color,\n    val background: Color,\n    val onBackground: Color,\n    val onSurface: Color,\n    val surface: Color,\n    val error: Color,\n    val onError: Color,\n    val success: Color,\n    val onSuccess: Color,\n    val warning: Color,\n    val onWarning: Color,\n    val info: Color,\n    val onInfo: Color,\n    val isLight: Boolean,\n) {\n\n    val warningGradient: Brush by lazy {\n        Brush.linearGradient(\n            listOf(warning, warning.darker())\n        )\n    }\n    val errorGradient: Brush by lazy {\n        Brush.linearGradient(\n            listOf(error, error.darker())\n        )\n    }\n    val successGradient: Brush by lazy {\n        Brush.linearGradient(\n            listOf(success, success.darker())\n        )\n    }\n    val infoGradient: Brush by lazy {\n        Brush.linearGradient(\n            listOf(info, info.darker())\n        )\n    }\n\n    val menuGradientBackground = surface\n    val menuBorderColor = onSurface / 0.1f\n    val onMenuColor = onSurface\n\n    val primaryGradientColors = listOf(primary, secondary)\n    val primaryGradient by lazy {\n        Brush.linearGradient(primaryGradientColors)\n    }\n    val onPrimaryGradient = Color.White\n\n    fun selectionGradient(\n        startAlpha: Float = 1f, endAlpha: Float = 0f,\n        color: Color = surface,\n    ): Brush {\n        return Brush.linearGradient(listOf(color / startAlpha, color / endAlpha))\n    }\n\n    val focusedBorderColor = primary\n\n\n    fun getContentColorFor(color: Color): Color {\n        return when (color) {\n            primary, primaryVariant -> onPrimary\n            secondary, secondaryVariant -> onSecondary\n            error -> onError\n            success -> onSuccess\n            background -> onBackground\n            surface -> onSurface\n            else -> Color.Unspecified\n        }\n    }\n\n    val contrast = if (isLight) Color.White else Color.Black\n    val onContrast = if (isLight) Color.Black else Color.White\n}\n\nprivate object AnimateMyColors {\n    @Composable\n    fun animatedColors(\n        toBeAnimated: MyColors,\n        spec: FiniteAnimationSpec<Color> = tween(500),\n    ): MyColors {\n        val primary by animated(toBeAnimated.primary, spec)\n        val primaryVariant by animated(toBeAnimated.primaryVariant, spec)\n        val onPrimary by animated(toBeAnimated.onPrimary, spec)\n\n        val secondary by animated(toBeAnimated.secondary, spec)\n        val secondaryVariant by animated(toBeAnimated.secondaryVariant, spec)\n        val onSecondary by animated(toBeAnimated.onSecondary, spec)\n\n        val background by animated(toBeAnimated.background, spec)\n        val onBackground by animated(toBeAnimated.onBackground, spec)\n\n        val surface by animated(toBeAnimated.surface, spec)\n        val onSurface by animated(toBeAnimated.onSurface, spec)\n\n        val success by animated(toBeAnimated.success, spec)\n        val onSuccess by animated(toBeAnimated.onSuccess, spec)\n\n        val error by animated(toBeAnimated.error, spec)\n        val onError by animated(toBeAnimated.onError, spec)\n\n        val warning by animated(toBeAnimated.warning, spec)\n        val onWarning by animated(toBeAnimated.onWarning, spec)\n\n        val info by animated(toBeAnimated.info, spec)\n        val onInfo by animated(toBeAnimated.onInfo, spec)\n\n\n        val isLight = toBeAnimated.isLight\n\n        return MyColors(\n            primary = primary,\n            primaryVariant = primaryVariant,\n            onPrimary = onPrimary,\n            secondary = secondary,\n            secondaryVariant = secondaryVariant,\n            onSecondary = onSecondary,\n            background = background,\n            onBackground = onBackground,\n            onSurface = onSurface,\n            surface = surface,\n            error = error,\n            onError = onError,\n            success = success,\n            onSuccess = onSuccess,\n            warning = warning,\n            onWarning = onWarning,\n            info = info,\n            onInfo = onInfo,\n            isLight = isLight,\n            name = toBeAnimated.name,\n            id = toBeAnimated.id,\n        )\n    }\n\n    @Composable\n    private fun animated(\n        color: Color,\n        animationSpec: AnimationSpec<Color> = tween(500),\n    ): State<Color> {\n        return animateColorAsState(color, animationSpec = animationSpec)\n    }\n}\n\n// it seems this method is more laggy! even though it uses single transition!\nprivate object AnimateMyColorsWithSingleTransition {\n    @Composable\n    fun animatedColors(\n        toBeAnimated: MyColors,\n        spec: FiniteAnimationSpec<Color> = tween(500)\n    ): MyColors {\n        val spec: @Composable Transition.Segment<MyColors>.() -> FiniteAnimationSpec<Color> = { spec }\n        val transition = updateTransition(toBeAnimated, \"animateMyColors\")\n\n        val primary by transition.animateColor(spec, \"primary\") { it.primary }\n        val primaryVariant by transition.animateColor(spec, \"primaryVariant\") { it.primaryVariant }\n        val onPrimary by transition.animateColor(spec, \"onPrimary\") { it.onPrimary }\n\n        val secondary by transition.animateColor(spec, \"secondary\") { it.secondary }\n        val secondaryVariant by transition.animateColor(spec, \"secondaryVariant\") { it.secondaryVariant }\n        val onSecondary by transition.animateColor(spec, \"onSecondary\") { it.onSecondary }\n\n        val background by transition.animateColor(spec, \"background\") { it.background }\n        val onBackground by transition.animateColor(spec, \"onBackground\") { it.onBackground }\n\n        val surface by transition.animateColor(spec, \"surface\") { it.surface }\n        val onSurface by transition.animateColor(spec, \"onSurface\") { it.onSurface }\n\n        val success by transition.animateColor(spec, \"success\") { it.success }\n        val onSuccess by transition.animateColor(spec, \"onSuccess\") { it.onSuccess }\n\n        val error by transition.animateColor(spec, \"error\") { it.error }\n        val onError by transition.animateColor(spec, \"onError\") { it.onError }\n\n        val warning by transition.animateColor(spec, \"warning\") { it.warning }\n        val onWarning by transition.animateColor(spec, \"onWarning\") { it.onWarning }\n\n        val info by transition.animateColor(spec, \"info\") { it.info }\n        val onInfo by transition.animateColor(spec, \"onInfo\") { it.onInfo }\n\n        return MyColors(\n            primary = primary,\n            primaryVariant = primaryVariant,\n            onPrimary = onPrimary,\n            secondary = secondary,\n            secondaryVariant = secondaryVariant,\n            onSecondary = onSecondary,\n            background = background,\n            onBackground = onBackground,\n            onSurface = onSurface,\n            surface = surface,\n            error = error,\n            onError = onError,\n            success = success,\n            onSuccess = onSuccess,\n            warning = warning,\n            onWarning = onWarning,\n            info = info,\n            onInfo = onInfo,\n            isLight = toBeAnimated.isLight,\n            name = toBeAnimated.name,\n            id = toBeAnimated.id,\n        )\n    }\n}\n\n@Composable\nfun animatedColors(\n    toBeAnimated: MyColors,\n    spec: FiniteAnimationSpec<Color> = tween(500),\n): MyColors {\n    return AnimateMyColors.animatedColors(\n        toBeAnimated = toBeAnimated,\n        spec = spec,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/ScrollbableContent.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.foundation.ScrollState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.layout.wrapContentWidth\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\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.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport io.github.oikvpqya.compose.fastscroller.ScrollbarAdapter\nimport io.github.oikvpqya.compose.fastscroller.ScrollbarStyle\nimport io.github.oikvpqya.compose.fastscroller.rememberScrollbarAdapter\n\n\n@Composable\nfun TwoDimensionScrollableContent(\n    modifier: Modifier,\n    content: @Composable () -> Unit,\n    verticalAdapter: ScrollbarAdapter,\n    horizontalAdapter: ScrollbarAdapter\n) {\n    Row(modifier) {\n        Column(Modifier.weight(1f)) {\n            Box(Modifier.weight(1f)) {\n                content()\n            }\n            if (horizontalAdapter.needScroll()) {\n                MultiplatformHorizontalScrollbar(\n                    horizontalAdapter,\n                    Modifier.padding(\n                        top = 4.dp,\n                        bottom = 4.dp,\n                    )\n                )\n            }\n        }\n        if (verticalAdapter.needScroll()) {\n            MultiplatformVerticalScrollbar(\n                verticalAdapter,\n                Modifier.padding(\n                    start = 4.dp,\n                    end = 4.dp,\n                    bottom = 4.dp\n                )\n            )\n        }\n    }\n}\n\n@Composable\nfun VerticalScrollableContent(\n    scrollState: ScrollState,\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit,\n) {\n    VerticalScrollableContent(\n        verticalAdapter = rememberScrollbarAdapter(scrollState),\n        modifier = modifier,\n        content = content,\n    )\n}\n\n@Composable\nfun VerticalScrollableContent(\n    lazyListState: LazyListState,\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit,\n) {\n    VerticalScrollableContent(\n        verticalAdapter = rememberScrollbarAdapter(lazyListState),\n        modifier = modifier,\n        content = content,\n    )\n}\n\n@Composable\nfun VerticalScrollableContent(\n    verticalAdapter: ScrollbarAdapter,\n    modifier: Modifier = Modifier,\n    style: ScrollbarStyle = LocalMultiplatformScrollbarStyle.current,\n    content: @Composable () -> Unit,\n) {\n    Box(modifier) {\n        val needScroll = verticalAdapter.needScroll()\n        val horizontalPadding = 4.dp\n        val endPadding = if (needScroll) {\n            style.thickness + horizontalPadding\n        } else {\n            0.dp\n        }\n        Box(Modifier.padding(end = endPadding)) {\n            content()\n        }\n        if (needScroll) {\n            MultiplatformVerticalScrollbar(\n                verticalAdapter,\n                modifier = Modifier\n                    .matchParentSize()\n                    .wrapContentWidth(Alignment.End)\n                    .padding(\n                        horizontal = horizontalPadding,\n                    )\n                    .width(style.thickness),\n                style = style,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/Scrollbar.kt",
    "content": "package com.abdownloadmanager.shared.util.ui\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.Modifier\nimport io.github.oikvpqya.compose.fastscroller.*\n\nval LocalMultiplatformScrollbarStyle = staticCompositionLocalOf<ScrollbarStyle> {\n    error(\"Scrollbar style not provided\")\n}\n\nfun ScrollbarAdapter.needScroll(): Boolean {\n    return contentSize > viewportSize\n}\n\nfun multiplatformDefaultScrollbarStyle(): ScrollbarStyle {\n    return defaultScrollbarStyle()\n}\n\n@Composable\nfun MultiplatformHorizontalScrollbar(\n    adapter: ScrollbarAdapter,\n    modifier: Modifier = Modifier,\n    style: ScrollbarStyle = LocalMultiplatformScrollbarStyle.current,\n    reverseLayout: Boolean = false,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    enablePressToScroll: Boolean = true,\n    indicator: @Composable (position: Float, isVisible: Boolean) -> Unit = { _, _ -> },\n) {\n    // I intentionally wrapped it seems there is a bug in the library that consume more than thickness and breaks the UI\n    Box(modifier) {\n        HorizontalScrollbar(\n            adapter = adapter,\n            style = style,\n            modifier = Modifier,\n            reverseLayout = reverseLayout,\n            interactionSource = interactionSource,\n            enablePressToScroll = enablePressToScroll,\n            indicator = indicator,\n        )\n    }\n}\n\n@Composable\nfun MultiplatformVerticalScrollbar(\n    adapter: ScrollbarAdapter,\n    modifier: Modifier = Modifier,\n    style: ScrollbarStyle = LocalMultiplatformScrollbarStyle.current,\n    reverseLayout: Boolean = false,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    enablePressToScroll: Boolean = true,\n    indicator: @Composable (position: Float, isVisible: Boolean) -> Unit = { _, _ -> },\n) {\n    Box(modifier) {\n        VerticalScrollbar(\n            adapter = adapter,\n            style = style,\n            modifier = Modifier,\n            reverseLayout = reverseLayout,\n            interactionSource = interactionSource,\n            enablePressToScroll = enablePressToScroll,\n            indicator = indicator,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/icon/MyIcons.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.icon\n\nimport com.abdownloadmanager.resources.icons.ABDMIcons\nimport com.abdownloadmanager.resources.icons.*\nimport com.abdownloadmanager.shared.util.ui.BaseMyColors\nimport ir.amirab.util.compose.IconSource\n\nobject MyIcons : BaseMyColors() {\n    override val appIcon = ABDMIcons.AppIcon.asIconSource(\"appIcon\", false)\n\n    override val settings = ABDMIcons.Settings.asIconSource(\"settings\")\n    override val flag = ABDMIcons.Flag.asIconSource(\"flag\")\n    override val fast = ABDMIcons.Fast.asIconSource(\"fast\")\n    override val search = ABDMIcons.Search.asIconSource(\"search\")\n    override val info = ABDMIcons.Info.asIconSource(\"info\")\n    override val check = ABDMIcons.Check.asIconSource(\"check\")\n    override val link = ABDMIcons.AddLink.asIconSource(\"link\")\n    override val download = ABDMIcons.DownSpeed.asIconSource(\"download\")\n    override val permission = ABDMIcons.Permission.asIconSource(\"permission\")\n\n    override val windowMinimize = ABDMIcons.WindowMinimize.asIconSource(\"windowMinimize\")\n    override val windowFloating = ABDMIcons.WindowFloating.asIconSource(\"windowFloating\")\n    override val windowMaximize = ABDMIcons.WindowMaximize.asIconSource(\"windowMaximize\")\n    override val windowClose = ABDMIcons.WindowClose.asIconSource(\"windowClose\")\n\n    override val exit = ABDMIcons.Exit.asIconSource(\"exit\")\n    override val edit = ABDMIcons.Edit.asIconSource(\"edit\")\n    override val undo = ABDMIcons.Undo.asIconSource(\"undo\")\n\n    override val openSource = ABDMIcons.OpenSource.asIconSource(\"openSource\")\n    override val telegram = ABDMIcons.Telegram.asIconSource(\"telegram\", false)\n    override val speaker = ABDMIcons.Speaker.asIconSource(\"speaker\")\n    override val group = ABDMIcons.Group.asIconSource(\"group\")\n\n    override val browserMozillaFirefox = ABDMIcons.BrowserMozillaFirefox.asIconSource(\"browserMozillaFirefox\", false)\n    override val browserGoogleChrome = ABDMIcons.BrowserGoogleChrome.asIconSource(\"browserGoogleChrome\", false)\n    override val browserMicrosoftEdge = ABDMIcons.BrowserMicrosoftEdge.asIconSource(\"browserMicrosoftEdge\", false)\n    override val browserOpera = ABDMIcons.BrowserOpera.asIconSource(\"browserOpera\", false)\n\n    override val next = ABDMIcons.Next.asIconSource(\"next\")\n    override val back = ABDMIcons.Back.asIconSource(\"back\")\n    override val up = ABDMIcons.Up.asIconSource(\"up\")\n    override val down = ABDMIcons.Down.asIconSource(\"down\")\n\n    override val activeCount = ABDMIcons.List.asIconSource(\"activeCount\")\n    override val speed = ABDMIcons.DownSpeed.asIconSource(\"speed\")\n\n    override val resume = ABDMIcons.Resume.asIconSource(\"resume\")\n    override val pause = ABDMIcons.Pause.asIconSource(\"pause\")\n    override val stop = ABDMIcons.Stop.asIconSource(\"stop\")\n\n    override val queue = ABDMIcons.Queue.asIconSource(\"queue\")\n    override val queueStart = ABDMIcons.QueueStart.asIconSource(\"queueStart\")\n    override val queueStop = ABDMIcons.QueueStop.asIconSource(\"queueStop\")\n\n    override val remove = ABDMIcons.Delete.asIconSource(\"remove\")\n    override val clear = ABDMIcons.Clear.asIconSource(\"clear\")\n    override val add = ABDMIcons.Plus.asIconSource(\"add\")\n    override val minus = ABDMIcons.Minus.asIconSource(\"add\")\n    override val paste = ABDMIcons.Clipboard.asIconSource(\"paste\")\n\n    override val copy = ABDMIcons.Copy.asIconSource(\"copy\")\n    override val refresh = ABDMIcons.Refresh.asIconSource(\"refresh\")\n    override val editFolder = ABDMIcons.Folder.asIconSource(\"editFolder\")\n\n    override val share = ABDMIcons.Share.asIconSource(\"share\")\n    override val file = ABDMIcons.File.asIconSource(\"file\")\n    override val folder = ABDMIcons.Folder.asIconSource(\"folder\")\n\n    override val fileOpen = file\n    override val folderOpen = folder\n    override val pictureFile = ABDMIcons.FilePicture.asIconSource(\"fileOpen\")\n    override val musicFile = ABDMIcons.FileMusic.asIconSource(\"folderOpen\")\n    override val zipFile = ABDMIcons.FileZip.asIconSource(\"pictureFile\")\n    override val videoFile = ABDMIcons.FileVideo.asIconSource(\"musicFile\")\n    override val applicationFile = ABDMIcons.FileApplication.asIconSource(\"zipFile\")\n    override val documentFile = ABDMIcons.FileDocument.asIconSource(\"videoFile\")\n    override val otherFile = ABDMIcons.FileUnknown.asIconSource(\"applicationFile\")\n\n    override val lock = ABDMIcons.Lock.asIconSource(\"lock\")\n    override val question = ABDMIcons.QuestionMark.asIconSource(\"question\")\n\n    override val grip = ABDMIcons.Grip.asIconSource(\"grip\")\n    override val sortUp = ABDMIcons.Sort123.asIconSource(\"sortUp\")\n    override val sortDown = ABDMIcons.Sort321.asIconSource(\"sortDown\")\n    override val verticalDirection = ABDMIcons.VerticalDirection.asIconSource(\"verticalDirection\")\n\n    override val browserIntegration = ABDMIcons.Earth.asIconSource(\"browserIntegration\")\n    override val appearance = ABDMIcons.Colors.asIconSource(\"appearance\")\n    override val downloadEngine = ABDMIcons.DownSpeed.asIconSource(\"downloadEngine\")\n    override val network = ABDMIcons.Network.asIconSource(\"network\")\n    override val language = ABDMIcons.Language.asIconSource(\"language\")\n\n    override val externalLink = ABDMIcons.ExternalLink.asIconSource(\"externalLink\")\n    override val earth = ABDMIcons.Earth.asIconSource(\"earth\")\n    override val hearth = ABDMIcons.Hearth.asIconSource(\"hearth\")\n    override val dragAndDrop = ABDMIcons.DragAndDrop.asIconSource(\"dragAndDrop\")\n\n\n    override val selectAll = ABDMIcons.SelectAll.asIconSource(\"selectAll\")\n    override val selectInside = ABDMIcons.SelectInside.asIconSource(\"selectInside\")\n    override val selectInvert = ABDMIcons.SelectInvert.asIconSource(\"selectInvert\")\n\n    override val menu = ABDMIcons.Menu.asIconSource(\"menu\")\n\n    override val close: IconSource = ABDMIcons.Clear.asIconSource(\"close\")\n\n    override val data: IconSource = ABDMIcons.Data.asIconSource(\"alphabet\")\n    override val alphabet: IconSource = ABDMIcons.Alphabet.asIconSource(\"alphabet\")\n    override val clock: IconSource = ABDMIcons.Clock.asIconSource(\"clock\")\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/theme/ISystemThemeDetector.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.theme\n\nimport kotlinx.coroutines.flow.Flow\n\ninterface ISystemThemeDetector {\n    val isSupported: Boolean\n    fun isDark(): Boolean\n    val systemThemeFlow: Flow<Boolean>\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/theme/MaterialRipple.kt",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\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 */\n\npackage com.abdownloadmanager.shared.util.ui.theme\n\nimport androidx.compose.foundation.Indication\nimport androidx.compose.foundation.IndicationNodeFactory\nimport androidx.compose.foundation.interaction.Interaction\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.material.ripple.RippleAlpha\nimport androidx.compose.material.ripple.createRippleModifierNode\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorProducer\nimport androidx.compose.ui.graphics.isSpecified\nimport androidx.compose.ui.graphics.luminance\nimport androidx.compose.ui.node.CompositionLocalConsumerModifierNode\nimport androidx.compose.ui.node.DelegatableNode\nimport androidx.compose.ui.node.DelegatingNode\nimport androidx.compose.ui.node.ObserverModifierNode\nimport androidx.compose.ui.node.currentValueOf\nimport androidx.compose.ui.node.observeReads\nimport androidx.compose.ui.unit.Dp\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.LocalMyColors\n\n/**\n * Creates a Ripple using the provided values and values inferred from the theme.\n *\n * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s\n * by drawing ripple animations and state layers.\n *\n * A Ripple responds to [PressInteraction.Press] by starting a new animation, and\n * responds to other [Interaction]s by showing a fixed state layer with varying alpha values\n * depending on the [Interaction].\n *\n * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple\n * will be used as the default [Indication] inside components such as\n * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in\n * addition to Material provided components that use a Ripple as well.\n *\n * You can also explicitly create a Ripple and provide it to custom components in order to change\n * the parameters from the default, such as to create an unbounded ripple with a fixed size.\n *\n * To create a Ripple with a manually defined color that can change over time, see the other\n * [ripple] overload with a [ColorProducer] parameter. This will avoid unnecessary recompositions\n * when changing the color, and preserve existing ripple state when the color changes.\n *\n * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded\n * ripples always animate from the target layout center, bounded ripples animate from the touch\n * position.\n * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be\n * calculated based on the target layout size.\n * @param color the color of the ripple. This color is usually the same color used by the text or\n * iconography in the component. This color will then have [RippleDefaults.rippleAlpha]\n * applied to calculate the final color used to draw the ripple. If [Color.Unspecified] is\n * provided the color used will be [RippleDefaults.rippleColor] instead.\n */\n@Stable\nfun ripple(\n    bounded: Boolean = true,\n    radius: Dp = Dp.Unspecified,\n    color: Color = Color.Unspecified,\n): IndicationNodeFactory {\n    return if (radius == Dp.Unspecified && color == Color.Unspecified) {\n        if (bounded) return DefaultBoundedRipple else DefaultUnboundedRipple\n    } else {\n        RippleNodeFactory(bounded, radius, color)\n    }\n}\n\n/**\n * Creates a Ripple using the provided values and values inferred from the theme.\n *\n * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s\n * by drawing ripple animations and state layers.\n *\n * A Ripple responds to [PressInteraction.Press] by starting a new ripple animation, and\n * responds to other [Interaction]s by showing a fixed state layer with varying alpha values\n * depending on the [Interaction].\n *\n * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple\n * will be used as the default [Indication] inside components such as\n * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in\n * addition to Material provided components that use a Ripple as well.\n *\n * You can also explicitly create a Ripple and provide it to custom components in order to change\n * the parameters from the default, such as to create an unbounded ripple with a fixed size.\n *\n * To create a Ripple with a static color, see the [ripple] overload with a [Color] parameter. This\n * overload is optimized for Ripples that have dynamic colors that change over time, to reduce\n * unnecessary recompositions.\n *\n * @param color the color of the ripple. This color is usually the same color used by the text or\n * iconography in the component. This color will then have [RippleDefaults.rippleAlpha]\n * applied to calculate the final color used to draw the ripple. If you are creating this\n * [ColorProducer] outside of composition (where it will be automatically remembered), make sure\n * that its instance is stable (such as by remembering the object that holds it), or remember the\n * returned [ripple] object to make sure that ripple nodes are not being created each recomposition.\n * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded\n * ripples always animate from the target layout center, bounded ripples animate from the touch\n * position.\n * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be\n * calculated based on the target layout size.\n */\n@Stable\nfun ripple(\n    color: ColorProducer,\n    bounded: Boolean = true,\n    radius: Dp = Dp.Unspecified,\n): IndicationNodeFactory {\n    return RippleNodeFactory(bounded, radius, color)\n}\n\n/**\n * Default values used by [ripple].\n */\nobject RippleDefaults {\n    /**\n     * Represents the default color that will be used for a ripple if a color has not been\n     * explicitly set on the ripple instance.\n     *\n     * @param contentColor the color of content (text or iconography) in the component that\n     * contains the ripple.\n     * @param lightTheme whether the theme is light or not\n     */\n    fun rippleColor(\n        contentColor: Color,\n        lightTheme: Boolean,\n    ): Color {\n        val contentLuminance = contentColor.luminance()\n        // If we are on a colored surface (typically indicated by low luminance content), the\n        // ripple color should be white.\n        return if (!lightTheme && contentLuminance < 0.5) {\n            Color.White\n            // Otherwise use contentColor\n        } else {\n            contentColor\n        }\n    }\n\n    /**\n     * Represents the default [RippleAlpha] that will be used for a ripple to indicate different\n     * states.\n     *\n     * @param contentColor the color of content (text or iconography) in the component that\n     * contains the ripple.\n     * @param lightTheme whether the theme is light or not\n     */\n    fun rippleAlpha(contentColor: Color, lightTheme: Boolean): RippleAlpha {\n        return when {\n            lightTheme -> {\n                if (contentColor.luminance() > 0.5) {\n                    LightThemeHighContrastRippleAlpha\n                } else {\n                    LightThemeLowContrastRippleAlpha\n                }\n            }\n\n            else -> {\n                DarkThemeRippleAlpha\n            }\n        }\n    }\n}\n\n/**\n * CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a\n * tree-local 'override' for ripples used inside components that you cannot directly control, such\n * as to change the color of a specific component's ripple, or disable it entirely by providing\n * `null`.\n *\n * In most cases you should rely on the default theme behavior for consistency with other components\n * - this exists as an escape hatch for individual components and is not intended to be used for\n * full theme customization across an application. For this use case you should instead build your\n * own custom ripple that queries your design system theme values directly using\n * [createRippleModifierNode].\n */\n@Suppress(\"OPT_IN_MARKER_ON_WRONG_TARGET\")\nval LocalRippleConfiguration: ProvidableCompositionLocal<RippleConfiguration?> =\n    compositionLocalOf { RippleConfiguration() }\n\n/**\n * Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases\n * the default values should be used, for custom design system use cases you should instead\n * build your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide\n * `null` using [LocalRippleConfiguration].\n *\n * @param color the color override for the ripple. If [Color.Unspecified], then the default color\n * from the theme will be used instead. Note that if the ripple has a color explicitly set with\n * the parameter on [ripple], that will always be used instead of this value.\n * @param rippleAlpha the [RippleAlpha] override for this ripple. If null, then the default alpha\n * will be used instead.\n */\n@Immutable\nclass RippleConfiguration(\n    val color: Color = Color.Unspecified,\n    val rippleAlpha: RippleAlpha? = null,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is RippleConfiguration) return false\n\n        if (color != other.color) return false\n        if (rippleAlpha != other.rippleAlpha) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = color.hashCode()\n        result = 31 * result + (rippleAlpha?.hashCode() ?: 0)\n        return result\n    }\n\n    override fun toString(): String {\n        return \"RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)\"\n    }\n}\n\n@Stable\nprivate class RippleNodeFactory private constructor(\n    private val bounded: Boolean,\n    private val radius: Dp,\n    private val colorProducer: ColorProducer?,\n    private val color: Color,\n) : IndicationNodeFactory {\n    constructor(\n        bounded: Boolean,\n        radius: Dp,\n        colorProducer: ColorProducer,\n    ) : this(bounded, radius, colorProducer, Color.Unspecified)\n\n    constructor(\n        bounded: Boolean,\n        radius: Dp,\n        color: Color,\n    ) : this(bounded, radius, null, color)\n\n    override fun create(interactionSource: InteractionSource): DelegatableNode {\n        val colorProducer = colorProducer ?: ColorProducer { color }\n        return DelegatingThemeAwareRippleNode(interactionSource, bounded, radius, colorProducer)\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is RippleNodeFactory) return false\n\n        if (bounded != other.bounded) return false\n        if (radius != other.radius) return false\n        if (colorProducer != other.colorProducer) return false\n        return color == other.color\n    }\n\n    override fun hashCode(): Int {\n        var result = bounded.hashCode()\n        result = 31 * result + radius.hashCode()\n        result = 31 * result + colorProducer.hashCode()\n        result = 31 * result + color.hashCode()\n        return result\n    }\n}\n\nprivate class DelegatingThemeAwareRippleNode(\n    private val interactionSource: InteractionSource,\n    private val bounded: Boolean,\n    private val radius: Dp,\n    private val color: ColorProducer,\n) : DelegatingNode(), CompositionLocalConsumerModifierNode, ObserverModifierNode {\n    private var rippleNode: DelegatableNode? = null\n\n    override fun onAttach() {\n        updateConfiguration()\n    }\n\n    override fun onObservedReadsChanged() {\n        updateConfiguration()\n    }\n\n    /**\n     * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to\n     * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of\n     * the ripple definition.\n     */\n    private fun updateConfiguration() {\n        observeReads {\n            val configuration = currentValueOf(LocalRippleConfiguration)\n            if (configuration == null) {\n                removeRipple()\n            } else {\n                if (rippleNode == null) attachNewRipple()\n            }\n        }\n    }\n\n    private fun attachNewRipple() {\n        val calculateColor = ColorProducer {\n            val userDefinedColor = color()\n            if (userDefinedColor.isSpecified) {\n                userDefinedColor\n            } else {\n                // If this is null, the ripple will be removed, so this should always be non-null in\n                // normal use\n                val rippleConfiguration = currentValueOf(LocalRippleConfiguration)\n                if (rippleConfiguration?.color?.isSpecified == true) {\n                    rippleConfiguration.color\n                } else {\n                    RippleDefaults.rippleColor(\n                        contentColor = currentValueOf(LocalContentColor),\n                        lightTheme = currentValueOf(LocalMyColors).isLight\n                    )\n                }\n            }\n        }\n        val calculateRippleAlpha = {\n            // If this is null, the ripple will be removed, so this should always be non-null in\n            // normal use\n            val rippleConfiguration = currentValueOf(LocalRippleConfiguration)\n            rippleConfiguration?.rippleAlpha ?: RippleDefaults.rippleAlpha(\n                contentColor = currentValueOf(LocalContentColor),\n                lightTheme = currentValueOf(LocalMyColors).isLight\n            )\n        }\n\n        rippleNode = delegate(\n            createRippleModifierNode(\n                interactionSource,\n                bounded,\n                radius,\n                calculateColor,\n                calculateRippleAlpha\n            )\n        )\n    }\n\n    private fun removeRipple() {\n        rippleNode?.let { undelegate(it) }\n    }\n}\n\nprivate val DefaultBoundedRipple = RippleNodeFactory(\n    bounded = true,\n    radius = Dp.Unspecified,\n    color = Color.Unspecified\n)\n\nprivate val DefaultUnboundedRipple = RippleNodeFactory(\n    bounded = false,\n    radius = Dp.Unspecified,\n    color = Color.Unspecified\n)\n\n/**\n * Alpha values for high luminance content in a light theme.\n *\n * This content will typically be placed on colored surfaces, so it is important that the\n * contrast here is higher to meet accessibility standards, and increase legibility.\n *\n * These levels are typically used for text / iconography in primary colored tabs /\n * bottom navigation / etc.\n */\nprivate val LightThemeHighContrastRippleAlpha = RippleAlpha(\n    pressedAlpha = 0.24f,\n    focusedAlpha = 0.24f,\n    draggedAlpha = 0.16f,\n    hoveredAlpha = 0.08f\n)\n\n/**\n * Alpha levels for low luminance content in a light theme.\n *\n * This content will typically be placed on grayscale surfaces, so the contrast here can be lower\n * without sacrificing accessibility and legibility.\n *\n * These levels are typically used for body text on the main surface (white in light theme, grey\n * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.\n */\nprivate val LightThemeLowContrastRippleAlpha = RippleAlpha(\n    pressedAlpha = 0.12f,\n    focusedAlpha = 0.12f,\n    draggedAlpha = 0.08f,\n    hoveredAlpha = 0.04f\n)\n\n/**\n * Alpha levels for all content in a dark theme.\n */\nprivate val DarkThemeRippleAlpha = RippleAlpha(\n    pressedAlpha = 0.10f,\n    focusedAlpha = 0.12f,\n    draggedAlpha = 0.08f,\n    hoveredAlpha = 0.04f\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/theme/MyShapes.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.theme\n\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.unit.dp\n\n\nval LocalMyShapes = staticCompositionLocalOf<MyShapes> {\n    error(\"LocalMyShapes not provided\")\n}\n\nval myShapes\n    @Composable\n    get() = LocalMyShapes.current\n\nprivate val ZeroCornerSize = CornerSize(0.dp)\n\n@Stable\ndata class MyShapes(\n    val defaultRounded: RoundedCornerShape,\n) {\n    val bottomSheet = defaultRounded.copy(\n        bottomStart = ZeroCornerSize,\n        bottomEnd = ZeroCornerSize,\n    )\n    val dialog = defaultRounded\n    fun createSheetWithCustomEdges(\n        topStart: Boolean,\n        bottomStart: Boolean,\n        topEnd: Boolean,\n        bottomEnd: Boolean,\n    ): RoundedCornerShape {\n        return RoundedCornerShape(\n            bottomStart = if (bottomStart) defaultRounded.bottomStart else ZeroCornerSize,\n            bottomEnd = if (bottomEnd) defaultRounded.bottomEnd else ZeroCornerSize,\n            topStart = if (topStart) defaultRounded.topStart else ZeroCornerSize,\n            topEnd = if (topEnd) defaultRounded.topEnd else ZeroCornerSize,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/theme/Sizing.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.theme\n\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\n\nval LocalSystemDensity = staticCompositionLocalOf<Density> {\n    error(\"LocalSystemDensity not provided\")\n}\n\nconst val DEFAULT_UI_SCALE = 1f\n\nval LocalUiScale = staticCompositionLocalOf<Float> { DEFAULT_UI_SCALE }\n\nval LocalTextSizes = compositionLocalOf<TextSizes> {\n    error(\"LocalTextSizes not provided\")\n}\n\nval myTextSizes\n    @Composable\n    get() = LocalTextSizes.current\n\n@Stable\ndata class TextSizes(\n    val xs: TextUnit,\n    val sm: TextUnit,\n    val base: TextUnit,\n    val lg: TextUnit,\n    val xl: TextUnit,\n    val x2l: TextUnit,\n    val x3l: TextUnit,\n    val x4l: TextUnit,\n    val x5l: TextUnit,\n)\n\nval LocalSpacing = compositionLocalOf<MySpacings> {\n    error(\"LocalSpacing not provided\")\n}\nval mySpacings\n    @Composable\n    get() = LocalSpacing.current\n\n\n@Stable\ndata class MySpacings(\n    val thumbSize: Dp,\n    val iconSize: Dp,\n    val smallSpace: Dp,\n    val mediumSpace: Dp,\n    val largeSpace: Dp,\n)\n\n\n/**\n * put this in every window because [Window] composable override [LocalDensity]\n */\n@Composable\nfun UiScaledContent(\n    defaultDensity: Density = LocalDensity.current,\n    uiScale: Float = LocalUiScale.current,\n    content: @Composable () -> Unit,\n) {\n    val density = remember(defaultDensity, uiScale) {\n        if (uiScale == DEFAULT_UI_SCALE) {\n            defaultDensity\n        } else {\n            Density(uiScale * defaultDensity.density)\n        }\n    }\n    CompositionLocalProvider(\n        LocalDensity provides density,\n        content,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/Icon.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport androidx.compose.foundation.Image\nimport com.abdownloadmanager.shared.util.ui.LocalContentAlpha\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.NonRestartableComposable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.paint\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.painter.BitmapPainter\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.toolingGraphicsLayer\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport ir.amirab.util.compose.IconSource\n\n@Composable\n@NonRestartableComposable\nfun Icon(\n    imageVector: ImageVector,\n    contentDescription: String?,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)\n) {\n    Icon(\n        painter = rememberVectorPainter(imageVector),\n        contentDescription = contentDescription,\n        modifier = modifier,\n        tint = tint\n    )\n}\n\n@Composable\n@NonRestartableComposable\nfun Icon(\n    bitmap: ImageBitmap,\n    contentDescription: String?,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)\n) {\n    val painter = remember(bitmap) { BitmapPainter(bitmap) }\n    Icon(\n        painter = painter,\n        contentDescription = contentDescription,\n        modifier = modifier,\n        tint = tint\n    )\n}\n\n@Composable\nfun Icon(\n    painter: Painter,\n    contentDescription: String?,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)\n) {\n    val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)\n    val semantics = if (contentDescription != null) {\n        Modifier.semantics {\n            this.contentDescription = contentDescription\n            this.role = Role.Image\n        }\n    } else {\n        Modifier\n    }\n    Box(\n        modifier.toolingGraphicsLayer().defaultSizeFor(painter)\n            .paint(\n                painter,\n                colorFilter = colorFilter,\n                contentScale = ContentScale.Fit\n            )\n            .then(semantics)\n    )\n}\n\nprivate fun Modifier.defaultSizeFor(painter: Painter) =\n        this.then(\n            if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {\n                DefaultIconSizeModifier\n            } else {\n                Modifier\n            }\n        )\n\nprivate fun Size.isInfinite() = width.isInfinite() && height.isInfinite()\n\n// Default icon size, for icons with no intrinsic size information\nprivate val DefaultIconSizeModifier = Modifier.size(24.dp)\n\n@Composable\nfun MyIcon(\n    icon: IconSource,\n    contentDescription: String?,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),\n) {\n    val painter = icon.rememberPainter()\n    if (icon.requiredTint) {\n        Icon(\n            painter = painter,\n            contentDescription = contentDescription,\n            modifier = modifier,\n            tint = tint,\n        )\n    } else {\n        Image(\n            painter = painter,\n            contentDescription = contentDescription,\n            modifier = modifier,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/MPBackHandler.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nexpect fun MPBackHandler(\n    isEnabled: Boolean = true,\n    onBack: () -> Unit,\n)\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/ScreenSurface.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.WithContentColor\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\n\n@Composable\nfun ScreenSurface(\n    modifier: Modifier,\n    background: Brush,\n    contentColor: Color,\n    content: @Composable BoxScope.() -> Unit,\n) {\n    Box(\n        modifier\n            .background(background)\n    ) {\n        WithContentColor(contentColor) {\n            content()\n        }\n    }\n}\n\n@Composable\nfun ScreenSurface(\n    modifier: Modifier,\n    background: Color,\n    contentColor: Color = myColors.getContentColorFor(background),\n    content: @Composable BoxScope.() -> Unit,\n) {\n    Box(\n        modifier\n            .background(background)\n    ) {\n        WithContentColor(contentColor) {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/commonMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/ScrolFade.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.ScrollState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\n\n\n@Composable\nfun BoxScope.ScrollFade(\n    scrollState: ScrollState,\n    orientation: Orientation,\n    gradientLength: Float = 0.2f, // 0f .. 1f\n    targetBackground: Color,\n) {\n    AnimatedVisibility(\n        scrollState.canScrollBackward,\n        modifier = Modifier.matchParentSize(),\n        enter = fadeIn(),\n        exit = fadeOut(),\n    ) {\n        Spacer(\n            Modifier\n                .fillMaxSize()\n                .background(\n                    Brush.gradientOrientation(\n                        colorStops = arrayOf(\n                            0f to targetBackground,\n                            gradientLength to Color.Transparent,\n                            1f to Color.Transparent,\n                        ),\n                        orientation = orientation,\n                    )\n                )\n        )\n    }\n    AnimatedVisibility(\n        scrollState.canScrollForward,\n        modifier = Modifier.matchParentSize(),\n        enter = fadeIn(),\n        exit = fadeOut(),\n    ) {\n        Spacer(\n            Modifier\n                .fillMaxSize()\n                .background(\n                    Brush.gradientOrientation(\n                        colorStops = arrayOf(\n                            0f to Color.Transparent,\n                            (1 - gradientLength) to Color.Transparent,\n                            1f to targetBackground,\n                        ),\n                        orientation = orientation,\n                    )\n                )\n        )\n    }\n}\n\nprivate fun Brush.Companion.gradientOrientation(\n    vararg colorStops: Pair<Float, Color>,\n    orientation: Orientation,\n): Brush {\n    return when (orientation) {\n        Orientation.Vertical -> Brush.verticalGradient(\n            colorStops = colorStops\n        )\n\n        Orientation.Horizontal -> {\n            Brush.horizontalGradient(\n                colorStops = colorStops\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/modifier/PointerHoverIcon.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.modifier\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport org.jetbrains.skiko.Cursor\n\nactual fun Modifier.myPointerHoverIcon(\n    pointerHoverIcon: MyPointerHoverIcon,\n    overrideDescendants: Boolean\n): Modifier {\n    return pointerHoverIcon(\n        pointerHoverIcon.toDesktopIcon(),\n        overrideDescendants = overrideDescendants,\n    )\n}\n\nprivate fun MyPointerHoverIcon.toDesktopIcon(): PointerIcon {\n    return when (this) {\n        MyPointerHoverIcon.Crosshair -> PointerIcon.Crosshair\n        MyPointerHoverIcon.Default -> PointerIcon.Default\n        MyPointerHoverIcon.Hand -> PointerIcon.Hand\n        MyPointerHoverIcon.Text -> PointerIcon.Text\n        MyPointerHoverIcon.HorizontalResize -> pointerIconFromCursorInt(Cursor.S_RESIZE_CURSOR)\n        MyPointerHoverIcon.VerticalResize -> pointerIconFromCursorInt(Cursor.E_RESIZE_CURSOR)\n    }\n}\n\nprivate fun pointerIconFromCursorInt(\n    cursorInt: Int\n): PointerIcon {\n    return PointerIcon(Cursor(cursorInt))\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/theme/MyContextMenuRepresentation.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.foundation.ContextMenuItem\nimport androidx.compose.foundation.ContextMenuRepresentation\nimport androidx.compose.foundation.ContextMenuState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.PopupProperties\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.SubMenu\nimport com.abdownloadmanager.shared.ui.widget.rememberMyPopupPositionProviderAtPosition\nimport ir.amirab.util.compose.action.buildMenu\nimport ir.amirab.util.compose.asStringSource\n\nprivate class MyContextMenuRepresentation : ContextMenuRepresentation {\n    @Composable\n    override fun Representation(state: ContextMenuState, items: () -> List<ContextMenuItem>) {\n        val status = state.status\n        if (status !is ContextMenuState.Status.Open) {\n            return\n        }\n        val contextItems = items()\n        val menuItems = remember(contextItems) {\n            buildMenu {\n                contextItems.map {\n                    item(title = it.label.asStringSource(), onClick = {\n                        it.onClick()\n                    })\n                }\n            }\n        }\n        val onCloseRequest = { state.status = ContextMenuState.Status.Closed }\n        Popup(\n            properties = PopupProperties(\n                focusable = true,\n            ),\n            onDismissRequest = onCloseRequest,\n            popupPositionProvider = rememberMyPopupPositionProviderAtPosition(\n                positionPx = status.rect.center\n            ),\n        ) {\n            SubMenu(menuItems, onCloseRequest)\n        }\n    }\n}\n\n@Composable\ninternal fun myContextMenuRepresentation(): ContextMenuRepresentation {\n    return remember {\n        MyContextMenuRepresentation()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/theme/PlatformThemeDefinitions.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.theme\n\nimport androidx.compose.foundation.LocalContextMenuRepresentation\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.abdownloadmanager.shared.util.div\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.MyShapes\nimport com.abdownloadmanager.shared.util.ui.theme.MySpacings\nimport com.abdownloadmanager.shared.util.ui.theme.TextSizes\nimport io.github.oikvpqya.compose.fastscroller.ScrollbarStyle\nimport io.github.oikvpqya.compose.fastscroller.ThumbStyle\nimport io.github.oikvpqya.compose.fastscroller.TrackStyle\n\n@Composable\nactual fun PlatformDependentProviders(content: @Composable (() -> Unit)) {\n    CompositionLocalProvider(\n        LocalContextMenuRepresentation provides myContextMenuRepresentation(),\n        content = content,\n    )\n}\n\n@Composable\nactual fun myPlatformScrollbarStyle(): ScrollbarStyle {\n    val shape = RoundedCornerShape(4.dp)\n    return ScrollbarStyle(\n        minimalHeight = 16.dp,\n        thickness = 6.dp,\n        thumbStyle = ThumbStyle(\n            shape = shape,\n            unhoverColor = myColors.onBackground / 10,\n            hoverColor = myColors.onBackground / 30,\n        ),\n        trackStyle = TrackStyle(\n            unhoverColor = Color.Transparent,\n            hoverColor = Color.Transparent,\n            shape = RectangleShape,\n        ),\n        hoverDurationMillis = 300,\n    )\n}\n\nprivate val desktopTextSizes = TextSizes(\n    xs = 8.sp,\n    sm = 10.sp,\n    base = 12.sp,\n    lg = 14.sp,\n    xl = 16.sp,\n    x2l = 18.sp,\n    x3l = 20.sp,\n    x4l = 22.sp,\n    x5l = 24.sp,\n)\nprivate val desktopSpacings = MySpacings(\n    thumbSize = 24.dp,\n    iconSize = 16.dp,\n    smallSpace = 4.dp,\n    mediumSpace = 8.dp,\n    largeSpace = 16.dp,\n)\n\nval desktopShapes = MyShapes(\n    defaultRounded = RoundedCornerShape(6.dp)\n)\n\n@Composable\nactual fun myPlatformTextSizes(): TextSizes {\n    return desktopTextSizes\n}\n\n@Composable\nactual fun myPlatformShapes(): MyShapes {\n    return desktopShapes\n}\n\n@Composable\nactual fun myPlatformSpacing(): MySpacings {\n    return desktopSpacings\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/util/LocalWindow.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.util\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\nimport java.awt.Window\n\nval LocalWindow: ProvidableCompositionLocal<Window> = compositionLocalOf {\n    error(\"LocalWindow not provided yet\")\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/Tooltip.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget\n\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.PointerType\nimport androidx.compose.ui.input.pointer.pointerInput\nimport kotlinx.coroutines.coroutineScope\n\n\nactual fun Modifier.detectTooltip(\n    state: MutableState<Boolean>,\n): Modifier {\n    return pointerInput(state) {\n        coroutineScope {\n            awaitPointerEventScope {\n                val pass = PointerEventPass.Main\n\n                while (true) {\n                    val event = awaitPointerEvent(pass)\n                    val inputType = event.changes[0].type\n                    if (inputType == PointerType.Mouse) {\n                        when (event.type) {\n                            PointerEventType.Enter -> {\n                                state.value = true\n                            }\n\n                            PointerEventType.Exit -> {\n                                state.value = false\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/MenuBar.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\nimport ir.amirab.util.compose.action.MenuItem\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun MenuBar(\n    modifier: Modifier = Modifier,\n    subMenuList: List<MenuItem.SubMenu>,\n) {\n    var openedItem: MenuItem.SubMenu? by remember {\n        mutableStateOf(null)\n    }\n    val onRequestClose = {\n        openedItem = null\n    }\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        for (subMenu in subMenuList) {\n            val isSelected = openedItem == subMenu\n            val interactionSource = remember { MutableInteractionSource() }\n            val isHovered by interactionSource.collectIsHoveredAsState()\n            LaunchedEffect(isHovered) {\n                if (isHovered && openedItem != null) {\n                    openedItem = subMenu\n                }\n            }\n            Column {\n                Column(\n                    modifier\n                        .hoverable(interactionSource)\n                        .clickable {\n                            openedItem = subMenu\n                        }\n                        .ifThen(isSelected) {\n                            background(myColors.surface)\n                        }\n                        .padding(horizontal = 8.dp, vertical = 4.dp)\n                        .wrapContentHeight(Alignment.CenterVertically)\n                ) {\n                    val text = subMenu.title.collectAsState().value.rememberString()\n                    Text(\n                        text = text,\n                        maxLines = 1,\n                        fontSize = myTextSizes.base,\n                        color = myColors.onBackground,\n                    )\n                }\n                if (isSelected) {\n                    MyDropDown(\n                        onDismissRequest = onRequestClose,\n                        focusable = false,\n                    ) {\n                        CompositionLocalProvider(\n                            LocalMenuBoxClip provides RectangleShape\n                        ) {\n                            SubMenu(subMenu, onRequestClose = onRequestClose)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/Option.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.rememberCursorPositionProvider\nimport ir.amirab.util.compose.action.MenuItem\n\n@Composable\nactual fun ShowOptionsInPopup(\n    menu: MenuItem.SubMenu,\n    onDismissRequest: () -> Unit\n) {\n    Popup(\n        popupPositionProvider = rememberCursorPositionProvider(\n            alignment = Alignment.BottomEnd\n        ),\n        onDismissRequest = onDismissRequest\n    ) {\n        RenderOptions(menu, onDismissRequest)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/custom/WithContextMenu.desktop.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.custom\n\nimport androidx.compose.foundation.PointerMatcher\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.onClick\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.rememberCursorPositionProvider\nimport ir.amirab.util.compose.action.MenuItem\n\n@Composable\nactual fun WithContextMenu(\n    menuProvider: () -> List<MenuItem>,\n    modifier: Modifier,\n    content: @Composable (() -> Unit)\n) {\n    val menu = remember(menuProvider) {\n        mutableStateOf(emptyList<MenuItem>())\n    }\n    val onDismissRequest = {\n        menu.value = emptyList()\n    }\n    Box(\n        modifier.onClick(\n            matcher = PointerMatcher.mouse(\n                PointerButton.Secondary\n            )\n        ) {\n            menu.value = menuProvider()\n        }\n    ) {\n        content()\n        if (menu.value.isNotEmpty()) {\n            Popup(\n                popupPositionProvider = rememberCursorPositionProvider(\n                    alignment = Alignment.BottomEnd\n                ),\n                onDismissRequest = onDismissRequest\n            ) {\n                SubMenu(\n                    subMenu = menu.value,\n                    onRequestClose = onDismissRequest,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/menu/native/NativeMenuBar.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.menu.native\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyShortcut\nimport androidx.compose.ui.window.FrameWindowScope\nimport androidx.compose.ui.window.MenuBar\nimport androidx.compose.ui.window.MenuScope\nimport com.abdownloadmanager.shared.util.LocalShortCutManager\nimport com.abdownloadmanager.shared.util.PlatformKeyStroke\nimport com.abdownloadmanager.shared.util.ShortcutManager\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.action.MenuItem\n\n@Composable\nfun NativeMenuBar(\n    scope: FrameWindowScope,\n    subMenuList: List<MenuItem.SubMenu>\n) {\n    val shortcutManager = LocalShortCutManager.current\n\n    scope.MenuBar {\n        subMenuList.forEach { item ->\n            val items by item.items.collectAsState()\n            val title by item.title.collectAsState()\n            val enabled by item.isEnabled.collectAsState()\n            Menu(title.rememberString(), enabled = enabled) {\n                items.forEach { renderMenuItem(it, shortcutManager) }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MenuScope.renderMenuItem(item: MenuItem, shortcutManager: ShortcutManager?) {\n    when (item) {\n        is MenuItem.SubMenu -> {\n            val items by item.items.collectAsState()\n            val title by item.title.collectAsState()\n            val enabled by item.isEnabled.collectAsState()\n            Menu(title.rememberString(), enabled = enabled) {\n                items.forEach { renderMenuItem(it, shortcutManager) }\n            }\n        }\n\n        is MenuItem.Separator -> Separator()\n        is MenuItem.SingleItem -> {\n            val title by item.title.collectAsState()\n            val icon by item.icon.collectAsState()\n            val enabled by item.isEnabled.collectAsState()\n            val shortcut = remember(shortcutManager, item) {\n                shortcutManager?.getShortCutOf(item)?.toKeyShortcut()\n            }\n            Item(\n                title.rememberString(),\n                onClick = item::onClick,\n                icon = icon?.suitablePainterForMenu(),\n                enabled = enabled,\n                shortcut = shortcut\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun IconSource.suitablePainterForMenu(): Painter {\n    val isLight = !isSystemInDarkTheme()\n    return if (isLight && requiredTint) {\n        val painter = rememberPainter()\n        remember(painter) {\n            painter.withTint(Color.Black)\n        }\n    } else rememberPainter()\n}\n\nfun Painter.withTint(tint: Color): Painter = object : Painter() {\n    override val intrinsicSize = this@withTint.intrinsicSize\n\n    override fun DrawScope.onDraw() {\n        with(this@withTint) {\n            draw(size = size, colorFilter = ColorFilter.tint(tint))\n        }\n    }\n}\n\n\nprivate fun PlatformKeyStroke.toKeyShortcut(): KeyShortcut {\n    val mods = getModifiers().map { it.trim() }\n\n    return KeyShortcut(\n        key = Key(keyCode),\n        ctrl = mods.any { it in listOf(\"⌃\", \"ctrl\", \"control\") },\n        alt = mods.any { it in listOf(\"⌥\", \"alt\", \"option\") },\n        shift = mods.any { it in listOf(\"⇧\", \"shift\") },\n        meta = mods.any { it in listOf(\"⌘\", \"meta\", \"command\") }\n    )\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/table/customtable/CellPadding.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.table.customtable\n\n\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/table/customtable/Table.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.table.customtable\n\nimport com.abdownloadmanager.shared.util.ui.LocalContentColor\nimport com.abdownloadmanager.shared.util.ui.widget.MyIcon\nimport com.abdownloadmanager.shared.util.ui.icon.MyIcons\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.util.ui.theme.myTextSizes\nimport ir.amirab.util.ifThen\nimport com.abdownloadmanager.shared.ui.widget.CheckBox\nimport com.abdownloadmanager.shared.ui.widget.menu.custom.MenuColumn\nimport com.abdownloadmanager.shared.util.div\nimport ir.amirab.util.flow.saved\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.*\nimport androidx.compose.foundation.v2.ScrollbarAdapter\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.pointer.PointerButton\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Popup\nimport androidx.compose.ui.window.rememberCursorPositionProvider\nimport com.abdownloadmanager.resources.Res\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.ui.widget.sort.SortIndicatorMode\nimport com.abdownloadmanager.shared.ui.widget.sort.isAscending\nimport com.abdownloadmanager.shared.ui.widget.sort.isDescending\nimport com.abdownloadmanager.shared.ui.widget.sort.toSortIndicatorMode\nimport com.abdownloadmanager.shared.util.ui.MultiplatformHorizontalScrollbar\nimport com.abdownloadmanager.shared.util.ui.MultiplatformVerticalScrollbar\nimport com.abdownloadmanager.shared.util.ui.needScroll\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\nimport ir.amirab.util.compose.resources.myStringResource\nimport ir.amirab.util.shifted\nimport kotlinx.coroutines.flow.*\nimport sh.calvin.reorderable.ReorderableColumn\nimport sh.calvin.reorderable.ReorderableListItemScope\n\nval LocalCellPadding = compositionLocalOf {\n    PaddingValues(horizontal = 4.dp, vertical = 0.dp)\n}\nval LocalTableSize = compositionLocalOf<TableSize> {\n    error(\"LocalTableConstraints not provided\")\n}\nval LocalResizeCellsOnResizeTable = compositionLocalOf<Boolean> {\n    error(\"LocalResizeCellsOnResizeTable not provided\")\n}\n\n@Composable\nfun <T, C : TableCell<T>> Table(\n    list: List<T>,\n    key: ((T) -> Any)? = null,\n    tableState: TableState<T, C>,\n    modifier: Modifier = Modifier,\n    listState: LazyListState = rememberLazyListState(),\n    horizontalScrollState: ScrollState = rememberScrollState(),\n    resizeCellsOnResizeTableWidth: Boolean = false,\n    renderHeaderCell: @Composable (C) -> Unit = { DefaultRenderHeader(it) },\n    drawOnEmpty: @Composable BoxScope.() -> Unit = {},\n    wrapHeader: @Composable TableScope.(rowContent: @Composable () -> Unit) -> Unit = { content -> content() },\n    wrapItem: @Composable TableScope.(index: Int, item: T, rowContent: @Composable () -> Unit) -> Unit = { _, _, content -> content() },\n    renderCell: @Composable TableScope.(C, T) -> Unit,\n) {\n    val scope = TableScope\n\n    val visibleCells by tableState.visibleCells.collectAsState()\n    val cellOrder by tableState.order.collectAsState()\n\n    val cells = remember(visibleCells, cellOrder) {\n        cellOrder.filter {\n            it in visibleCells\n        }\n    }\n\n\n    val sortedBy by tableState.sortBy.collectAsState()\n    val customWidths by tableState.customSizes.collectAsState()\n    TwoDimensionScrollbar(\n        modifier = modifier,\n        content = {\n            BoxWithConstraints(Modifier.fillMaxSize()) {\n                CompositionLocalProvider(\n                    LocalTableSize provides TableSize(\n                        visibleWidth = maxWidth,\n                        visibleHeight = maxWidth,\n                    ),\n                    LocalResizeCellsOnResizeTable provides resizeCellsOnResizeTableWidth\n                ) {\n                    var showColumnConfig by remember {\n                        mutableStateOf(false)\n                    }\n                    if (showColumnConfig) {\n                        ShowColumnConfigMenu(\n                            onDismissRequest = { showColumnConfig = false },\n                            tableState = tableState\n                        )\n                    }\n                    Column(\n                        modifier = Modifier\n                            .horizontalScroll(horizontalScrollState),\n                    ) {\n                        scope.wrapHeader {\n                            Row(\n                                Modifier\n                                    .onClick(\n                                        matcher = {\n                                            it.button == PointerButton.Secondary\n                                        }\n                                    ) { showColumnConfig = true }\n                                    .height(IntrinsicSize.Max),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                cells.forEach { cell ->\n                                    val delta = scope.deltaWidthFraction(cell.size)\n                                    val shouldResizeWidthOnResizeTable = scope.getIsResizeCellOnResizeTable()\n                                    LaunchedEffect(cell.size, delta) {\n                                        if (shouldResizeWidthOnResizeTable && cell.size is CellSize.Resizeable) {\n                                            tableState.onCellSizeChanged(cell) { it * delta }\n                                        }\n                                    }\n                                    Row(\n                                        Modifier\n                                            .width(customWidths[cell] ?: cell.size.defaultWidth)\n//                                    .border(width = LocalCellPadding.current.calculateTopPadding(), myColors.primary,)\n                                            .padding(LocalCellPadding.current),\n                                        verticalAlignment = Alignment.CenterVertically\n                                    ) {\n                                        MaybeResizeableCell(\n                                            cell,\n                                            onResizeCell = {\n                                                tableState.onCellSizeChanged(cell, it)\n                                            }\n                                        ) {\n                                            MaybeSortableCell(\n                                                cell,\n                                                sortedBy,\n                                                {\n                                                    @Suppress(\"UNCHECKED_CAST\")\n                                                    tableState.setSortBy(Sort(cell as SortableCell<T>, it))\n                                                }\n                                            ) {\n                                                Box(Modifier.weight(1f)) {\n                                                    renderHeaderCell(cell)\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        val sortedList = remember(list, sortedBy) {\n                            tableState.sortedList(list, sortedBy)\n                        }\n                        LazyColumn(\n                            Modifier\n                                .fillMaxHeight(),\n                            state = listState,\n                        ) {\n                            itemsIndexed(\n                                sortedList,\n                                key = if (key != null) { _, item -> key(item) } else null\n                            ) { index, item ->\n                                scope.wrapItem(index, item) {\n                                    Row(\n                                        verticalAlignment = Alignment.CenterVertically\n                                    ) {\n                                        cells.forEach { cell ->\n                                            Box(\n                                                Modifier.width(customWidths[cell] ?: cell.size.defaultWidth)\n                                                    .padding(LocalCellPadding.current)\n                                            ) {\n                                                scope.renderCell(cell, item)\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n//        Spacer(Modifier.size(4.dp))\n                    }\n                    if (list.isEmpty()) {\n                        Box(Modifier.padding().fillMaxSize()) {\n                            drawOnEmpty()\n                        }\n                    }\n                }\n            }\n        },\n        horizontalAdapter = rememberScrollbarAdapter(horizontalScrollState),\n        verticalAdapter = rememberScrollbarAdapter(listState),\n    )\n\n}\n\n\n@Composable\nprivate fun TwoDimensionScrollbar(\n    modifier: Modifier,\n    content: @Composable () -> Unit,\n    verticalAdapter: ScrollbarAdapter,\n    horizontalAdapter: ScrollbarAdapter\n) {\n    Row(modifier) {\n        Column(Modifier.weight(1f)) {\n            Box(Modifier.weight(1f)) {\n                content()\n            }\n            if (horizontalAdapter.needScroll()) {\n                MultiplatformHorizontalScrollbar(\n                    horizontalAdapter,\n                    Modifier.padding(\n                        top = 4.dp,\n                        bottom = 4.dp,\n                    )\n                )\n            }\n        }\n        if (verticalAdapter.needScroll()) {\n            MultiplatformVerticalScrollbar(\n                verticalAdapter,\n                Modifier.padding(\n                    start = 4.dp,\n                    end = 4.dp,\n                    bottom = 4.dp\n                )\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun <T, C : TableCell<T>> ShowColumnConfigMenu(\n    onDismissRequest: () -> Unit,\n    tableState: TableState<T, C>,\n) {\n    Popup(\n        popupPositionProvider = rememberCursorPositionProvider(\n            alignment = Alignment.BottomEnd\n        ),\n        onDismissRequest = onDismissRequest\n    ) {\n        val visibleItems by tableState.visibleCells.collectAsState()\n        val forceVisibleItems = tableState.forceVisibleCells\n        MenuColumn {\n            Row(\n                Modifier.padding(8.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                MyIcon(MyIcons.settings, null, Modifier.size(12.dp))\n                Spacer(Modifier.size(8.dp))\n                Text(\n                    myStringResource(Res.string.customize_columns),\n                    fontSize = myTextSizes.base\n                )\n            }\n            Spacer(Modifier.fillMaxWidth().height(1.dp).background(myColors.onSurface / 5))\n            val orderedCells by tableState.order.collectAsState()\n            ReorderableColumn(\n                list = orderedCells,\n                onSettle = { from, to ->\n                    tableState.setOrder {\n                        it.shifted(from, to - from)\n                    }\n                },\n                content = { _, cell, _ ->\n                    key(cell) {\n                        ReorderableItem {\n                            CellConfigItem(\n                                modifier = Modifier.fillMaxWidth(),\n                                cell = cell,\n                                isVisible = cell in visibleItems,\n                                isForceVisible = cell in forceVisibleItems,\n                                setVisible = { checked ->\n                                    tableState.setVisibleCells {\n                                        val contains = it.contains(cell)\n                                        if (checked) {\n                                            it.ifThen(!contains) { plus(cell) }\n                                        } else {\n                                            it.ifThen(contains) { minus(cell) }\n                                        }\n                                    }\n                                },\n                                setSort = { sort ->\n                                    tableState.setSortBy(sort)\n                                },\n                                sortBy = tableState.sortBy.collectAsState().value\n                            )\n                        }\n                    }\n                },\n            )\n            Spacer(Modifier.fillMaxWidth().height(1.dp).background(myColors.onSurface / 5))\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier\n                    .clickable(onClick = tableState::reset)\n                    .fillMaxWidth()\n                    .padding(8.dp)\n            ) {\n                MyIcon(MyIcons.undo, null, Modifier.size(12.dp))\n                Spacer(Modifier.size(8.dp))\n                Text(myStringResource(Res.string.reset))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun <T, Cell : TableCell<T>> ReorderableListItemScope.CellConfigItem(\n    modifier: Modifier,\n    cell: Cell,\n    isVisible: Boolean,\n    isForceVisible: Boolean,\n    setVisible: (Boolean) -> Unit,\n    sortBy: Sort<SortableCell<T>>?,\n    setSort: (Sort<SortableCell<T>>?) -> Unit,\n) {\n    Row(\n        modifier\n            .padding(8.dp)\n            .height(IntrinsicSize.Max),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        MyIcon(\n            icon = MyIcons.grip,\n            contentDescription = null,\n            modifier = Modifier\n                .clip(myShapes.defaultRounded)\n                .clickable {}\n                .draggableHandle()\n                .padding(4.dp)\n                .size(12.dp),\n        )\n        Spacer(Modifier.width(8.dp))\n        CheckBox(\n            value = isVisible,\n            enabled = !isForceVisible,\n            onValueChange = {\n                setVisible(it)\n            },\n            size = 12.dp\n        )\n        Spacer(Modifier.width(8.dp))\n        Text(\n            cell.name.rememberString(),\n            Modifier\n                .weight(1f)\n                .ifThen(!isVisible || isForceVisible) {\n                    alpha(0.5f)\n                },\n        )\n        Spacer(Modifier.width(8.dp))\n        if (cell is SortableCell<*>) {\n            SortIndicator(\n                Modifier.fillMaxHeight()\n                    .clickable {\n                        @Suppress(\"UNCHECKED_CAST\")\n                        setSort(\n                            sortBy?.takeIf { it.cell == cell }?.reverse() ?: Sort(\n                                cell as SortableCell<T>,\n                                Sort.DEFAULT_IS_DESCENDING\n                            )\n                        )\n                    }.padding(horizontal = 2.dp)\n                    .wrapContentHeight(),\n                sortBy\n                    ?.takeIf { it.cell == cell }\n                    ?.toSortIndicatorMode()\n                    ?: SortIndicatorMode.None\n            )\n        }\n    }\n}\n\ninterface TableScope {\n    companion object : TableScope\n\n    @Composable\n    fun getTableSize() = LocalTableSize.current\n\n    @Composable\n    fun getIsResizeCellOnResizeTable() = LocalResizeCellsOnResizeTable.current\n\n    @Composable\n    fun lastWidths(key: Any): Pair<Dp, Dp> {\n        val tableSize by rememberUpdatedState(getTableSize())\n        var result by remember(key) {\n            mutableStateOf(tableSize.visibleWidth to tableSize.visibleWidth)\n        }\n        LaunchedEffect(key) {\n            snapshotFlow { tableSize.visibleWidth }\n                .saved(2)\n                .onEach {\n                    when (it.size) {\n                        0 -> null\n                        1 -> {\n                            result.copy(second = it.first())\n                        }\n\n                        else -> {\n                            it[0] to it[1]\n                        }\n                    }?.let {\n                        result = it\n                    }\n                }\n                .launchIn(this)\n        }\n        return result\n    }\n\n    @Composable\n    fun deltaWidthFraction(key: Any): Float {\n        return lastWidths(key).run { second / first }\n    }\n\n    @Composable\n    fun deltaWidth(key: Any): Dp {\n        return lastWidths(key).run { second - first }\n    }\n}\n\n@Composable\nfun SortIndicator(\n    modifier: Modifier = Modifier,\n    mode: SortIndicatorMode,\n) {\n    val size = 6.dp\n    Column(modifier) {\n//        val currentAlpha = LocalContentAlpha.current\n        val color = LocalContentColor.current\n        val passiveAlpha = color / 0.25f\n        val activeAlpha = color / 0.75f\n//        val activeAlpha=(currentAlpha + 0.5f).coerceAtMost(1f)\n        MyIcon(\n            MyIcons.sortUp,\n            null,\n            Modifier\n                .size(size),\n            tint = if (mode.isAscending()) {\n                activeAlpha\n            } else {\n                passiveAlpha\n            }\n        )\n        MyIcon(\n            MyIcons.sortDown,\n            null,\n            Modifier\n                .size(size),\n            tint = if (mode.isDescending()) {\n                activeAlpha\n            } else {\n                passiveAlpha\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/table/customtable/TableUtils.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.table.customtable\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.ui.widget.resizeHandle\nimport com.abdownloadmanager.shared.util.div\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.hoverable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsDraggedAsState\nimport androidx.compose.foundation.interaction.collectIsHoveredAsState\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.onClick\nimport com.abdownloadmanager.shared.ui.widget.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerIcon\nimport androidx.compose.ui.input.pointer.pointerHoverIcon\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.ui.widget.sort.ComparatorProvider\nimport com.abdownloadmanager.shared.ui.widget.sort.Sort\nimport com.abdownloadmanager.shared.ui.widget.sort.SortIndicatorMode\nimport com.abdownloadmanager.shared.ui.widget.sort.sorted\nimport com.abdownloadmanager.shared.ui.widget.sort.toSortIndicatorMode\nimport ir.amirab.util.compose.StringSource\nimport ir.amirab.util.flow.mapStateFlow\nimport ir.amirab.util.swapped\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.merge\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.Serializable\nimport java.awt.Cursor\n\n@Stable\ndata class TableSize(\n    val visibleHeight: Dp,\n    val visibleWidth: Dp,\n)\n\n@Immutable\nsealed interface CellSize {\n    val defaultWidth: Dp\n\n    data class Resizeable(\n        val range: ClosedRange<Dp>,\n        override val defaultWidth: Dp = range.start,\n    ) : CellSize\n\n    data class Fixed(\n        override val defaultWidth: Dp\n    ) : CellSize\n}\n\n@Stable\ninterface TableCell<Item> {\n    val id: String\n    val name: StringSource\n    val size: CellSize\n}\n\ninterface CustomCellRenderer {\n    @Composable\n    fun drawHeader()\n}\n\ninterface SortableCell<Item> : TableCell<Item>, ComparatorProvider<Item> {\n    override fun comparator(): Comparator<Item>\n}\n\n\n@Composable\nfun DefaultRenderHeader(cell: TableCell<*>) {\n    if (cell is CustomCellRenderer) {\n        cell.drawHeader()\n    } else {\n        Text(\n            cell.name.rememberString(),\n            Modifier.fillMaxWidth(),\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\n@Composable\nfun RowScope.MaybeResizeableCell(\n    cell: TableCell<*>,\n    onResizeCell: ((Dp) -> Dp) -> Unit,\n    content: @Composable () -> Unit,\n) {\n    when (cell.size) {\n        is CellSize.Fixed -> {\n            content()\n        }\n\n        is CellSize.Resizeable -> {\n            Row(\n                Modifier.weight(1f),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                val mInteractionSource = remember {\n                    MutableInteractionSource()\n                }\n                content()\n                CellResizeHandle(\n                    Modifier.width(12.dp)\n                        .fillMaxHeight(),\n                    orientation = Orientation.Horizontal,\n                    mInteractionSource,\n                    color = myColors.onBackground / 50,\n                    inactiveColor = myColors.onBackground / 10,\n                ) { delta ->\n                    onResizeCell {\n                        it + delta\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun CellResizeHandle(\n    modifier: Modifier,\n    orientation: Orientation = Orientation.Horizontal,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    color: Color = myColors.surface,\n    inactiveColor: Color = myColors.surface / 50,\n    onDrag: (Dp) -> Unit,\n) {\n    val isHovered by interactionSource.collectIsHoveredAsState()\n    val isDragging by interactionSource.collectIsDraggedAsState()\n\n    val hoverIcon = remember(orientation) {\n        PointerIcon(\n            Cursor(\n                when (orientation) {\n                    Orientation.Vertical -> Cursor.S_RESIZE_CURSOR\n                    Orientation.Horizontal -> Cursor.E_RESIZE_CURSOR\n                }\n            )\n        )\n    }\n    val background = animateColorAsState(\n        if (isHovered || isDragging) color\n        else inactiveColor\n    ).value\n    Box(\n        modifier\n            .pointerHoverIcon(hoverIcon, true)\n            .hoverable(interactionSource)\n            .resizeHandle(\n                orientation = orientation,\n                interactionSource = interactionSource,\n                onDrag = onDrag,\n            )\n    ) {\n        Row(\n            Modifier\n                .fillMaxSize().wrapContentSize()\n        ) {\n            val m = Modifier\n                .fillMaxHeight()\n                .width(1.dp)\n                .background(background)\n            Spacer(m)\n        }\n    }\n}\n\n@Composable\nfun <T> RowScope.MaybeSortableCell(\n    cell: TableCell<T>,\n    sortedBy: Sort<SortableCell<T>>?,\n    setSort: (isAscending: Boolean) -> Unit,\n    content: @Composable () -> Unit,\n) {\n    if (cell is SortableCell) {\n        val iHaveSorted = sortedBy.takeIf {\n            it?.cell == cell\n        }\n        Row(\n            Modifier\n                .weight(1f)\n                .onClick {\n                    setSort(iHaveSorted?.reverse()?.isDescending() ?: Sort.DEFAULT_IS_DESCENDING)\n                },\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            SortIndicator(\n                Modifier,\n                iHaveSorted?.toSortIndicatorMode()\n                    ?: SortIndicatorMode.None\n            )\n            Spacer(Modifier.width(2.dp))\n            content()\n        }\n    } else {\n        content()\n    }\n}\n\n@Stable\nclass TableState<Item, Cell : TableCell<Item>>(\n    val cells: List<Cell>,\n    val forceVisibleCells: List<Cell> = emptyList(),\n    val initialCustomSizes: Map<Cell, Dp> = emptyMap(),\n    val initialSortBy: Sort<SortableCell<Item>>? = null,\n    val initialOrder: List<Cell> = cells,\n    val initialVisibleItems: List<Cell> = cells,\n) {\n    private val _customSizes = MutableStateFlow<Map<Cell, Dp>>(initialCustomSizes)\n    val customSizes = _customSizes.asStateFlow()\n\n    fun setCustomSizes(sizes: Map<Cell, Dp>) {\n        setCustomSizes { sizes }\n    }\n\n    fun setCustomSizes(sizes: (Map<Cell, Dp>) -> Map<Cell, Dp>) {\n        _customSizes.update {\n            sizes(it)\n        }\n    }\n\n    fun onCellSizeChanged(cell: Cell, change: (Dp) -> Dp) {\n        val customSizes = _customSizes.value\n        val size = cell.size as? CellSize.Resizeable ?: run {\n            error(\"can't resize this column because it have a FixedSize\")\n        }\n        val dp = customSizes[cell]\n        val x = change((dp ?: size.defaultWidth)).coerceIn(size.range)\n        if (x == cell.size.defaultWidth) {\n            setCustomSizes {\n                it.minus(cell)\n            }\n        } else {\n            setCustomSizes {\n                customSizes.plus(cell to x)\n            }\n        }\n    }\n\n    private val _sortBy = MutableStateFlow<Sort<SortableCell<Item>>?>(initialSortBy)\n    val sortBy = _sortBy.asStateFlow()\n    fun setSortBy(cell: Sort<SortableCell<Item>>?) {\n        this._sortBy.update { cell }\n    }\n\n    private val _order = MutableStateFlow(initialOrder)\n    val order = _order\n        .asStateFlow()\n        .mapStateFlow {\n            val remainingCells = cells.subtract(it.toSet())\n            it.plus(remainingCells)\n        }\n\n    fun setOrder(updater: (List<Cell>) -> List<Cell>) {\n        _order.update {\n            updater(it)\n        }\n    }\n\n    fun setOrder(cell: Cell, delta: Int) {\n        setOrder {\n            val index = it.indexOf(cell)\n            val newIndex = (index + delta)\n            val shouldMove = newIndex in it.indices\n            if (shouldMove) {\n                it.swapped(index, newIndex)\n            } else it\n        }\n    }\n\n    fun setOrder(order: List<Cell>) {\n        setOrder { order }\n    }\n\n    private val _visibleCells = MutableStateFlow<List<Cell>>(initialVisibleItems)\n    val visibleCells = _visibleCells.asStateFlow()\n        .mapStateFlow {\n            it.plus(forceVisibleCells.subtract(it.toSet()))\n        }\n\n    fun setVisibleCells(cells: (List<Cell>) -> List<Cell>) {\n        _visibleCells.update {\n            cells(it).distinct().toMutableList()\n        }\n    }\n\n    fun setVisibleCells(cells: List<Cell>) {\n        setVisibleCells { cells }\n    }\n\n    fun reset() {\n        setCustomSizes(initialCustomSizes)\n        setVisibleCells(initialVisibleItems)\n        setOrder(initialOrder)\n        setSortBy(initialSortBy)\n    }\n\n    val onPropChange = merge(\n        order,\n        customSizes,\n        visibleCells,\n        sortBy,\n    )\n\n    fun save(): SerializableTableState {\n        val sizes = customSizes.value.mapKeys {\n            it.key.id\n        }.mapValues {\n            it.value.value\n        }\n        val sortBy = sortBy.value\n        return SerializableTableState(\n            sizes = sizes,\n            sortBy = sortBy?.let {\n                SortBy(name = sortBy.cell.id, descending = sortBy.isDescending())\n            },\n            order = order.value.map { it.id },\n            visibleCells = visibleCells.value.map { it.id }\n        )\n    }\n\n    fun load(s: SerializableTableState) {\n        setCustomSizes {\n            val cellsThatHaveCustomWidth = findCellById(s.sizes.keys)\n            cellsThatHaveCustomWidth.associateWith { s.sizes[it.id]!!.dp }\n        }\n        setOrder(findCellById(s.order))\n        setSortBy(\n            s.sortBy?.let { sortBy ->\n                findCellById(sortBy.name)?.let {\n                    Sort(it as SortableCell<Item>, sortBy.descending)\n                }\n            }\n        )\n        setVisibleCells(findCellById(s.visibleCells))\n    }\n\n\n    private fun findCellById(name: String): Cell? {\n        return cells.find { it.id == name }\n    }\n\n    private fun findCellById(list: Iterable<String>): List<Cell> {\n        return list.mapNotNull { name ->\n            findCellById(name)\n        }\n    }\n\n    fun sortedList(list: List<Item>, sortBy: Sort<SortableCell<Item>>? = this.sortBy.value): List<Item> {\n        return sortBy?.sorted(list) ?: list\n    }\n\n    /**\n     * get range of items based on the current sort of table\n     */\n    fun <ID> getARangeOfItems(\n        list: List<Item>,\n        id: (Item) -> ID,\n        fromItem: ID,\n        toItem: ID,\n    ): List<ID> {\n        return sortedList(list).map(id).dropWhile {\n            it != fromItem && it != toItem\n        }.dropLastWhile {\n            it != fromItem && it != toItem\n        }\n    }\n\n    fun getItemPosition(\n        list: List<Item>,\n        selector: (Item) -> Boolean,\n    ): Int {\n        return sortedList(list)\n            .indexOfFirst(selector)\n    }\n\n    @Serializable\n    data class SerializableTableState(\n        val sizes: Map<String, Float> = emptyMap(),\n        val sortBy: SortBy? = null,\n        val visibleCells: List<String> = emptyList(),\n        val order: List<String> = emptyList(),\n    )\n\n    @Serializable\n    data class SortBy(\n        val name: String,\n        val descending: Boolean,\n    )\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/ui/widget/table/customtable/styled/MyStyledHeader.kt",
    "content": "package com.abdownloadmanager.shared.ui.widget.table.customtable.styled\n\nimport com.abdownloadmanager.shared.util.ui.myColors\nimport com.abdownloadmanager.shared.ui.widget.table.customtable.TableScope\nimport com.abdownloadmanager.shared.util.ui.WithContentAlpha\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.abdownloadmanager.shared.util.ui.theme.myShapes\n\n@Composable\nfun TableScope.MyStyledTableHeader(\n    itemHorizontalPadding: Dp,\n    content:@Composable ()->Unit,\n) {\n    val shape = myShapes.defaultRounded\n    WithContentAlpha(0.75f) {\n        Box(Modifier\n            .widthIn(getTableSize().visibleWidth)\n            .padding(bottom = 1.dp)\n            .shadow(\n                elevation = 1.dp,\n                shape = shape,\n            )\n            .padding(bottom = 1.dp)\n            .clip(shape)\n            .background(myColors.surface)\n            .padding(vertical = 8.dp, horizontal = itemHorizontalPadding)\n        ) {\n            content()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/ClipboardUtil.desktop.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport org.jetbrains.skiko.ClipboardManager\n\nactual object ClipboardUtil {\n    private val clipboardManager = ClipboardManager()\n\n    actual fun copy(text: String) {\n        runCatching {\n            clipboardManager.setText(text.toString())\n        }\n    }\n\n    actual fun read(): String? {\n        return runCatching {\n            clipboardManager.getText()\n        }.getOrNull()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/DesktopDiskStat.desktop.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport ir.amirab.downloader.utils.IDiskStat\nimport java.io.File\n\nactual typealias PlatformDiskStat = DesktopDiskStat\n\nclass DesktopDiskStat : IDiskStat {\n    override fun getRemainingSpace(path: File): Long {\n        return path.freeSpace\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/PlatformThemeDetector.desktop.kt",
    "content": "package com.abdownloadmanager.shared.util\n\nimport com.abdownloadmanager.shared.util.ui.theme.ISystemThemeDetector\nimport com.jthemedetecor.OsThemeDetector\nimport kotlinx.coroutines.channels.awaitClose\nimport kotlinx.coroutines.flow.callbackFlow\nimport kotlinx.coroutines.flow.emitAll\nimport kotlinx.coroutines.flow.flow\n\nactual typealias PlatformThemeDetector = DesktopSystemThemeDetector\n\nclass DesktopSystemThemeDetector : ISystemThemeDetector {\n    override val isSupported by lazy {\n        runCatching {\n            OsThemeDetector.isSupported()\n        }.getOrElse { false }\n    }\n    private val detector by lazy { OsThemeDetector.getDetector() }\n\n    private val isSystemDarkFlowByLibrary = callbackFlow<Boolean> {\n        val listener: (Boolean) -> Unit = { isDark: Boolean ->\n            trySend(isDark)\n        }\n        detector.registerListener(listener)\n        awaitClose {\n            detector.removeListener(listener)\n        }\n    }\n\n    override fun isDark() = detector.isDark\n    override val systemThemeFlow = flow {\n        if (!isSupported) {\n            return@flow\n        }\n        emit(detector.isDark)\n        emitAll(isSystemDarkFlowByLibrary)\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/DesktopDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport com.abdownloadmanager.shared.util.SystemDownloadLocationProvider\nimport java.io.File\n\nabstract class DesktopDownloadLocationProvider() : SystemDownloadLocationProvider() {\n    override fun getCommonDownloadLocation(): File {\n        return File(System.getProperty(\"user.home\"), \"Downloads\")\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/LinuxDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport java.io.File\nimport java.io.Reader\nimport java.util.Properties\n\nclass LinuxDownloadLocationProvider : DesktopDownloadLocationProvider() {\n    override fun getCurrentDownloadLocation(): File? {\n        val properties = getConfigFileReader()?.use {\n            Properties().apply {\n                load(it)\n            }\n        }\n        return properties\n            ?.getWithResolvedVariables(\"XDG_DOWNLOAD_DIR\")\n            ?.let(::File)\n            ?.canonicalFile\n    }\n\n    private fun getUserDirsFile(): File {\n        val xdgConfigFromEnv: File? = System.getenv(\"XDG_CONFIG_HOME\")\n            ?.takeIf { it.isNotEmpty() }\n            ?.let(::File)\n        val configDir = (xdgConfigFromEnv ?: File(System.getProperty(\"user.home\"), \".config\"))\n        return File(configDir, \"user-dirs.dirs\")\n    }\n\n    private fun getConfigFileReader(): Reader? {\n        return getUserDirsFile()\n            .takeIf { it.exists() }\n            ?.bufferedReader(Charsets.UTF_8)\n    }\n\n    private fun Properties.getWithResolvedVariables(key: String): String? {\n        return runCatching {\n            getProperty(key)\n                ?.hydrateEnvVariables()\n                ?.trim('\"')\n                ?.trim('\\'')\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    private val variableRegex = \"\\\\$\\\\{?([A-Za-z0-9_]+)\\\\}?\".toRegex()\n    private fun String.hydrateEnvVariables(): String {\n        return variableRegex.replace(this) {\n            val variableName = it.groupValues[1]\n            System.getenv(variableName)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/MacDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport java.io.File\n\nclass MacDownloadLocationProvider : DesktopDownloadLocationProvider() {\n    override fun getCurrentDownloadLocation(): File? {\n        return getCommonDownloadLocation()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/PlatformDownloadLocationProvider.desktop.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport com.abdownloadmanager.shared.util.SystemDownloadLocationProvider\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.asDesktop\n\nactual fun getPlatformDownloadLocationProvider(): SystemDownloadLocationProvider {\n    return when (Platform.asDesktop()) {\n        Platform.Desktop.Windows -> WindowsDownloadLocationProvider()\n        Platform.Desktop.Linux -> LinuxDownloadLocationProvider()\n        Platform.Desktop.MacOS -> MacDownloadLocationProvider()\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/downloadlocation/WindowsDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager.shared.util.downloadlocation\n\nimport com.sun.jna.platform.win32.KnownFolders\nimport com.sun.jna.platform.win32.Shell32\nimport com.sun.jna.platform.win32.WinNT\nimport com.sun.jna.ptr.PointerByReference\nimport java.io.File\n\nclass WindowsDownloadLocationProvider : DesktopDownloadLocationProvider() {\n    override fun getCurrentDownloadLocation(): File? {\n        val pathRef = PointerByReference()\n        val hr = Shell32.INSTANCE.SHGetKnownFolderPath(KnownFolders.FOLDERID_Downloads, 0, WinNT.HANDLE(), pathRef)\n        if (hr.toInt() != 0) {\n            throw RuntimeException(\"Failed to get Downloads folder (HRESULT=${hr.toInt()})\")\n        }\n        val downloadsPath = pathRef.value.getWideString(0)\n        return File(downloadsPath).canonicalFile\n    }\n}\n"
  },
  {
    "path": "shared/app/src/desktopMain/kotlin/com/abdownloadmanager/shared/util/ui/widget/MPBackHandler.desktop.kt",
    "content": "package com.abdownloadmanager.shared.util.ui.widget\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nactual fun MPBackHandler(isEnabled: Boolean, onBack: () -> Unit) {\n    // improvements: hook to [escape] button using onKeyEvent and trigger on back here\n}\n"
  },
  {
    "path": "shared/auto-start/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain.dependencies {\n            implementation(project(\":shared:utils\"))\n        }\n        val desktopMain by getting\n        desktopMain.dependencies {\n            //    // for windows, we use registry\n            implementation(libs.jna.platform)\n        }\n    }\n}\n\nandroid {\n    compileSdk = 36\n    namespace = \"ir.amirab.util.startup\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/androidMain/kotlin/ir/amirab/util/startup/AndroidStartupManager.kt",
    "content": "package ir.amirab.util.startup\n\nimport android.content.BroadcastReceiver\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.pm.PackageManager\nclass AndroidStartupManager(\n    private val context: Context,\n    private val receiverClass: Class<out BroadcastReceiver>,\n) : AbstractStartupManager() {\n    override fun install() {\n        context.packageManager.setComponentEnabledSetting(\n            ComponentName(context, receiverClass),\n            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,\n            PackageManager.DONT_KILL_APP\n        )\n    }\n\n    override fun uninstall() {\n        context.packageManager.setComponentEnabledSetting(\n            ComponentName(context, receiverClass),\n            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,\n            PackageManager.DONT_KILL_APP\n        )\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/androidMain/kotlin/ir/amirab/util/startup/Startup.android.kt",
    "content": "package ir.amirab.util.startup\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\n\nactual object Startup {\n    fun getStartUpManager(\n        context: Context,\n        bootReceiver: Class<out BroadcastReceiver>,\n    ): AndroidStartupManager {\n        return AndroidStartupManager(context, bootReceiver)\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/commonMain/kotlin/ir/amirab/util/startup/AbstractStartupManager.kt",
    "content": "package ir.amirab.util.startup\n\nabstract class AbstractStartupManager {\n    @Throws(Exception::class)\n    abstract fun install()\n    abstract fun uninstall()\n}\n"
  },
  {
    "path": "shared/auto-start/src/commonMain/kotlin/ir/amirab/util/startup/Startup.kt",
    "content": "package ir.amirab.util.startup\n\nexpect object Startup\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/AbstractDesktopStartupManager.kt",
    "content": "package ir.amirab.util.startup\n\nabstract class AbstractDesktopStartupManager(\n    val name: String,\n    val path: String,\n    val args: List<String>,\n) : AbstractStartupManager() {\n    protected fun getExecutableWithArgs(): String {\n        return buildList {\n            add(path.quoted())\n            addAll(args)\n        }.joinToString(\" \")\n    }\n\n\n    private fun String.quoted(): String {\n        return \"\\\"$this\\\"\"\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/HeadlessStartupDesktop.kt",
    "content": "package ir.amirab.util.startup\n\nclass HeadlessStartupDesktop(\n    name: String,\n    path: String,\n    args: List<String>,\n) : AbstractDesktopStartupManager(\n    name = name,\n    path = path,\n    args = args,\n) {\n    @Throws(Exception::class)\n    override fun install() {\n    }\n\n    override fun uninstall() {\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/MacOSStartupDesktop.kt",
    "content": "package ir.amirab.util.startup\n\nimport java.io.File\n\nclass MacOSStartupDesktop(\n    name: String,\n    path: String,\n    args: List<String>,\n) : AbstractDesktopStartupManager(\n    path = path,\n    name = name,\n    args = args,\n) {\n    private fun getFile(): File {\n        if (!launchAgentsDir.exists()) {\n            launchAgentsDir.mkdirs()\n        }\n\n        return File(launchAgentsDir, super.name + \".plist\")\n    }\n\n    @Throws(Exception::class)\n    override fun install() {\n        val file = getFile()\n\n        val plistContent = buildString {\n            appendLine(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\")\n            appendLine(\"<plist version=\\\"1.0\\\">\")\n            appendLine(\"<dict>\")\n            appendLine(\"\\t<key>Label</key>\")\n            appendLine(\"\\t<string>${super.name}.startup</string>\")\n            appendLine(\"\\t<key>ProgramArguments</key>\")\n            appendLine(\"\\t<array>\")\n            appendLine(\"\\t\\t<string>/usr/bin/open</string>\")\n            appendLine(\"\\t\\t<string>-a</string>\")\n            appendLine(\"\\t\\t<string>${super.path}</string>\")\n            appendLine(\"\\t\\t<string>--args</string>\")\n            args.forEach {\n                appendLine(\"\\t\\t<string>$it</string>\")\n            }\n            appendLine(\"\\t</array>\")\n            appendLine(\"\\t<key>RunAtLoad</key>\")\n            appendLine(\"\\t<true/>\")\n            appendLine(\"\\t<key>LimitLoadToSessionType</key>\")\n            appendLine(\"\\t<string>Aqua</string>\")\n            appendLine(\"</dict>\")\n            appendLine(\"</plist>\")\n        }\n\n        file.bufferedWriter().use { it.write(plistContent) }\n\n        runLaunchctlCommand(LOAD_COMMAND, file.path)\n    }\n\n    override fun uninstall() {\n        val file = getFile()\n        if (file.exists()) {\n            runLaunchctlCommand(UNLOAD_COMMAND, file.path)\n            file.delete()\n        }\n    }\n\n\n    private fun runLaunchctlCommand(command: String, filePath: String) {\n        ProcessBuilder(\"launchctl\", command, filePath)\n            .inheritIO()\n            .start()\n            .waitFor()\n    }\n\n    companion object {\n        @get:Throws(Exception::class)\n        val launchAgentsDir: File\n            get() {\n                var home = System.getProperty(\"user.home\")\n\n                if (Utils.isRoot) {\n                    home = \"\"\n                }\n\n                return File(\"$home/Library/LaunchAgents/\")\n            }\n\n        private const val UNLOAD_COMMAND = \"unload\"\n        private const val LOAD_COMMAND = \"load\"\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/Startup.kt",
    "content": "package ir.amirab.util.startup\n\nimport ir.amirab.util.platform.Platform\n\nactual object Startup {\n    /**\n     * Add file to startup\n     * @param name Name of key/file\n     * @param path Path to file\n     * @throws Exception\n     */\n    @Throws(Exception::class)\n    fun getStartUpManagerForDesktop(\n        name: String,\n        path: String?,\n        args: List<String>,\n        packageName: String,\n    ): AbstractDesktopStartupManager {\n        if (path==null){\n            //there is no installation path provided so we use no-op\n            return noImplStartUpManager()\n        }\n        val os = Platform.getCurrentPlatform()\n        val startup=when (os) {\n            Platform.Desktop.Linux -> {\n                if (Utils.isHeadless) {\n                    HeadlessStartupDesktop(name, path, args)\n                } else {\n                    UnixXDGStartupDesktop(name, path, args, packageName)\n                }\n            }\n\n            Platform.Desktop.MacOS -> MacOSStartupDesktop(name, path, args)\n            Platform.Desktop.Windows -> WindowsStartupDesktop(name, path, args)\n            Platform.Android -> error(\"this code should not be called in android\")\n        }\n        return startup\n    }\n\n    private fun noImplStartUpManager(): HeadlessStartupDesktop {\n        return HeadlessStartupDesktop(\"\", \"\", emptyList())\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/UnixXDGStartupDesktop.kt",
    "content": "package ir.amirab.util.startup\n\nimport java.io.File\n\nclass UnixXDGStartupDesktop(\n    name: String,\n    path: String,\n    args: List<String>,\n    val desktopEntryFileName: String,\n) : AbstractDesktopStartupManager(\n    name = name,\n    path = path,\n    args = args,\n) {\n\n    private fun getIconFilePath(): String? {\n        return runCatching {\n            val file = File(path)\n            val name = file.name\n            return file\n                .parentFile.parentFile\n                .resolve(\"lib/$name.png\")\n                .takeIf { it.exists() }?.path\n        }.getOrNull()\n    }\n\n    private fun getAutoStartFile(): File {\n        if (!autostartDir.exists()) {\n            autostartDir.mkdirs()\n        }\n        return File(autostartDir, \"$desktopEntryFileName.desktop\")\n    }\n\n    @Throws(Exception::class)\n    override fun install() {\n        val name = this.name\n        val exec = getExecutableWithArgs()\n        val icon = getIconFilePath()\n        getAutoStartFile().writeText(\n            buildString {\n                appendLine(\"[Desktop Entry]\")\n                appendLine(\"Type=Application\")\n                appendLine(\"Name=$name\")\n                appendLine(\"Exec=$exec\")\n                icon?.let { icon ->\n                    appendLine(\"Icon=$icon\")\n                }\n                appendLine(\"Terminal=false\")\n                appendLine(\"NoDisplay=true\")\n            }\n        )\n    }\n\n    override fun uninstall() {\n        getAutoStartFile().delete()\n    }\n\n    companion object {\n        val autostartDir: File\n            get() {\n                val home = System.getProperty(\"user.home\")\n\n                return File(\"$home/.config/autostart/\")\n            }\n    }\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/Utils.kt",
    "content": "package ir.amirab.util.startup\n\nimport ir.amirab.util.platform.Platform\nimport java.awt.GraphicsEnvironment\nimport java.io.BufferedReader\nimport java.io.File\nimport java.io.InputStreamReader\n\nobject Utils {\n    @get:Throws(Exception::class)\n    val isRoot: Boolean\n        get() = Platform.getCurrentPlatform() !== Platform.Desktop.Windows && BufferedReader(\n            InputStreamReader(Runtime.getRuntime().exec(\"whoami\").inputStream)\n        ).readLine() == \"root\"\n\n    val isHeadless: Boolean\n        get() = GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance()\n}\n"
  },
  {
    "path": "shared/auto-start/src/desktopMain/kotlin/ir/amirab/util/startup/WindowsStartupDesktop.kt",
    "content": "package ir.amirab.util.startup\n\nimport com.sun.jna.platform.win32.Advapi32Util\nimport com.sun.jna.platform.win32.WinReg\n\nclass WindowsStartupDesktop(\n    name: String,\n    path: String,\n    args: List<String>,\n) : AbstractDesktopStartupManager(\n    name = name,\n    path = path,\n    args = args\n) {\n    @Throws(Exception::class)\n    override fun install() {\n        val data = getExecutableWithArgs()\n        Advapi32Util.registrySetStringValue(\n            WinReg.HKEY_CURRENT_USER,\n            \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\",\n            this.name,\n            data\n        )\n    }\n\n    override fun uninstall() {\n        try {\n            Advapi32Util.registryDeleteValue(\n                WinReg.HKEY_CURRENT_USER,\n                \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\",\n                this.name,\n            )\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(MyPlugins.composeBase)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain.dependencies {\n            implementation(libs.compose.runtime)\n            implementation(libs.compose.foundation)\n            implementation(libs.compose.ui)\n            implementation(project(\":shared:utils\"))\n            api(project(\":shared:resources:contracts\"))\n        }\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"ir.amirab.util.compose\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/Helpers.kt",
    "content": "package ir.amirab.util.compose\n\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\n\nfun Dp.dpToPx(density: Density): Float {\n    return with(density) { toPx() }\n}\n\nfun Int.pxToDp(density: Density): Dp {\n    return with(density) { toDp() }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/IIconResolver.kt",
    "content": "package ir.amirab.util.compose\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\ninterface IIconResolver {\n    fun resolve(uri: String): IconSource?\n}\n\nval LocalIconFromUriResolver = staticCompositionLocalOf<IIconResolver> {\n    error(\"LocalIconFromUriResolver not provided\")\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/IconSource.kt",
    "content": "package ir.amirab.util.compose\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\n\n@Immutable\nsealed interface IconSource {\n    val value: Any\n    val requiredTint: Boolean\n    val uri: String?\n\n    @Composable\n    fun rememberPainter(): Painter\n\n    @Immutable\n    data class VectorIconSource(\n        override val value: ImageVector,\n        override val requiredTint: Boolean,\n        override val uri: String? = null,\n    ) : IconSource {\n        @Composable\n        override fun rememberPainter(): Painter = rememberVectorPainter(value)\n    }\n\n    @Immutable\n    data class PainterIconSource(\n        override val value: Painter,\n        override val requiredTint: Boolean,\n        override val uri: String? = null,\n    ) : IconSource {\n        @Composable\n        override fun rememberPainter(): Painter = value\n    }\n\n    companion object\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/StringSource.kt",
    "content": "package ir.amirab.util.compose\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport arrow.core.combine\nimport ir.amirab.util.compose.localizationmanager.LanguageManager\nimport ir.amirab.util.compose.localizationmanager.withReplacedArgs\nimport ir.amirab.util.compose.resources.MyStringResource\nimport ir.amirab.util.compose.resources.myStringResource\n\n@Immutable\nsealed interface StringSource {\n    @Composable\n    fun rememberString(): String\n\n    @Composable\n    fun rememberString(args: Map<String, String>): String\n    fun getString(): String\n    fun getString(args: Map<String, String>): String\n\n    @Immutable\n    data class FromString(\n        val value: String,\n    ) : StringSource {\n        @Composable\n        override fun rememberString(): String {\n            return value\n        }\n\n        @Composable\n        override fun rememberString(args: Map<String, String>): String {\n            return remember(args) {\n                if (args.isEmpty()) {\n                    value\n                } else {\n                    value.withReplacedArgs(args)\n                }\n            }\n        }\n\n        override fun getString(): String {\n            return value\n        }\n\n        override fun getString(args: Map<String, String>): String {\n            return if (args.isEmpty()) {\n                value\n            } else {\n                value.withReplacedArgs(args)\n            }\n        }\n    }\n\n    @Immutable\n    data class FromStringResource(\n        val value: MyStringResource,\n        val extraArgs: Map<String, String> = emptyMap(),\n    ) : StringSource {\n        @Composable\n        override fun rememberString(): String {\n            return myStringResource(value, extraArgs)\n        }\n\n        @Composable\n        override fun rememberString(args: Map<String, String>): String {\n            val argList = remember(extraArgs, args) {\n                extraArgs.plus(args)\n            }\n            return if (argList.isEmpty()) {\n                myStringResource(value)\n            } else {\n                myStringResource(value, argList)\n            }\n        }\n\n        private fun getLanguageManager(): LanguageManager {\n            return LanguageManager.instance\n        }\n\n        override fun getString(): String {\n            return getLanguageManager()\n                .getMessage(value.id)\n                .withReplacedArgs(extraArgs)\n        }\n\n        override fun getString(args: Map<String, String>): String {\n            return getLanguageManager()\n                .getMessage(value.id)\n                .withReplacedArgs(extraArgs.plus(args))\n        }\n    }\n\n    @Immutable\n    data class CombinedStringSource(\n        val values: List<StringSource>,\n        val separator: String,\n    ) : StringSource {\n        @Composable\n        override fun rememberString(): String {\n            return values.map {\n                it.rememberString()\n            }.joinToString(separator)\n        }\n\n        @Composable\n        override fun rememberString(args: Map<String, String>): String {\n            return values.map {\n                it.rememberString(args)\n            }.joinToString(separator)\n        }\n\n        override fun getString(): String {\n            return values.map {\n                it.getString()\n            }.joinToString(separator)\n        }\n\n        override fun getString(args: Map<String, String>): String {\n            return values.map {\n                it.getString(args)\n            }.joinToString(separator)\n        }\n    }\n}\n\nfun MyStringResource.asStringSource(): StringSource {\n    return StringSource.FromStringResource(this)\n}\n\nfun MyStringResource.asStringSourceWithARgs(args: Map<String, String>): StringSource {\n    return StringSource.FromStringResource(this, args)\n}\n\nfun String.asStringSource(): StringSource {\n    return StringSource.FromString(this)\n}\n\nfun List<StringSource>.combineStringSources(separator: String = \"\"): StringSource {\n    return StringSource.CombinedStringSource(this, separator)\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/action/AnAction.kt",
    "content": "package ir.amirab.util.compose.action\n\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\nabstract class AnAction(\n    title: StringSource,\n    icon: IconSource? = null,\n) : MenuItem.SingleItem(\n    title = title,\n    icon = icon,\n) {\n    override fun onClick() = actionPerformed()\n\n    abstract fun actionPerformed()\n}\n\n\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/action/Extensions.kt",
    "content": "package ir.amirab.util.compose.action\n\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.*\n\ninline fun simpleAction(\n    title: StringSource,\n    icon: IconSource? = null,\n    crossinline onActionPerformed: AnAction.() -> Unit,\n): AnAction {\n    return object : AnAction(\n        title = title, icon = icon,\n    ) {\n        override fun actionPerformed() = onActionPerformed()\n    }\n}\n\ninline fun simpleAction(\n    title: StringSource,\n    icon: IconSource? = null,\n    checkEnable: StateFlow<Boolean>,\n    crossinline onActionPerformed: AnAction.() -> Unit,\n): AnAction {\n    return object : AnAction(\n        title = title, icon = icon,\n    ) {\n        override val isEnabled: StateFlow<Boolean> = checkEnable\n        override fun actionPerformed() = onActionPerformed()\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/action/MenuDsl.kt",
    "content": "package ir.amirab.util.compose.action\n\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.compose.StringSource\n\n@DslMarker\nprivate annotation class MenuDsl\n\n@MenuDsl\nclass MenuScope {\n    private val list = mutableListOf<MenuItem>()\n    fun item(\n        title: StringSource,\n        icon: IconSource? = null,\n        onClick: AnAction.() -> Unit,\n    ) {\n        val action = simpleAction(title, icon, onClick)\n        list.add(action)\n    }\n\n    fun subMenu(\n        title: StringSource,\n        icon: IconSource? = null,\n        block: MenuScope.() -> Unit,\n    ) {\n        val subMenu = MenuItem.SubMenu(\n            title = title,\n            icon = icon,\n            items = MenuScope().apply(block).build()\n        )\n        list.add(subMenu)\n    }\n\n    fun separator() {\n        MenuItem.Separator\n            .let(list::add)\n    }\n\n    operator fun MenuItem.unaryPlus() {\n        this.let(list::add)\n    }\n\n    fun build() = list.toList()\n}\n\nfun buildMenu(block: MenuScope.() -> Unit) = MenuScope().apply(block).build()\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/action/MenuItem.kt",
    "content": "package ir.amirab.util.compose.action\n\nimport ir.amirab.util.compose.IconSource\nimport ir.amirab.util.flow.mapStateFlow\nimport androidx.compose.runtime.*\nimport ir.amirab.util.compose.StringSource\nimport kotlinx.coroutines.flow.*\n\nsealed interface MenuItem {\n    @Stable\n    interface ReadableItem {\n        //compose aware property\n        val icon: StateFlow<IconSource?>\n\n        //compose aware property\n        val title: StateFlow<StringSource>\n    }\n\n    interface CanBeModified {\n        fun setIcon(icon: IconSource?)\n        fun setTitle(title: StringSource)\n    }\n\n    interface HasEnable {\n        //compose aware property\n        val isEnabled: StateFlow<Boolean>\n    }\n\n    interface CanChangeEnabled {\n        fun setEnabled(boolean: Boolean)\n    }\n\n    interface ClickableItem : HasEnable {\n        fun onClick()\n    }\n\n    abstract class SingleItem(\n        title: StringSource,\n        icon: IconSource? = null,\n    ) : MenuItem,\n        ClickableItem,\n        ReadableItem,\n        CanBeModified,\n        CanChangeEnabled,\n            () -> Unit {\n        var shouldDismissOnClick: Boolean = true\n\n\n        private val _title: MutableStateFlow<StringSource> = MutableStateFlow(title)\n        private val _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)\n        private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)\n\n        override val title: StateFlow<StringSource> = _title.asStateFlow()\n        override val icon: StateFlow<IconSource?> = _icon.asStateFlow()\n        override val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow()\n\n        override fun setEnabled(boolean: Boolean) {\n            _isEnabled.update { boolean }\n        }\n\n        override fun setIcon(icon: IconSource?) {\n            _icon.update { icon }\n        }\n\n        override fun setTitle(title: StringSource) {\n            _title.update { title }\n        }\n\n        final override fun invoke() {\n            if (isEnabled.value) {\n                onClick()\n            }\n        }\n\n        abstract override fun onClick()\n    }\n\n    class SubMenu(\n        icon: IconSource? = null,\n        title: StringSource,\n        items: List<MenuItem>,\n    ) : MenuItem,\n        ReadableItem,\n        HasEnable {\n        private var _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)\n        private var _title: MutableStateFlow<StringSource> = MutableStateFlow(title)\n        private val _items: MutableStateFlow<List<MenuItem>> = MutableStateFlow(items)\n\n        override var icon: StateFlow<IconSource?> = _icon.asStateFlow()\n        override var title: StateFlow<StringSource> = _title.asStateFlow()\n\n        val items: StateFlow<List<MenuItem>> = _items.asStateFlow()\n        fun setItems(newItems: List<MenuItem>) {\n            _items.update { newItems }\n        }\n\n        override val isEnabled: StateFlow<Boolean> = this.items.mapStateFlow {\n            it.isNotEmpty()\n        }\n    }\n\n    data object Separator : MenuItem\n}\n\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/contants/Constants.kt",
    "content": "package ir.amirab.util.compose.contants\n\n/**\n * URIs used in this app\n */\nconst val RESOURCE_PROTOCOL = \"app-resource\"\nconst val FILE_PROTOCOL = \"file\"\nconst val SYSTEM_PROTOCOL = \"system\"\nconst val ICON_PROTOCOL = \"icon\"\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/layout/RelativeAlignment.kt",
    "content": "package ir.amirab.util.compose.layout\n\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.LayoutDirection\n\nclass RelativeAlignment(\n    val mainAlignment: Alignment,\n    val relative: IntOffset,\n) : Alignment {\n    override fun align(\n        size: IntSize,\n        space: IntSize,\n        layoutDirection: LayoutDirection\n    ): IntOffset {\n        val result = mainAlignment.align(size, space, layoutDirection)\n        val resultWithOffset = result + relative\n        return IntOffset(\n            resultWithOffset.x.coerceIn(0, space.width),\n            resultWithOffset.y.coerceIn(0, space.height),\n        )\n    }\n\n\n    class Horizontal(\n        val mainAlignment: Alignment.Horizontal,\n        val relative: Int,\n    ) : Alignment.Horizontal {\n        override fun align(\n            size: Int,\n            space: Int,\n            layoutDirection: LayoutDirection\n        ): Int {\n            val result = mainAlignment.align(size, space, layoutDirection)\n            val resultWithOffset = result + relative\n            return resultWithOffset.coerceIn(0..space)\n        }\n    }\n\n    class Vertical(\n        val mainAlignment: Alignment.Vertical,\n        val relative: Int,\n    ) : Alignment.Vertical {\n        override fun align(\n            size: Int,\n            space: Int,\n        ): Int {\n            val result = mainAlignment.align(size, space)\n            val resultWithOffset = result + relative\n            return resultWithOffset.coerceIn(0..space)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/ILanguageNameProvider.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport java.util.Locale\n\ninterface ILanguageNameProvider {\n    fun getNativeName(myLocale: MyLocale): String\n    fun getEnglishName(myLocale: MyLocale): String\n    fun getName(myLocale: MyLocale): LanguageName\n}\n\ndata class LanguageName(\n    val nativeName: String,\n    val englishName: String,\n)\n\nobject LanguageNameProvider : ILanguageNameProvider {\n    private val list = mapOf(\n        \"af\" to LanguageName(\"Afrikaans\", \"Afrikaans\"),\n        \"ak\" to LanguageName(\"Akan\", \"Akan\"),\n        \"am\" to LanguageName(\"አማርኛ\", \"Amharic\"),\n        \"ar\" to LanguageName(\"العربية\", \"Arabic\"),\n        \"as\" to LanguageName(\"অসমীয়া\", \"Assamese\"),\n        \"az\" to LanguageName(\"Azərbaycanca\", \"Azerbaijani\"),\n        \"be\" to LanguageName(\"Беларуская\", \"Belarusian\"),\n        \"bg\" to LanguageName(\"Български\", \"Bulgarian\"),\n        \"bm\" to LanguageName(\"Bamanankan\", \"Bambara\"),\n        \"bn\" to LanguageName(\"বাংলা\", \"Bengali\"),\n        \"bo\" to LanguageName(\"བོད་སྐད་\", \"Tibetan\"),\n        \"bqi\" to LanguageName(\"لۊری بختیاری\", \"Luri Bakhtiari\"),\n        \"br\" to LanguageName(\"Brezhoneg\", \"Breton\"),\n        \"bs\" to LanguageName(\"Bosanski\", \"Bosnian\"),\n        \"ca\" to LanguageName(\"Català\", \"Catalan\"),\n        \"ckb\" to LanguageName(\"کوردیی سۆرانی\", \"Kurdish (Sorani)\"),\n        \"cs\" to LanguageName(\"Čeština\", \"Czech\"),\n        \"cy\" to LanguageName(\"Cymraeg\", \"Welsh\"),\n        \"da\" to LanguageName(\"Dansk\", \"Danish\"),\n        \"de\" to LanguageName(\"Deutsch\", \"German\"),\n        \"de_AT\" to LanguageName(\"Österreichisches Deutsch\", \"Austrian German\"),\n        \"de_CH\" to LanguageName(\"Schweizer Hochdeutsch\", \"Swiss German\"),\n        \"dz\" to LanguageName(\"རྫོང་ཁ\", \"Dzongkha\"),\n        \"ee\" to LanguageName(\"Eʋegbe\", \"Ewe\"),\n        \"el\" to LanguageName(\"Ελληνικά\", \"Greek\"),\n        \"en\" to LanguageName(\"English\", \"English\"),\n        \"eo\" to LanguageName(\"Esperanto\", \"Esperanto\"),\n        \"es\" to LanguageName(\"Español\", \"Spanish\"),\n        \"et\" to LanguageName(\"Eesti\", \"Estonian\"),\n        \"eu\" to LanguageName(\"Euskara\", \"Basque\"),\n        \"fa\" to LanguageName(\"فارسی\", \"Persian\"),\n        \"ff\" to LanguageName(\"Pulaar\", \"Fulah\"),\n        \"fi\" to LanguageName(\"Suomi\", \"Finnish\"),\n        \"fo\" to LanguageName(\"Føroyskt\", \"Faroese\"),\n        \"fr\" to LanguageName(\"Français\", \"French\"),\n        \"fr_CA\" to LanguageName(\"Français canadien\", \"Canadian French\"),\n        \"fr_CH\" to LanguageName(\"Français suisse\", \"Swiss French\"),\n        \"fy\" to LanguageName(\"Frysk\", \"Western Frisian\"),\n        \"ga\" to LanguageName(\"Gaeilge\", \"Irish\"),\n        \"gd\" to LanguageName(\"Gàidhlig\", \"Scottish Gaelic\"),\n        \"gl\" to LanguageName(\"Galego\", \"Galician\"),\n        \"gu\" to LanguageName(\"ગુજરાતી\", \"Gujarati\"),\n        \"gv\" to LanguageName(\"Gaelg\", \"Manx\"),\n        \"ha\" to LanguageName(\"Hausa\", \"Hausa\"),\n        \"he\" to LanguageName(\"עברית\", \"Hebrew\"),\n        \"hi\" to LanguageName(\"हिन्दी\", \"Hindi\"),\n        \"hr\" to LanguageName(\"Hrvatski\", \"Croatian\"),\n        \"hu\" to LanguageName(\"Magyar\", \"Hungarian\"),\n        \"hy\" to LanguageName(\"Հայերեն\", \"Armenian\"),\n        \"id\" to LanguageName(\"Bahasa Indonesia\", \"Indonesian\"),\n        \"ig\" to LanguageName(\"Igbo\", \"Igbo\"),\n        \"ii\" to LanguageName(\"ꆈꌠꉙ\", \"Sichuan Yi\"),\n        \"is\" to LanguageName(\"Íslenska\", \"Icelandic\"),\n        \"it\" to LanguageName(\"Italiano\", \"Italian\"),\n        \"ja\" to LanguageName(\"日本語\", \"Japanese\"),\n        \"ka\" to LanguageName(\"ქართული\", \"Georgian\"),\n        \"ki\" to LanguageName(\"Gikuyu\", \"Kikuyu\"),\n        \"kk\" to LanguageName(\"Қазақ тілі\", \"Kazakh\"),\n        \"kl\" to LanguageName(\"Kalaallisut\", \"Greenlandic\"),\n        \"km\" to LanguageName(\"ខ្មែរ\", \"Khmer\"),\n        \"kn\" to LanguageName(\"ಕನ್ನಡ\", \"Kannada\"),\n        \"ko\" to LanguageName(\"한국어\", \"Korean\"),\n        \"ks\" to LanguageName(\"کٲشُر\", \"Kashmiri\"),\n        \"kw\" to LanguageName(\"Kernewek\", \"Cornish\"),\n        \"ky\" to LanguageName(\"Кыргызча\", \"Kyrgyz\"),\n        \"lb\" to LanguageName(\"Lëtzebuergesch\", \"Luxembourgish\"),\n        \"lg\" to LanguageName(\"Luganda\", \"Ganda\"),\n        \"ln\" to LanguageName(\"Lingála\", \"Lingala\"),\n        \"lo\" to LanguageName(\"ລາວ\", \"Lao\"),\n        \"lt\" to LanguageName(\"Lietuvių\", \"Lithuanian\"),\n        \"lu\" to LanguageName(\"Tshiluba\", \"Luba-Katanga\"),\n        \"lv\" to LanguageName(\"Latviešu\", \"Latvian\"),\n        \"mg\" to LanguageName(\"Malagasy\", \"Malagasy\"),\n        \"mk\" to LanguageName(\"Македонски\", \"Macedonian\"),\n        \"ml\" to LanguageName(\"മലയാളം\", \"Malayalam\"),\n        \"mn\" to LanguageName(\"Монгол\", \"Mongolian\"),\n        \"mr\" to LanguageName(\"मराठी\", \"Marathi\"),\n        \"ms\" to LanguageName(\"Bahasa Melayu\", \"Malay\"),\n        \"mt\" to LanguageName(\"Malti\", \"Maltese\"),\n        \"my\" to LanguageName(\"ဗမာ\", \"Burmese\"),\n        \"nb\" to LanguageName(\"Norsk Bokmål\", \"Norwegian Bokmål\"),\n        \"nd\" to LanguageName(\"IsiNdebele\", \"North Ndebele\"),\n        \"ne\" to LanguageName(\"नेपाली\", \"Nepali\"),\n        \"nl\" to LanguageName(\"Nederlands\", \"Dutch\"),\n        \"nl_BE\" to LanguageName(\"Vlaams\", \"Flemish\"),\n        \"nn\" to LanguageName(\"Nynorsk\", \"Norwegian Nynorsk\"),\n        \"no\" to LanguageName(\"Norsk\", \"Norwegian\"),\n        \"om\" to LanguageName(\"Oromoo\", \"Oromo\"),\n        \"or\" to LanguageName(\"ଓଡ଼ିଆ\", \"Odia\"),\n        \"os\" to LanguageName(\"Ирон\", \"Ossetic\"),\n        \"pa\" to LanguageName(\"ਪੰਜਾਬੀ\", \"Punjabi\"),\n        \"pl\" to LanguageName(\"Polski\", \"Polish\"),\n        \"ps\" to LanguageName(\"پښتو\", \"Pashto\"),\n        \"pt\" to LanguageName(\"Português\", \"Portuguese\"),\n        \"pt_BR\" to LanguageName(\"Português do Brasil\", \"Brazilian Portuguese\"),\n        \"qu\" to LanguageName(\"Runasimi\", \"Quechua\"),\n        \"rm\" to LanguageName(\"Rumantsch\", \"Romansh\"),\n        \"rn\" to LanguageName(\"Ikirundi\", \"Rundi\"),\n        \"ro\" to LanguageName(\"Română\", \"Romanian\"),\n        \"ro_MD\" to LanguageName(\"moldovenească\", \"Moldovan\"),\n        \"ru\" to LanguageName(\"Русский\", \"Russian\"),\n        \"rw\" to LanguageName(\"Kinyarwanda\", \"Kinyarwanda\"),\n        \"se\" to LanguageName(\"Davvisámegiella\", \"Northern Sami\"),\n        \"sg\" to LanguageName(\"Sängö\", \"Sango\"),\n        \"sh\" to LanguageName(\"Srpskohrvatski\", \"Serbo-Croatian\"),\n        \"si\" to LanguageName(\"සිංහල\", \"Sinhala\"),\n        \"sk\" to LanguageName(\"Slovenčina\", \"Slovak\"),\n        \"sl\" to LanguageName(\"Slovenščina\", \"Slovene\"),\n        \"sn\" to LanguageName(\"chiShona\", \"Shona\"),\n        \"so\" to LanguageName(\"Soomaali\", \"Somali\"),\n        \"sq\" to LanguageName(\"Shqip\", \"Albanian\"),\n        \"sr\" to LanguageName(\"Српски\", \"Serbian\"),\n        \"sv\" to LanguageName(\"Svenska\", \"Swedish\"),\n        \"sw\" to LanguageName(\"Kiswahili\", \"Swahili\"),\n        \"ta\" to LanguageName(\"தமிழ்\", \"Tamil\"),\n        \"te\" to LanguageName(\"తెలుగు\", \"Telugu\"),\n        \"th\" to LanguageName(\"ไทย\", \"Thai\"),\n        \"ti\" to LanguageName(\"ትግርኛ\", \"Tigrinya\"),\n        \"tl\" to LanguageName(\"Tagalog\", \"Tagalog\"),\n        \"to\" to LanguageName(\"lea fakatonga\", \"Tongan\"),\n        \"tr\" to LanguageName(\"Türkçe\", \"Turkish\"),\n        \"ug\" to LanguageName(\"ئۇيغۇرچە\", \"Uyghur\"),\n        \"uk\" to LanguageName(\"Українська\", \"Ukrainian\"),\n        \"ur\" to LanguageName(\"اردو\", \"Urdu\"),\n        \"uz\" to LanguageName(\"Oʻzbekcha\", \"Uzbek\"),\n        \"vi\" to LanguageName(\"Tiếng Việt\", \"Vietnamese\"),\n        \"yi\" to LanguageName(\"ייִדיש\", \"Yiddish\"),\n        \"yo\" to LanguageName(\"Èdè Yorùbá\", \"Yoruba\"),\n        \"zh\" to LanguageName(\"中文\", \"Chinese\"),\n        \"zh_CN\" to LanguageName(\"简体中文\", \"Simplified Chinese\"),\n        \"zh_TW\" to LanguageName(\"正體中文\", \"Traditional Chinese\"),\n        \"zu\" to LanguageName(\"isiZulu\", \"Zulu\")\n    )\n\n    override fun getNativeName(myLocale: MyLocale): String {\n        return getName(myLocale).nativeName\n    }\n\n    override fun getEnglishName(myLocale: MyLocale): String {\n        return getName(myLocale).englishName\n    }\n\n    override fun getName(myLocale: MyLocale): LanguageName {\n        val languageCode = myLocale.languageCode\n        val countryCode = myLocale.countryCode\n        if (countryCode != null) {\n            list[\"${languageCode}_${countryCode}\"]?.let {\n                return it\n            }\n        }\n        list[languageCode]?.let {\n            return it\n        }\n        return default(myLocale).let { LanguageName(it, it) }\n    }\n\n    private fun default(myLocale: MyLocale): String {\n        return myLocale\n            .toLocale()\n            .let { it.getDisplayName(it) }\n    }\n}\n\nprivate fun MyLocale.toLocale(): Locale {\n    val language = languageCode\n    val country = countryCode\n    return if (country == null) {\n        Locale(language)\n    } else {\n        Locale(language, country)\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/LanguageManager.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.resources.contracts.MyLanguageResource\nimport ir.amirab.util.flow.mapStateFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.runBlocking\nimport java.io.InputStream\nimport java.util.*\n\nclass LanguageManager(\n    storage: LanguageStorage,\n    private val languageSourceProvider: LanguageSourceProvider,\n) {\n    @Suppress(\"PrivatePropertyName\")\n    private val DefaultLanguageInfo = languageSourceProvider\n        .defaultLanguageResource\n        .let {\n            MyLocale\n                .fromLanguageResource(it)\n                .toLanguageInfo(it)\n        }\n\n    private val _languageList: MutableStateFlow<List<LanguageInfo>> = MutableStateFlow(emptyList())\n    val languageList = _languageList.asStateFlow()\n    val systemLanguageOrDefault: LanguageInfo by lazy {\n        getSystemLanguageIfWeCanUse()\n    }\n    val selectedLanguageInStorage = storage.selectedLanguage\n    val selectedLanguage = storage.selectedLanguage.mapStateFlow {\n        it ?: systemLanguageOrDefault.toLocaleString()\n    }\n\n    //    val selectedLanguageInfo = selectedLanguage.mapStateFlow {\n//        bestLanguageInfo(it)\n//    }\n    val isRtl = selectedLanguage.mapStateFlow { selectedLanguage ->\n        rtlLanguages.any { selectedLanguage.startsWith(it) }\n    }\n\n    fun boot() {\n        _languageList.value = getAvailableLanguages()\n        instance = this\n    }\n\n    fun selectLanguage(languageInfo: LanguageInfo?) {\n//        ensure that language info is in the list!\n//        val languageInfo = languageList.value.find { it == languageInfo }\n//        selectedLanguage.value = (languageInfo ?: DefaultLanguageInfo).toLocaleString()\n        selectedLanguageInStorage.value = languageInfo?.toLocaleString()\n    }\n\n    fun getMessage(key: String): String {\n        return getMessageContainer().getMessage(key)?.takeIf { it.isNotBlank() }\n            ?: defaultLanguageData.value.getMessage(key)\n            ?: key\n    }\n\n    private fun getRequestedLanguage(): String {\n        return selectedLanguage.value\n    }\n\n    @Volatile\n    private var loadedLanguage: LoadedLanguage? = null\n\n    private val defaultLanguageData = lazy {\n        createMessageContainer(DefaultLanguageInfo)\n    }\n\n    private fun createMessageContainer(\n        languageInfo: LanguageInfo,\n    ): MessageData {\n        return when {\n            languageInfo == DefaultLanguageInfo && defaultLanguageData.isInitialized() -> defaultLanguageData.value\n            else -> PropertiesMessageContainer(\n                Properties().apply {\n                    kotlin.runCatching {\n                        openStream(languageInfo.resource)\n                            .reader(Charsets.UTF_8)\n                            .use {\n                                load(it)\n                            }\n                    }.onFailure {\n                        println(\"Error while loading language data!\")\n                        it.printStackTrace()\n                    }\n                }\n            )\n        }\n    }\n\n    /**\n     * Find the best language info for the given locale.\n     * the returned language is guaranteed to be available. (at least [DefaultLanguageInfo])\n     */\n    private fun bestLanguageInfo(locale: String): LanguageInfo {\n        return languageList.value.find {\n            it.toLocaleString() == locale\n        } ?: DefaultLanguageInfo\n    }\n\n    private fun getMessageContainer(): MessageData {\n        val requestedLanguage = getRequestedLanguage()\n        this.loadedLanguage.let { loadedLanguage ->\n            if (loadedLanguage != null && loadedLanguage.languageInfo.toLocaleString() == requestedLanguage) {\n                return loadedLanguage.messageData\n            }\n        }\n        synchronized(this) {\n            // make sure not created earlier\n            this.loadedLanguage.let { loadedLanguage ->\n                if (loadedLanguage != null && loadedLanguage.languageInfo.toLocaleString() == requestedLanguage) {\n                    return loadedLanguage.messageData\n                }\n            }\n            val languageInfo = bestLanguageInfo(requestedLanguage)\n            val created = LoadedLanguage(\n                languageInfo,\n                createMessageContainer(languageInfo)\n            )\n            this.loadedLanguage = created\n            return created.messageData\n        }\n    }\n\n    private fun getAvailableLanguages(): List<LanguageInfo> {\n        return languageSourceProvider.allLanguageResources\n            .mapNotNull {\n                runCatching {\n                    MyLocale\n                        .fromLanguageResource(it)\n                        .toLanguageInfo(it)\n                }.onFailure {\n                    println(\"fail to load $it\")\n                    it.printStackTrace()\n                }\n                    .getOrNull()\n            }\n\n    }\n\n    private fun getSystemLanguageIfWeCanUse(): LanguageInfo {\n        val systemLocale = getSystemLocale().toString()\n        return bestLanguageInfo(systemLocale)\n    }\n\n    companion object {\n        lateinit var instance: LanguageManager\n\n        fun openStream(source: MyLanguageResource): InputStream {\n            return runBlocking {\n                source.getData().inputStream()\n            }\n        }\n\n        private fun MyLocale.toLanguageInfo(\n            languageResource: MyLanguageResource,\n        ): LanguageInfo {\n            return LanguageInfo(\n                locale = MyLocale(\n                    languageCode = languageCode,\n                    countryCode = countryCode,\n                ),\n                nativeName = LanguageNameProvider.getNativeName(this),\n                resource = languageResource,\n            )\n        }\n\n        private val rtlLanguages = arrayOf(\"ar\", \"bqi\", \"ckb\", \"fa\", \"he\", \"iw\", \"ji\", \"ur\", \"yi\")\n    }\n}\n\ninterface MessageData {\n    fun getMessage(key: String): String?\n}\n\nclass PropertiesMessageContainer(\n    private val properties: Properties,\n) : MessageData {\n    override fun getMessage(key: String): String? {\n        return properties.getProperty(key)\n    }\n}\n\nprivate data class LoadedLanguage(\n    val languageInfo: LanguageInfo,\n    val messageData: MessageData,\n)\n\n@Immutable\ndata class LanguageInfo(\n    val locale: MyLocale,\n    val nativeName: String,\n    val resource: MyLanguageResource,\n) {\n    fun toLocaleString(): String {\n        return locale.toString()\n    }\n}\n\nprivate fun getSystemLocale(): MyLocale {\n    val javaSystemLocale = Locale.getDefault(Locale.Category.DISPLAY)\n    return MyLocale(\n        languageCode = javaSystemLocale.language,\n        countryCode = javaSystemLocale.country,\n    )\n}\nfun MyLocale.Companion.fromLanguageResource(languageResource: MyLanguageResource): MyLocale {\n    return languageResource.language.split(\"_\").run {\n        MyLocale(\n            languageCode = get(0),\n            countryCode = getOrNull(1)\n        )\n    }\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/LanguageSourceProvider.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport ir.amirab.resources.contracts.MyLanguageResource\n\n/**\n * at the moment we only use bundled strings\n */\nclass LanguageSourceProvider(\n    val defaultLanguageResource: MyLanguageResource,\n    val allLanguageResources: List<MyLanguageResource>,\n)\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/LanguageStorage.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ninterface LanguageStorage {\n    // null means auto\n    val selectedLanguage: MutableStateFlow<String?>\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/LocalLanguageManager.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.LayoutDirection\n\nval LocalLanguageManager = staticCompositionLocalOf<LanguageManager> {\n    error(\"LocalLanguageManager not provided\")\n}\nval LocaleLanguageDirection = staticCompositionLocalOf<LayoutDirection> {\n    error(\"LocaleLanguageDirection not provided\")\n}\n\n@Composable\nfun WithLanguageDirection(\n    content: @Composable () -> Unit,\n) {\n    CompositionLocalProvider(\n        LocalLayoutDirection provides LocaleLanguageDirection.current,\n    ) {\n        content()\n    }\n}"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/MyLocale.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport androidx.compose.runtime.Immutable\nimport ir.amirab.resources.contracts.MyLanguageResource\n\n@Immutable\ndata class MyLocale(\n    val languageCode: String,\n    val countryCode: String?,\n) {\n    override fun toString(): String {\n        return buildString {\n            append(languageCode)\n            countryCode?.let {\n                append(\"_\")\n                append(it)\n            }\n        }\n    }\n\n    companion object\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/localizationmanager/StringVariableReplacer.kt",
    "content": "package ir.amirab.util.compose.localizationmanager\n\nimport arrow.core.fold\n\nprivate fun String.replaceWithVariable(name: String, value: String): String {\n    return replace(\"{{$name}}\", value)\n}\n\ninternal fun String.withReplacedArgs(args: Map<String, String>): String {\n    return args.fold(this) { acc, entry ->\n        acc.replaceWithVariable(entry.key, entry.value)\n    }\n}"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/modifiers/AutoMirror.kt",
    "content": "package ir.amirab.util.compose.modifiers\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.LayoutDirection\n\nfun Modifier.autoMirror() = composed {\n    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl\n    scale(\n        scaleX = if (isRtl) -1f else 1f,\n        scaleY = 1f\n    )\n}"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/modifiers/Clickables.kt",
    "content": "package ir.amirab.util.compose.modifiers\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.ui.Modifier\n\nfun Modifier.hijackClick(): Modifier {\n    return silentClickable {\n        // nothing\n    }\n}\n\nfun Modifier.silentClickable(\n    interactionSource: MutableInteractionSource? = null,\n    onClick: () -> Unit\n): Modifier {\n    return clickable(\n        interactionSource = interactionSource,\n        indication = null,\n        onClick = onClick,\n    )\n}\n"
  },
  {
    "path": "shared/compose-utils/src/commonMain/kotlin/ir/amirab/util/compose/resources/Resources.kt",
    "content": "package ir.amirab.util.compose.resources\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport ir.amirab.util.compose.localizationmanager.LocalLanguageManager\nimport ir.amirab.util.compose.localizationmanager.withReplacedArgs\n\ntypealias MyStringResource = ir.amirab.resources.contracts.MyStringResource\n\n@Composable\nfun myStringResource(key: MyStringResource): String {\n    val languageManager = LocalLanguageManager.current\n    val language by languageManager.selectedLanguage.collectAsState()\n    return remember(language, key) {\n        languageManager.getMessage(key.id)\n    }\n}\n\n@Composable\nfun myStringResource(key: MyStringResource, args: Map<String, String>): String {\n    val languageManager = LocalLanguageManager.current\n    val language by languageManager.selectedLanguage.collectAsState()\n    return remember(language, key, args) {\n        languageManager\n            .getMessage(key.id)\n            .withReplacedArgs(args)\n    }\n}"
  },
  {
    "path": "shared/config/build.gradle.kts",
    "content": "plugins {\n    id(MyPlugins.kotlin)\n}\ndependencies {\n    implementation(libs.androidx.datastore)\n    implementation(libs.kotlin.serialization.json)\n}"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/Config.kt",
    "content": "package ir.amirab.util.config\n\nimport java.util.Collections\nimport kotlin.reflect.KType\nimport kotlin.reflect.typeOf\n\n\n\n\n\ninterface Config {\n    fun toMap(): Map<String, Any?>\n\n    fun <T:Any>put(key: ConfigKey.OfPrimitiveType<T>, value: T){\n        put(key.keyName,key.primitiveType,value)\n    }\n\n    fun <T:Any>put(key: String,type: PrimitiveType<T>,value:T)\n    fun putInt(key: String, value: Int) = put(key,PrimitiveType.Int,value)\n    fun putFloat(key: String, value: Float) = put(key,PrimitiveType.Float,value)\n    fun putLong(key: String, value: Long) = put(key,PrimitiveType.Long,value)\n    fun putDouble(key: String, value: Double) = put(key,PrimitiveType.Double,value)\n    fun putBoolean(key: String, value: Boolean) = put(key,PrimitiveType.Boolean,value)\n    fun putString(key: String, value: String) = put(key,PrimitiveType.String,value)\n\n    fun <T> get(key: String, type: PrimitiveType<T>): T?\n    fun <T>get(key: ConfigKey.OfPrimitiveType<T>):T?{\n        return get(key.keyName,key.primitiveType)\n    }\n\n    fun getInt(key: String): Int? = get(key, Config.PrimitiveType.Int)\n    fun getFloat(key: String): Float? = get(key, Config.PrimitiveType.Float)\n    fun getLong(key: String): Long? = get(key, Config.PrimitiveType.Long)\n    fun getDouble(key: String): Double? = get(key, Config.PrimitiveType.Double)\n    fun getBoolean(key: String): Boolean? = get(key, Config.PrimitiveType.Boolean)\n    fun getString(key: String): String? = get(key,Config.PrimitiveType.String)\n\n\n    fun removeKey(key: String)\n    fun removeKey(key: ConfigKey) {\n        removeKey(key.keyName)\n    }\n    sealed interface PrimitiveType<T> {\n        companion object{\n            fun ensureIsPrimitive(value: Any) {\n                require(value is Number || value is kotlin.String || value is kotlin.Boolean) {\n                    \"value must be number|string|boolean was ${value::class.qualifiedName}\"\n                }\n            }\n            fun <T>fromType(type:KType):PrimitiveType<T>?{\n                @Suppress(\"UNCHECKED_CAST\")\n                return when(type){\n                    typeOf<kotlin.Int>() -> PrimitiveType.Int\n                    typeOf<kotlin.Long>() -> PrimitiveType.Long\n                    typeOf<kotlin.Float>() -> PrimitiveType.Float\n                    typeOf<kotlin.Double>() -> PrimitiveType.Double\n                    typeOf<kotlin.String>() -> PrimitiveType.String\n                    typeOf<kotlin.Boolean>() -> PrimitiveType.Boolean\n                    else -> null\n                } as PrimitiveType<T>?\n            }\n            fun <T:Any>fromValue(value: T):PrimitiveType<T>?{\n                @Suppress(\"UNCHECKED_CAST\")\n                return when(value){\n                    is kotlin.Int-> PrimitiveType.Int\n                    is kotlin.Long-> PrimitiveType.Long\n                    is kotlin.Float-> PrimitiveType.Float\n                    is kotlin.Double-> PrimitiveType.Double\n                    is kotlin.String-> PrimitiveType.String\n                    is kotlin.Boolean-> PrimitiveType.Boolean\n                    else -> null\n                } as PrimitiveType<T>?\n            }\n        }\n        fun toType(value: Any):T?\n        data object Int: PrimitiveType<kotlin.Int> {\n            override fun toType(value: Any): kotlin.Int? {\n                return if (value is Number){\n                    value.toInt()\n                }else{\n                    value.toString().toIntOrNull()\n                }\n            }\n\n        }\n        data object Long: PrimitiveType<kotlin.Long> {\n            override fun toType(value: Any): kotlin.Long? {\n                return if (value is Number){\n                    value.toLong()\n                }else{\n                    value.toString().toLongOrNull()\n                }\n            }\n        }\n        data object Float: PrimitiveType<kotlin.Float> {\n            override fun toType(value: Any): kotlin.Float? {\n                return if (value is Number){\n                    value.toFloat()\n                }else{\n                    value.toString().toFloatOrNull()\n                }\n            }\n        }\n        data object Double: PrimitiveType<kotlin.Double> {\n            override fun toType(value: Any): kotlin.Double? {\n                return if (value is Number){\n                    value.toDouble()\n                }else{\n                    value.toString().toDoubleOrNull()\n                }\n            }\n        }\n        data object String: PrimitiveType<kotlin.String> {\n            override fun toType(value: Any): kotlin.String {\n                return value.toString()\n            }\n        }\n\n        data object Boolean: PrimitiveType<kotlin.Boolean> {\n            override fun toType(value: Any): kotlin.Boolean? {\n                return if (value is kotlin.Boolean){\n                    value\n                }else{\n                    value.toString().toBooleanStrictOrNull()\n                }\n            }\n        }\n    }\n}\n\n\n\nclass MapConfig() : Config {\n    constructor(config: Config) : this() {\n        this.map.putAll(config.toMap())\n    }\n\n    private val map = Collections.synchronizedMap(linkedMapOf<String, Any?>())\n\n    override fun <T:Any> put(key: String, type: Config.PrimitiveType<T>, value: T) {\n        map[key] = value\n    }\n\n    override fun <T> get(key: String, type: Config.PrimitiveType<T>): T? {\n        val value = map[key] ?: return null\n        Config.PrimitiveType.ensureIsPrimitive(value)\n        return type.toType(value)\n    }\n\n    override fun removeKey(key: String) {\n        map.remove(key)\n    }\n\n    override fun toMap(): Map<String, Any?> {\n        return map.toMap()\n    }\n\n    override fun toString(): String {\n        return toMap().toString()\n    }\n\n    override fun equals(other: Any?): Boolean {\n        val otherMap = when(other){\n            is MapConfig -> other.map\n            is Config -> other.toMap()\n            else -> null\n        }\n        return map == otherMap\n    }\n\n    override fun hashCode(): Int {\n        return map.hashCode() ?: 0\n    }\n\n}\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/ConfigKeyWithPrimitiveType.kt",
    "content": "package ir.amirab.util.config\n\nsealed class ConfigKey {\n    abstract val keyName: String\n\n    class OfPrimitiveType<T> internal constructor(\n        override val keyName: String,\n        val primitiveType: Config.PrimitiveType<T>,\n    ) : ConfigKey()\n\n    class OfNotPrimitiveType<T>(\n        override val keyName: String,\n    ) : ConfigKey()\n}\n\nfun <T> keyOfEncoded(name: String) = ConfigKey.OfNotPrimitiveType<T>(\n    keyName = name,\n)\n\nfun intKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.Int\n)\n\nfun floatKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.Float\n)\n\nfun longKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.Long\n)\n\nfun doubleKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.Double\n)\n\nfun booleanKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.Boolean\n)\n\nfun stringKeyOf(name: String) = ConfigKey.OfPrimitiveType(\n    keyName = name, primitiveType = Config.PrimitiveType.String\n)\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/ConfigToJson.kt",
    "content": "package ir.amirab.util.config\n\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.JsonObject\n\nobject ConfigToJson {\n    private val nestedMapCreator get() = NestedMapCreator()\n    fun fromJson(configRegistry: Config, element: JsonObject) {\n        val x = JsonObjectToMap().transformJsonObject(element)\n        nestedMapCreator.createFlatten(x)\n            .forEach { (key, value) ->\n                if (value!=null){\n                    val type = Config.PrimitiveType.fromValue(value)\n                    if (type!=null){\n                        configRegistry.put(key,type,value)\n                    }\n                }\n            }\n    }\n\n    fun toJson(configRegistry: Config): JsonElement {\n        return MapToJsonObject()\n            .transformMap(nestedMapCreator.createdNested(configRegistry.toMap()))\n    }\n}\n\nfun Config.toJson(): JsonElement {\n    return ConfigToJson.toJson(this)\n}\nfun <T : Config> T.loadFromJson(json: JsonObject): T {\n    ConfigToJson.fromJson(this, json)\n    return this\n}"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/JsonMapper.kt",
    "content": "package ir.amirab.util.config\n\nimport kotlinx.serialization.json.*\n\n\nclass JsonObjectToMap {\n    private fun transform(jsonElement: JsonElement): Any? {\n        return when (jsonElement) {\n            is JsonArray -> transformJsonArray(jsonElement)\n            is JsonObject -> transformJsonObject(jsonElement)\n            is JsonPrimitive -> transformJsonPrimitive(jsonElement)\n        }\n    }\n\n    fun transformJsonObject(jsonObject: JsonObject): Map<String, Any?> {\n        return jsonObject.mapValues {\n            transform(it.value)\n        }\n    }\n\n    fun transformJsonArray(jsonArray: JsonArray): List<Any?> {\n        return jsonArray.map {\n            transform(it)\n        }\n    }\n\n    private fun transformJsonPrimitive(primitive: JsonPrimitive): Any? {\n        if (primitive.isString) {\n            return primitive.content\n        }\n        if (primitive == JsonNull) return null\n        //bool or number\n        primitive.booleanOrNull?.let {\n            return it\n        }\n        if (primitive.content.contains(\".\")) {\n            return primitive.double\n        }\n        return primitive.long\n    }\n}\n\nclass MapToJsonObject {\n    private fun transformList(list: List<*>): JsonArray {\n        return JsonArray(\n            list.map {\n                transform(it)\n            }\n        )\n    }\n\n    fun transformMap(map: Map<String, Any?>): JsonObject {\n        return map.mapValues {\n            transform(it.value)\n        }.let {\n            JsonObject(it)\n        }\n    }\n\n    private fun transform(value: Any?): JsonElement {\n        return when (value) {\n            null -> JsonNull\n            is Map<*, *> -> {\n                @Suppress(\"UNCHECKED_CAST\")\n                transformMap(value as Map<String, Any>)\n            }\n\n            is List<*> -> {\n                transformList(value)\n            }\n\n            else -> transformPrimitive(value)\n        }\n    }\n\n    private fun transformPrimitive(value: Any): JsonPrimitive {\n        return when (value) {\n            is Number -> JsonPrimitive(value)\n            is Boolean -> JsonPrimitive(value)\n            is String -> JsonPrimitive(value)\n            else -> error(\"not supported this type ${value::class.qualifiedName}\")\n        }\n    }\n}\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/NestedCreator.kt",
    "content": "package ir.amirab.util.config\n\nclass NestedMapCreator(\n    private val separator: String = \".\"\n) {\n    private fun createNested(\n        output: MutableMap<String, Any?>,\n        segments: List<String>\n    ): MutableMap<String, Any?> {\n        if (segments.isEmpty()) {\n            return output\n        }\n        val sub = segments.drop(1)\n        val segment = segments.first()\n        val maybeMap = output[segment]\n        val map = if (maybeMap is MutableMap<*, *>) {\n            @Suppress(\"UNCHECKED_CAST\")\n            maybeMap as MutableMap<String, Any?>\n        } else {\n            val createdMap = mutableMapOf<String, Any?>().apply {\n                if (maybeMap != null) {\n                    put(\"\", maybeMap)\n                }\n            }\n            createdMap\n        }\n        output[segment] = map\n        return createNested(map, sub)\n    }\n\n    fun createdNested(\n        flatten: Map<String, Any?>,\n        output: MutableMap<String, Any?> = mutableMapOf()\n    ): Map<String, Any?> {\n        for ((key, value) in flatten) {\n            val segments = key.split(separator)\n            val map = createNested(output, segments.dropLast(1))\n            val lastSegment = segments.last()\n            val maybeMap = map[lastSegment]\n            if (maybeMap is MutableMap<*, *>) {\n                @Suppress(\"UNCHECKED_CAST\")\n                maybeMap as MutableMap<String, Any?>\n                maybeMap.put(\"\", value)\n            } else {\n                map[lastSegment] = value\n            }\n\n\n//            println(map)\n        }\n        return output\n    }\n\n\n    fun createFlatten(\n        nested: Map<String, Any?>,\n        prefixes: List<String> = emptyList(),\n        output: MutableMap<String, Any?> = mutableMapOf()\n    ): Map<String, Any?> {\n        for ((key, value) in nested) {\n            val flattenKeySegments = prefixes + key\n//            println(flattenKeySegments.joinToString(separator))\n            if (value is Map<*, *>) {\n                @Suppress(\"UNCHECKED_CAST\")\n                createFlatten(value as Map<String, Any?>, flattenKeySegments, output)\n            } else {\n                val flattenKey = flattenKeySegments\n                    .filter{ it.isNotEmpty() }\n                    .joinToString(separator)\n                output[flattenKey] = value\n            }\n        }\n        return output\n    }\n}\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/datastore/KotlinSerializationDataStore.kt",
    "content": "package ir.amirab.util.config.datastore\n\nimport androidx.datastore.core.CorruptionException\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.core.DataStoreFactory\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.json.Json\nimport androidx.datastore.core.Serializer\nimport androidx.datastore.core.handlers.ReplaceFileCorruptionHandler\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.SerializationException\nimport kotlinx.serialization.json.decodeFromStream\nimport kotlinx.serialization.json.encodeToStream\nimport kotlinx.serialization.serializer\nimport java.io.File\nimport java.io.InputStream\nimport java.io.OutputStream\n\nclass KotlinSerializationDataStore<T>(\n    val json: Json,\n    val serializer: KSerializer<T>,\n    val default: () -> T,\n) : Serializer<T> {\n    override val defaultValue: T get() = default()\n    override suspend fun readFrom(input: InputStream): T {\n        try {\n            @OptIn(ExperimentalSerializationApi::class)\n            return json.decodeFromStream(serializer, input)\n        } catch (e: SerializationException) {\n            throw CorruptionException(\"cant decode this input\", e)\n        }\n    }\n\n    override suspend fun writeTo(t: T, output: OutputStream) {\n        @OptIn(ExperimentalSerializationApi::class)\n        json.encodeToStream(serializer, t, output)\n    }\n}\n\ninline fun <reified T> kotlinxSerializationDataStore(\n    file: File,\n    json: Json,\n    noinline default: () -> T,\n): DataStore<T> {\n    return DataStoreFactory.create(\n        serializer = KotlinSerializationDataStore(\n            json = json,\n            serializer = serializer<T>(),\n            default = {\n                // no data found\n                default()\n            }\n        ),\n        produceFile = { file },\n        corruptionHandler = ReplaceFileCorruptionHandler(\n            produceNewData = {\n                // exception thrown during decoding\n                default()\n            }\n        ),\n    )\n}\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/datastore/MapConfigDataStore.kt",
    "content": "package ir.amirab.util.config.datastore\n\nimport androidx.datastore.core.CorruptionException\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.core.DataStoreFactory\nimport androidx.datastore.core.Serializer\nimport androidx.datastore.core.handlers.ReplaceFileCorruptionHandler\nimport ir.amirab.util.config.MapConfig\nimport ir.amirab.util.config.loadFromJson\nimport ir.amirab.util.config.toJson\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.SerializationException\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.decodeFromStream\nimport java.io.File\nimport java.io.InputStream\nimport java.io.OutputStream\n\n\nclass MyConfigSerializer(\n    private val  json: Json\n) : Serializer<MapConfig> {\n    override val defaultValue: MapConfig\n        get() = MapConfig()\n\n    @OptIn(ExperimentalSerializationApi::class)\n    override suspend fun readFrom(input: InputStream): MapConfig {\n        return withContext(Dispatchers.IO) {\n            MapConfig().apply {\n                try {\n                    loadFromJson(json.decodeFromStream(input))\n                }catch (e:SerializationException){\n                    throw CorruptionException(\"Json is corrupted\",e)\n                }\n            }\n        }\n    }\n\n    override suspend fun writeTo(t: MapConfig, output: OutputStream) {\n        withContext(Dispatchers.IO) {\n            output.write(json.encodeToString(t.toJson()).toByteArray())\n        }\n    }\n}\n\nfun createMapConfigDatastore(\n    file: File,\n    json: Json,\n): DataStore<MapConfig> {\n    return DataStoreFactory.create(\n        serializer = MyConfigSerializer(json),\n        produceFile = { file },\n        corruptionHandler = ReplaceFileCorruptionHandler{\n            MapConfig()\n        },\n    )\n}\n\nsuspend fun DataStore<MapConfig>.edit(\n    editor: (MapConfig) -> Unit,\n) {\n    updateData {\n        val newConfig = MapConfig(it)\n        editor(newConfig)\n        newConfig\n    }\n}\n"
  },
  {
    "path": "shared/config/src/main/kotlin/ir/amirab/util/config/extensions.kt",
    "content": "package ir.amirab.util.config\n\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.serializer\n\n\ncontext(json: Json)\ninline fun <reified T : Any> MapConfig.putEncoded(key: String, value: T) {\n    putString(key, json.encodeToString(serializer<T>(), value))\n}\n\ncontext(json: Json)\ninline fun <reified T> MapConfig.putEncodedNullable(key: ConfigKey.OfNotPrimitiveType<T>, value: T?) {\n    if (value != null) {\n        putString(key.keyName, json.encodeToString(serializer<T>(), value))\n    } else {\n        removeKey(key)\n    }\n}\n\ncontext(_: Json)\ninline fun <reified T : Any> MapConfig.putEncoded(key: ConfigKey.OfNotPrimitiveType<T>, value: T) {\n    putEncoded<T>(key.keyName, value)\n}\n\ncontext(json: Json)\ninline fun <reified T> MapConfig.getDecoded(key: String): T? {\n    val str = getString(key) ?: return null\n    return runCatching<T> {\n        json.decodeFromString(str)\n    }\n        .onFailure {\n            //log error\n        }\n        .getOrNull()\n}\n\ncontext(_: Json)\ninline fun <reified T> MapConfig.getDecoded(key: ConfigKey.OfNotPrimitiveType<T>): T? {\n    return getDecoded(key.keyName)\n}\n\ninline fun <reified T : Any> MapConfig.putNullable(key: ConfigKey.OfPrimitiveType<T>, value: T?) {\n    if (value == null) {\n        removeKey(key)\n    } else {\n        put(key, value)\n    }\n}\n"
  },
  {
    "path": "shared/nanohttp4k/build.gradle.kts",
    "content": "plugins {\n    id(MyPlugins.kotlin)\n}\ndependencies {\n    api(libs.http4k.core)\n    implementation(libs.nanoHttpd.core)\n}"
  },
  {
    "path": "shared/nanohttp4k/src/main/kotlin/ir/amirab/util/http4k/NanoHttpServer.kt",
    "content": "package ir.amirab.util.http4k\n\nimport fi.iki.elonen.NanoHTTPD\nimport org.http4k.core.HttpHandler\nimport org.http4k.core.Response\nimport org.http4k.core.Status\nimport org.http4k.core.Request as Http4KRequest\nimport org.http4k.core.Response as Http4kResponse\nimport org.http4k.core.Method as Http4kMethod\nimport org.http4k.server.Http4kServer\nimport org.http4k.server.ServerConfig\nimport java.io.FilterInputStream\nimport java.io.InputStream\nimport kotlin.math.min\n\nprivate open class LimitedInputStream(inputStream: InputStream, val maxRead: Long) : FilterInputStream(inputStream) {\n    fun allowedRemaining() = maxRead - readCount\n\n    var readCount = 0\n        private set\n\n    fun maxReached() = allowedRemaining() <= 0\n\n    private fun reachedEnd(): Int {\n        return -1\n    }\n\n    override fun read(): Int {\n        if (maxReached()) {\n            return reachedEnd()\n        }\n        readCount++\n        return super.read()\n    }\n\n    override fun read(b: ByteArray): Int {\n        return read(b, 0, b.size)\n    }\n\n    override fun read(b: ByteArray, off: Int, len: Int): Int {\n        val allowedRead = allowedRemaining()\n        if (allowedRead <= 0) {\n            return reachedEnd()\n        }\n        val newLen = min(allowedRead, len.toLong())\n\n        val result = super.read(b, off, newLen.toInt())\n        if (result >= 0) {\n            readCount += result\n        }\n        return result\n    }\n\n    override fun readAllBytes(): ByteArray {\n        return readNBytes(Int.MAX_VALUE)\n    }\n\n    override fun readNBytes(len: Int): ByteArray {\n        val newLen = constrainRequestedLength(len)\n        if (newLen < 0) {\n            return byteArrayOf()\n        }\n        return super.readNBytes(newLen)\n    }\n\n    fun constrainRequestedLength(len: Int): Int {\n        return min(allowedRemaining(), len.toLong()).toInt()\n    }\n\n}\n\n/**\n * Nano http give me the socket's input stream\n * so Instead of the close the underlying socket\n * I skipp the remaining body size\n * so the socket is not closed and can be reused\n */\nprivate class NanoHttpDInputStream(\n    inputStream: InputStream, length: Long\n) : LimitedInputStream(inputStream, length) {\n    override fun close() {\n        skip(allowedRemaining())\n    }\n}\n\nprivate class NanoHttpDForHttp4K(\n    hostName: String,\n    port: Int,\n    private val handler: HttpHandler,\n    private val isDebugMode: Boolean,\n) : NanoHTTPD(hostName, port) {\n    private fun IHTTPSession.toHttp4KRequest(): Http4KRequest {\n\n        val length = (this as HTTPSession).bodySize\n        // I duplicate that input stream because http4k close that but it is the underlying input stream\n//        val body = ByteArrayInputStream(inputStream.readNBytes(length))\n        val body = NanoHttpDInputStream(inputStream, length)\n        return Http4KRequest(\n            method = Http4kMethod.valueOf(method.name),\n            uri = this.uri\n        ).body(\n            body = body,\n            length = length\n        )\n            .headers(headers.map { it.key to it.value })\n    }\n\n    private fun Http4kResponse.toNanoHttpResponse(): Response {\n        val length = this.body.length\n\n        val bodyInputStream = this.body.stream\n\n        val nanoResponseStatus = Response.Status.lookup(status.code)\n\n        val response = if (length != null) {\n            newFixedLengthResponse(\n                nanoResponseStatus,\n                null,\n                bodyInputStream,\n                length\n            )\n        } else {\n            newChunkedResponse(\n                nanoResponseStatus,\n                null,\n                bodyInputStream,\n            )\n        }\n        headers.forEach {\n            response.addHeader(it.first, it.second)\n        }\n        return response\n    }\n\n    override fun serve(session: IHTTPSession): Response {\n        val response = kotlin.runCatching {\n            session.toHttp4KRequest().use { request ->\n                handler(request)\n            }\n        }.getOrElse { throwable ->\n            val shortDescription = \"${throwable::class.simpleName} ${throwable.localizedMessage}\"\n            val extraInfo = if (isDebugMode) {\n                throwable.stackTraceToString()\n            } else null\n            Response(Status.INTERNAL_SERVER_ERROR)\n                .body(\"Error $shortDescription\\n$extraInfo\")\n        }\n        return response.toNanoHttpResponse()\n    }\n}\n\nprivate class NanoHttpServer(\n    val hostName: String,\n    val port: Int,\n    handler: HttpHandler,\n    debug: Boolean,\n) : Http4kServer {\n    val server = NanoHttpDForHttp4K(hostName, port, handler, debug)\n    override fun port(): Int {\n        return if (port > 0) port\n        else server.listeningPort\n    }\n\n    override fun start(): Http4kServer = apply {\n        server.start()\n    }\n\n    override fun stop(): Http4kServer = apply {\n        server.stop()\n    }\n}\n\nclass NanoHttp(\n    val hostName: String,\n    val port: Int,\n    val isDebugMode: Boolean = false,\n) : ServerConfig {\n    override fun toServer(http: HttpHandler): Http4kServer {\n        return NanoHttpServer(hostName, port, http, isDebugMode)\n    }\n}"
  },
  {
    "path": "shared/nanohttp4k/src/main/resources/rules.pro",
    "content": "-keep class fi.iki.elonen.**{ *; }\n\n-keep class org.http4k.** { *; }\n\n-dontwarn org.http4k.**"
  },
  {
    "path": "shared/resources/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport java.util.Properties\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(MyPlugins.composeBase)\n    id(Plugins.Android.library)\n}\nval ourPackageName = \"com.abdownloadmanager.resources\"\nval propertiesToKotlinTask by tasks.registering(PropertiesToKotlinTask::class) {\n    outputDir.set(file(\"build/tasks/propertiesToKotlinTask\"))\n    generatedFileName.set(\"String.kt\")\n    packageName.set(ourPackageName)\n    myStringResourceClass.set(\"ir.amirab.resources.contracts.MyStringResource\")\n    propertyFiles.from(\"src/commonMain/resources/com/abdownloadmanager/resources/locales/en_US.properties\")\n}\nval generateResourceMap by tasks.registering(GenerateResourceMap::class) {\n    outputDir.set(file(\"build/tasks/generateResourceMapTask\"))\n    generatedFileName.set(\"ResourceMap.kt\")\n    packageName.set(ourPackageName)\n    baseFolder.set(file(\"src/commonMain/resources/\"))\n}\nval generateResObject by tasks.registering(GenerateResObject::class) {\n    outputDir.set(file(\"build/tasks/generateResObjectTask\"))\n    generatedFileName.set(\"Res.kt\")\n    packageName.set(ourPackageName)\n    dependsOn(propertiesToKotlinTask)\n    dependsOn(generateResourceMap)\n}\n\nkotlin {\n    jvm(\"desktop\")\n    androidTarget {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain {\n            kotlin {\n                srcDirs(propertiesToKotlinTask.map { it.outputDir })\n                srcDirs(generateResourceMap.map { it.outputDir })\n                srcDirs(generateResObject.map { it.outputDir })\n            }\n            dependencies {\n                implementation(libs.compose.ui)\n                api(libs.okio.okio)\n                implementation(project(\":shared:resources:contracts\"))\n            }\n        }\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"com.abdownloadmanager.resources\"\n    defaultConfig {\n        minSdk = 26\n    }\n    sourceSets.named(\"main\") {\n        resources.srcDir(\"src/commonMain/resources\")\n    }\n}\nabstract class GenerateResObject @Inject constructor(\n    project: Project\n) : DefaultTask() {\n\n    @get:Input\n    val packageName = project.objects.property<String>()\n\n    @get:OutputDirectory\n    val outputDir = project.objects.directoryProperty()\n\n    @get:Input\n    val generatedFileName = project.objects.property<String>()\n\n    @TaskAction\n    fun run() {\n        val content = buildString {\n            appendLine(\"package ${packageName.get()}\")\n            appendLine(\"object Res {\")\n            appendLine(\"  val string = Strings\")\n            appendLine(\"  val sourceMap = ResourceMap\")\n            appendLine(\"}\")\n        }\n        outputDir.file(generatedFileName).get().asFile.writer().use {\n            it.write(content)\n        }\n    }\n}\n\nabstract class GenerateResourceMap @Inject constructor(\n    project: Project\n) : DefaultTask() {\n\n    @get:Input\n    val packageName = project.objects.property<String>()\n\n    @get:InputDirectory\n    val baseFolder = project.objects.directoryProperty()\n\n    @get:OutputDirectory\n    val outputDir = project.objects.directoryProperty()\n\n    @get:Input\n    val generatedFileName = project.objects.property<String>()\n\n    @TaskAction\n    fun run() {\n        val base = baseFolder.asFile.get()\n        val files = base.walkTopDown()\n            .filter {\n                it.isFile\n            }.map {\n                it\n                    .relativeTo(base).toString()\n                    .replace(\"\\\\\", \"/\")\n            }\n        val content = buildString {\n            appendLine(\"package ${packageName.get()}\")\n            appendLine(\"object ResourceMap {\")\n            appendLine(\"  val files = listOf(\")\n            val doubleQuotes = \"\\\"\".repeat(3)\n            for (file in files) {\n                appendLine(\"    $doubleQuotes$file$doubleQuotes,\")\n            }\n            appendLine(\"  )\")\n            appendLine(\"}\")\n        }\n        outputDir.file(generatedFileName).get().asFile.writer().use {\n            it.write(content)\n        }\n    }\n}\n\nabstract class PropertiesToKotlinTask @Inject constructor(\n    project: Project\n) : DefaultTask() {\n    @get:InputFiles\n    val propertyFiles = project.objects.fileCollection()\n\n    @get:Input\n    val packageName = project.objects.property<String>()\n\n    @get:Input\n    val myStringResourceClass = project.objects.property<String>()\n\n    @get:OutputDirectory\n    val outputDir = project.objects.directoryProperty()\n\n    @get:Input\n    val generatedFileName = project.objects.property<String>()\n\n    @TaskAction\n    fun run() {\n        val properties = Properties()\n        propertyFiles.forEach { file ->\n            file.inputStream().use { inputStream ->\n                properties.load(inputStream)\n            }\n        }\n        val content = createFileString(\n            packageName.get(),\n            myStringResourceClass.get(),\n            properties\n        )\n        outputDir.file(generatedFileName).get().asFile.writer().use {\n            it.write(content)\n        }\n    }\n\n    private fun createFileString(\n        packageName: String,\n        myStringResourceClass: String,\n        properties: Properties,\n    ): String {\n        val myStringResourceClassName = myStringResourceClass\n            .split(\".\").last()\n        val variableRegex by lazy { \"\\\\{\\\\{(?<variable>.+?)\\\\}\\\\}\".toRegex() }\n        fun findVariablesOfValue(value: String): List<String> {\n            return variableRegex\n                .findAll(value)\n                .toList()\n                .map {\n                    it.groups[\"variable\"]!!.value\n                }\n        }\n\n        fun propertyToCode(key: String, value: String): String {\n            val args = findVariablesOfValue(value)\n            val defination = \"val `$key` = $myStringResourceClassName(\\\"$key\\\")\"\n            if (args.isEmpty()) {\n                return defination\n            } else {\n                val comment = buildString {\n                    append(\"/**\\n\")\n                    append(\"accepted args:\\n\")\n                    args.forEach { value ->\n                        append(\"@param [$value]\\n\")\n                    }\n                    append(\"*/\")\n                }\n                val argCreatorFunction = buildString {\n                    append(\"fun `${key}_createArgs`(\")\n                    args.forEachIndexed { index, value ->\n                        append(\"$value: String\")\n                        if (index != args.lastIndex) {\n                            append(\", \")\n                        }\n                    }\n                    append(\") = \")\n                    append(\"mapOf(\")\n                    args.forEachIndexed { index, value ->\n                        append(\"\\\"$value\\\" to $value\")\n                        if (index != args.lastIndex) {\n                            append(\", \")\n                        }\n                    }\n                    append(\")\")\n                }\n                return \"$defination\\n$comment\\n$argCreatorFunction\"\n            }\n\n        }\n\n        return buildString {\n            appendLine(\"@file:Suppress(\\\"RemoveRedundantBackticks\\\", \\\"FunctionName\\\")\")\n            appendLine(\"package $packageName\")\n            appendLine(\"import $myStringResourceClass\")\n\n            appendLine(\"object Strings {\")\n            for (property in properties) {\n                val key = property.key.toString()\n                val value = property.value.toString()\n                val codeLines = propertyToCode(key, value).lines()\n                for (line in codeLines) {\n                    appendLine(\"  $line\")\n                }\n            }\n            appendLine(\"}\")\n        }\n    }\n}\n"
  },
  {
    "path": "shared/resources/contracts/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets.commonMain.dependencies {\n        implementation(libs.okio.okio)\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"com.abdownloadmanager.resources.contracts\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "shared/resources/contracts/src/commonMain/kotlin/ir/amirab/resources/contracts/MyLanguageResource.kt",
    "content": "package ir.amirab.resources.contracts\n\nimport okio.FileSystem\nimport okio.Path\n\nsealed interface MyLanguageResource {\n    val language: String\n    val getData: suspend () -> ByteArray\n\n    data class BundledLanguageResource(\n        override val language: String,\n        override val getData: suspend () -> ByteArray,\n    ) : MyLanguageResource\n\n    class ExternalLanguageResource(\n        val path: Path,\n    ) : MyLanguageResource {\n        override val language: String\n            get() = path.name.substringBeforeLast(\".\")\n        override val getData: suspend () -> ByteArray = {\n            FileSystem.SYSTEM.read(path) {\n                readByteArray()\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "shared/resources/contracts/src/commonMain/kotlin/ir/amirab/resources/contracts/MyStringResource.kt",
    "content": "package ir.amirab.resources.contracts\n\n@JvmInline\nvalue class MyStringResource(val id: String)"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/ABDMLanguageResources.kt",
    "content": "package com.abdownloadmanager.resources\n\nimport ir.amirab.resources.contracts.MyLanguageResource\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\n\nobject ABDMLanguageResources {\n    private const val LOCALES_DIRECTORY = \"com/abdownloadmanager/resources/locales/\"\n    val defaultLanguageResource = run {\n        val defaultName = \"en_US\"\n        MyLanguageResource.BundledLanguageResource(\n            language = defaultName,\n            getData = suspend { ResourceUtil.readResourceAsByteArray(\"$LOCALES_DIRECTORY$defaultName.properties\") }\n        )\n    }\n    val languages: List<MyLanguageResource>\n        get() = ResourceMap\n            .files\n            .filter { it.startsWith(LOCALES_DIRECTORY) }\n            .map {\n                MyLanguageResource.BundledLanguageResource(\n                    language = it.split(\"/\").last().split(\".\").first(),\n                    getData = suspend { ResourceUtil.readResourceAsByteArray(it) }\n                )\n            }\n\n}\n\ninternal object ResourceUtil {\n    fun readResourceAsByteArray(path: String): ByteArray {\n        return FileSystem.RESOURCES.read(path.toPath()) {\n            readByteArray()\n        }\n    }\n\n    fun readResourceAsString(path: String): String {\n        return FileSystem.RESOURCES.read(path.toPath()) {\n            readUtf8()\n        }\n    }\n}\n\n\n\nobject ABDMResources {\n    fun getTranslatorsContent(): String {\n        return ResourceUtil\n            .readResourceAsString(\"com/abdownloadmanager/resources/credits/translators.json\")\n    }\n}\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/ABDMIcons.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nobject ABDMIcons\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/AddLink.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.AddLink: ImageVector\n    get() {\n        if (_AddLink != null) {\n            return _AddLink!!\n        }\n        _AddLink = ImageVector.Builder(\n            name = \"AddLink\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(12.11f, 15.39f)\n                lineTo(8.23f, 19.27f)\n                curveTo(8.001f, 19.5f, 7.728f, 19.683f, 7.427f, 19.808f)\n                curveTo(7.127f, 19.933f, 6.805f, 19.997f, 6.48f, 19.997f)\n                curveTo(6.155f, 19.997f, 5.833f, 19.933f, 5.533f, 19.808f)\n                curveTo(5.232f, 19.683f, 4.96f, 19.5f, 4.73f, 19.27f)\n                curveTo(4.498f, 19.041f, 4.315f, 18.769f, 4.189f, 18.468f)\n                curveTo(4.064f, 18.168f, 3.999f, 17.846f, 3.999f, 17.52f)\n                curveTo(3.999f, 17.194f, 4.064f, 16.872f, 4.189f, 16.572f)\n                curveTo(4.315f, 16.271f, 4.498f, 15.999f, 4.73f, 15.77f)\n                lineTo(8.61f, 11.89f)\n                curveTo(8.798f, 11.702f, 8.904f, 11.446f, 8.904f, 11.18f)\n                curveTo(8.904f, 10.914f, 8.798f, 10.658f, 8.61f, 10.47f)\n                curveTo(8.422f, 10.282f, 8.166f, 10.176f, 7.9f, 10.176f)\n                curveTo(7.634f, 10.176f, 7.378f, 10.282f, 7.19f, 10.47f)\n                lineTo(3.31f, 14.36f)\n                curveTo(2.528f, 15.211f, 2.106f, 16.331f, 2.13f, 17.486f)\n                curveTo(2.155f, 18.641f, 2.624f, 19.742f, 3.441f, 20.559f)\n                curveTo(4.258f, 21.376f, 5.359f, 21.846f, 6.514f, 21.87f)\n                curveTo(7.669f, 21.894f, 8.789f, 21.472f, 9.64f, 20.69f)\n                lineTo(13.53f, 16.81f)\n                curveTo(13.623f, 16.717f, 13.697f, 16.606f, 13.748f, 16.484f)\n                curveTo(13.798f, 16.362f, 13.824f, 16.232f, 13.824f, 16.1f)\n                curveTo(13.824f, 15.968f, 13.798f, 15.838f, 13.748f, 15.716f)\n                curveTo(13.697f, 15.594f, 13.623f, 15.483f, 13.53f, 15.39f)\n                curveTo(13.437f, 15.297f, 13.326f, 15.223f, 13.204f, 15.172f)\n                curveTo(13.082f, 15.122f, 12.952f, 15.096f, 12.82f, 15.096f)\n                curveTo(12.688f, 15.096f, 12.558f, 15.122f, 12.436f, 15.172f)\n                curveTo(12.314f, 15.223f, 12.203f, 15.297f, 12.11f, 15.39f)\n                close()\n                moveTo(8.83f, 15.17f)\n                curveTo(8.923f, 15.263f, 9.034f, 15.336f, 9.156f, 15.386f)\n                curveTo(9.278f, 15.436f, 9.408f, 15.461f, 9.54f, 15.46f)\n                curveTo(9.672f, 15.461f, 9.802f, 15.436f, 9.924f, 15.386f)\n                curveTo(10.046f, 15.336f, 10.157f, 15.263f, 10.25f, 15.17f)\n                lineTo(15.17f, 10.25f)\n                curveTo(15.358f, 10.062f, 15.464f, 9.806f, 15.464f, 9.54f)\n                curveTo(15.464f, 9.274f, 15.358f, 9.018f, 15.17f, 8.83f)\n                curveTo(14.982f, 8.642f, 14.726f, 8.536f, 14.46f, 8.536f)\n                curveTo(14.194f, 8.536f, 13.938f, 8.642f, 13.75f, 8.83f)\n                lineTo(8.83f, 13.75f)\n                curveTo(8.736f, 13.843f, 8.662f, 13.954f, 8.611f, 14.075f)\n                curveTo(8.56f, 14.197f, 8.534f, 14.328f, 8.534f, 14.46f)\n                curveTo(8.534f, 14.592f, 8.56f, 14.723f, 8.611f, 14.845f)\n                curveTo(8.662f, 14.967f, 8.736f, 15.077f, 8.83f, 15.17f)\n                close()\n                moveTo(21f, 18f)\n                horizontalLineTo(20f)\n                verticalLineTo(17f)\n                curveTo(20f, 16.735f, 19.895f, 16.48f, 19.707f, 16.293f)\n                curveTo(19.52f, 16.105f, 19.265f, 16f, 19f, 16f)\n                curveTo(18.735f, 16f, 18.48f, 16.105f, 18.293f, 16.293f)\n                curveTo(18.105f, 16.48f, 18f, 16.735f, 18f, 17f)\n                verticalLineTo(18f)\n                horizontalLineTo(17f)\n                curveTo(16.735f, 18f, 16.48f, 18.105f, 16.293f, 18.293f)\n                curveTo(16.105f, 18.48f, 16f, 18.735f, 16f, 19f)\n                curveTo(16f, 19.265f, 16.105f, 19.52f, 16.293f, 19.707f)\n                curveTo(16.48f, 19.895f, 16.735f, 20f, 17f, 20f)\n                horizontalLineTo(18f)\n                verticalLineTo(21f)\n                curveTo(18f, 21.265f, 18.105f, 21.52f, 18.293f, 21.707f)\n                curveTo(18.48f, 21.895f, 18.735f, 22f, 19f, 22f)\n                curveTo(19.265f, 22f, 19.52f, 21.895f, 19.707f, 21.707f)\n                curveTo(19.895f, 21.52f, 20f, 21.265f, 20f, 21f)\n                verticalLineTo(20f)\n                horizontalLineTo(21f)\n                curveTo(21.265f, 20f, 21.52f, 19.895f, 21.707f, 19.707f)\n                curveTo(21.895f, 19.52f, 22f, 19.265f, 22f, 19f)\n                curveTo(22f, 18.735f, 21.895f, 18.48f, 21.707f, 18.293f)\n                curveTo(21.52f, 18.105f, 21.265f, 18f, 21f, 18f)\n                close()\n                moveTo(16.81f, 13.53f)\n                lineTo(20.69f, 9.64f)\n                curveTo(21.472f, 8.789f, 21.894f, 7.669f, 21.87f, 6.514f)\n                curveTo(21.846f, 5.359f, 21.376f, 4.258f, 20.559f, 3.441f)\n                curveTo(19.742f, 2.624f, 18.641f, 2.155f, 17.486f, 2.13f)\n                curveTo(16.331f, 2.106f, 15.211f, 2.528f, 14.36f, 3.31f)\n                lineTo(10.47f, 7.19f)\n                curveTo(10.377f, 7.283f, 10.303f, 7.394f, 10.252f, 7.516f)\n                curveTo(10.202f, 7.638f, 10.176f, 7.768f, 10.176f, 7.9f)\n                curveTo(10.176f, 8.032f, 10.202f, 8.162f, 10.252f, 8.284f)\n                curveTo(10.303f, 8.406f, 10.377f, 8.517f, 10.47f, 8.61f)\n                curveTo(10.563f, 8.703f, 10.674f, 8.777f, 10.796f, 8.828f)\n                curveTo(10.918f, 8.878f, 11.048f, 8.904f, 11.18f, 8.904f)\n                curveTo(11.312f, 8.904f, 11.442f, 8.878f, 11.564f, 8.828f)\n                curveTo(11.686f, 8.777f, 11.797f, 8.703f, 11.89f, 8.61f)\n                lineTo(15.77f, 4.73f)\n                curveTo(16f, 4.5f, 16.272f, 4.317f, 16.573f, 4.192f)\n                curveTo(16.873f, 4.067f, 17.195f, 4.003f, 17.52f, 4.003f)\n                curveTo(17.845f, 4.003f, 18.167f, 4.067f, 18.468f, 4.192f)\n                curveTo(18.768f, 4.317f, 19.041f, 4.5f, 19.27f, 4.73f)\n                curveTo(19.502f, 4.959f, 19.685f, 5.231f, 19.811f, 5.532f)\n                curveTo(19.937f, 5.832f, 20.001f, 6.154f, 20.001f, 6.48f)\n                curveTo(20.001f, 6.806f, 19.937f, 7.128f, 19.811f, 7.428f)\n                curveTo(19.685f, 7.729f, 19.502f, 8.001f, 19.27f, 8.23f)\n                lineTo(15.39f, 12.11f)\n                curveTo(15.296f, 12.203f, 15.222f, 12.314f, 15.171f, 12.435f)\n                curveTo(15.12f, 12.557f, 15.094f, 12.688f, 15.094f, 12.82f)\n                curveTo(15.094f, 12.952f, 15.12f, 13.083f, 15.171f, 13.205f)\n                curveTo(15.222f, 13.326f, 15.296f, 13.437f, 15.39f, 13.53f)\n                curveTo(15.483f, 13.624f, 15.594f, 13.698f, 15.715f, 13.749f)\n                curveTo(15.837f, 13.8f, 15.968f, 13.826f, 16.1f, 13.826f)\n                curveTo(16.232f, 13.826f, 16.363f, 13.8f, 16.485f, 13.749f)\n                curveTo(16.607f, 13.698f, 16.717f, 13.624f, 16.81f, 13.53f)\n                close()\n            }\n        }.build()\n\n        return _AddLink!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _AddLink: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Alphabet.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Alphabet: ImageVector\n    get() {\n        if (_Alphabet != null) {\n            return _Alphabet!!\n        }\n        _Alphabet = ImageVector.Builder(\n            name = \"Alphabet\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(9f, 12f)\n                curveTo(9f, 11.735f, 8.895f, 11.481f, 8.707f, 11.293f)\n                curveTo(8.519f, 11.105f, 8.265f, 11f, 8f, 11f)\n                horizontalLineTo(6f)\n                curveTo(5.448f, 11f, 5f, 10.552f, 5f, 10f)\n                curveTo(5f, 9.448f, 5.448f, 9f, 6f, 9f)\n                horizontalLineTo(8f)\n                curveTo(8.796f, 9f, 9.558f, 9.316f, 10.121f, 9.879f)\n                curveTo(10.684f, 10.441f, 11f, 11.204f, 11f, 12f)\n                verticalLineTo(17f)\n                curveTo(11f, 17.552f, 10.552f, 18f, 10f, 18f)\n                horizontalLineTo(7f)\n                curveTo(6.204f, 18f, 5.442f, 17.684f, 4.879f, 17.121f)\n                curveTo(4.316f, 16.559f, 4f, 15.796f, 4f, 15f)\n                curveTo(4f, 14.204f, 4.316f, 13.441f, 4.879f, 12.879f)\n                curveTo(5.442f, 12.316f, 6.204f, 12f, 7f, 12f)\n                horizontalLineTo(9f)\n                close()\n                moveTo(18f, 12f)\n                curveTo(18f, 11.735f, 17.895f, 11.481f, 17.707f, 11.293f)\n                curveTo(17.52f, 11.105f, 17.265f, 11f, 17f, 11f)\n                horizontalLineTo(16f)\n                curveTo(15.735f, 11f, 15.481f, 11.105f, 15.293f, 11.293f)\n                curveTo(15.105f, 11.481f, 15f, 11.735f, 15f, 12f)\n                verticalLineTo(15f)\n                curveTo(15f, 15.265f, 15.105f, 15.519f, 15.293f, 15.707f)\n                curveTo(15.481f, 15.895f, 15.735f, 16f, 16f, 16f)\n                horizontalLineTo(17f)\n                curveTo(17.265f, 16f, 17.52f, 15.895f, 17.707f, 15.707f)\n                curveTo(17.895f, 15.519f, 18f, 15.265f, 18f, 15f)\n                verticalLineTo(12f)\n                close()\n                moveTo(6.005f, 15.099f)\n                curveTo(6.028f, 15.328f, 6.129f, 15.543f, 6.293f, 15.707f)\n                curveTo(6.481f, 15.895f, 6.735f, 16f, 7f, 16f)\n                horizontalLineTo(9f)\n                verticalLineTo(14f)\n                horizontalLineTo(7f)\n                curveTo(6.735f, 14f, 6.481f, 14.105f, 6.293f, 14.293f)\n                curveTo(6.105f, 14.481f, 6f, 14.735f, 6f, 15f)\n                lineTo(6.005f, 15.099f)\n                close()\n                moveTo(20f, 15f)\n                curveTo(20f, 15.796f, 19.684f, 16.559f, 19.121f, 17.121f)\n                curveTo(18.559f, 17.684f, 17.796f, 18f, 17f, 18f)\n                horizontalLineTo(16f)\n                curveTo(15.548f, 18f, 15.109f, 17.895f, 14.709f, 17.704f)\n                curveTo(14.528f, 17.886f, 14.277f, 18f, 14f, 18f)\n                curveTo(13.448f, 18f, 13f, 17.552f, 13f, 17f)\n                verticalLineTo(7f)\n                curveTo(13f, 6.448f, 13.448f, 6f, 14f, 6f)\n                curveTo(14.552f, 6f, 15f, 6.448f, 15f, 7f)\n                verticalLineTo(9.175f)\n                curveTo(15.318f, 9.062f, 15.656f, 9f, 16f, 9f)\n                horizontalLineTo(17f)\n                curveTo(17.796f, 9f, 18.559f, 9.316f, 19.121f, 9.879f)\n                curveTo(19.684f, 10.441f, 20f, 11.204f, 20f, 12f)\n                verticalLineTo(15f)\n                close()\n            }\n        }.build()\n\n        return _Alphabet!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Alphabet: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/AppIcon.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.AppIcon: ImageVector\n    get() {\n        if (_AppIcon != null) {\n            return _AppIcon!!\n        }\n        _AppIcon = ImageVector.Builder(\n            name = \"AppIcon\",\n            defaultWidth = 48.dp,\n            defaultHeight = 48.dp,\n            viewportWidth = 48f,\n            viewportHeight = 48f\n        ).apply {\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFFC631FF),\n                        1f to Color(0xFF4DC4FE)\n                    ),\n                    start = Offset(50.062f, 23.014f),\n                    end = Offset(-1.681f, 23.014f)\n                ),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(26.89f, 0.892f)\n                curveTo(26.89f, 0.399f, 26.51f, 0f, 26.04f, 0f)\n                horizontalLineTo(21.96f)\n                curveTo(21.49f, 0f, 21.11f, 0.399f, 21.11f, 0.892f)\n                verticalLineTo(17.84f)\n                curveTo(21.11f, 18.333f, 20.729f, 18.732f, 20.26f, 18.732f)\n                horizontalLineTo(18.851f)\n                curveTo(18.16f, 18.732f, 17.758f, 19.552f, 18.16f, 20.143f)\n                lineTo(23.308f, 27.706f)\n                curveTo(23.647f, 28.205f, 24.353f, 28.205f, 24.692f, 27.706f)\n                lineTo(29.84f, 20.143f)\n                curveTo(30.242f, 19.552f, 29.84f, 18.732f, 29.149f, 18.732f)\n                horizontalLineTo(27.74f)\n                curveTo(27.271f, 18.732f, 26.89f, 18.333f, 26.89f, 17.84f)\n                verticalLineTo(0.892f)\n                close()\n                moveTo(1.827f, 33.184f)\n                curveTo(0.621f, 30.273f, 0f, 27.152f, 0f, 24f)\n                horizontalLineTo(7.201f)\n                curveTo(9.804f, 24f, 11.831f, 26.188f, 12.827f, 28.592f)\n                curveTo(13.43f, 30.048f, 14.314f, 31.371f, 15.428f, 32.485f)\n                curveTo(16.543f, 33.6f, 17.866f, 34.484f, 19.322f, 35.087f)\n                curveTo(20.777f, 35.69f, 22.338f, 36f, 23.914f, 36f)\n                curveTo(25.49f, 36f, 27.05f, 35.69f, 28.506f, 35.087f)\n                curveTo(29.962f, 34.484f, 31.285f, 33.6f, 32.399f, 32.485f)\n                curveTo(33.513f, 31.371f, 34.397f, 30.048f, 35f, 28.592f)\n                curveTo(35.996f, 26.188f, 38.023f, 24f, 40.626f, 24f)\n                horizontalLineTo(48f)\n                curveTo(48f, 27.152f, 47.379f, 30.273f, 46.173f, 33.184f)\n                curveTo(44.967f, 36.096f, 43.199f, 38.742f, 40.971f, 40.971f)\n                curveTo(38.742f, 43.199f, 36.096f, 44.967f, 33.184f, 46.173f)\n                curveTo(30.273f, 47.379f, 27.152f, 48f, 24f, 48f)\n                curveTo(20.848f, 48f, 17.727f, 47.379f, 14.816f, 46.173f)\n                curveTo(11.904f, 44.967f, 9.258f, 43.199f, 7.029f, 40.971f)\n                curveTo(4.801f, 38.742f, 3.033f, 36.096f, 1.827f, 33.184f)\n                close()\n                moveTo(11.772f, 5.211f)\n                curveTo(11.126f, 5.731f, 11.016f, 6.686f, 11.525f, 7.345f)\n                curveTo(12.035f, 8.003f, 12.971f, 8.116f, 13.617f, 7.596f)\n                lineTo(15.808f, 5.832f)\n                curveTo(16.454f, 5.312f, 16.564f, 4.357f, 16.055f, 3.698f)\n                curveTo(15.545f, 3.039f, 14.609f, 2.927f, 13.963f, 3.447f)\n                lineTo(11.772f, 5.211f)\n                close()\n                moveTo(36.468f, 5.211f)\n                curveTo(37.114f, 5.731f, 37.224f, 6.686f, 36.715f, 7.345f)\n                curveTo(36.205f, 8.003f, 35.269f, 8.116f, 34.623f, 7.596f)\n                lineTo(32.432f, 5.832f)\n                curveTo(31.786f, 5.312f, 31.676f, 4.357f, 32.185f, 3.698f)\n                curveTo(32.695f, 3.039f, 33.631f, 2.927f, 34.277f, 3.447f)\n                lineTo(36.468f, 5.211f)\n                close()\n                moveTo(4.543f, 17.654f)\n                curveTo(3.778f, 17.346f, 3.403f, 16.464f, 3.704f, 15.683f)\n                lineTo(4.728f, 13.033f)\n                curveTo(5.03f, 12.253f, 5.895f, 11.87f, 6.66f, 12.177f)\n                curveTo(7.425f, 12.485f, 7.8f, 13.368f, 7.499f, 14.148f)\n                lineTo(6.475f, 16.798f)\n                curveTo(6.173f, 17.578f, 5.308f, 17.962f, 4.543f, 17.654f)\n                close()\n                moveTo(44.536f, 15.683f)\n                curveTo(44.838f, 16.464f, 44.462f, 17.346f, 43.697f, 17.654f)\n                curveTo(42.932f, 17.962f, 42.067f, 17.578f, 41.765f, 16.798f)\n                lineTo(40.741f, 14.148f)\n                curveTo(40.44f, 13.368f, 40.815f, 12.485f, 41.58f, 12.177f)\n                curveTo(42.345f, 11.87f, 43.21f, 12.253f, 43.512f, 13.033f)\n                lineTo(44.536f, 15.683f)\n                close()\n            }\n            path(\n                fill = SolidColor(Color.Black),\n                fillAlpha = 0.25f\n            ) {\n                moveTo(40.97f, 40.97f)\n                curveTo(36.47f, 45.471f, 30.365f, 48f, 24f, 48f)\n                curveTo(17.635f, 48f, 11.53f, 45.471f, 7.029f, 40.97f)\n                verticalLineTo(40.97f)\n                curveTo(9.558f, 38.441f, 13.649f, 38.586f, 16.921f, 40.03f)\n                curveTo(19.13f, 41.006f, 21.538f, 41.524f, 24f, 41.524f)\n                curveTo(26.462f, 41.524f, 28.87f, 41.006f, 31.079f, 40.03f)\n                curveTo(34.351f, 38.586f, 38.441f, 38.441f, 40.97f, 40.97f)\n                verticalLineTo(40.97f)\n                close()\n            }\n        }.build()\n\n        return _AppIcon!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _AppIcon: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Back.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Back: ImageVector\n    get() {\n        if (_Back != null) {\n            return _Back!!\n        }\n        _Back = ImageVector.Builder(\n            name = \"Back\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(15.707f, 5.293f)\n                curveTo(16.098f, 5.683f, 16.098f, 6.317f, 15.707f, 6.707f)\n                lineTo(10.414f, 12f)\n                lineTo(15.707f, 17.293f)\n                curveTo(16.098f, 17.683f, 16.098f, 18.317f, 15.707f, 18.707f)\n                curveTo(15.317f, 19.098f, 14.683f, 19.098f, 14.293f, 18.707f)\n                lineTo(8.293f, 12.707f)\n                curveTo(7.902f, 12.317f, 7.902f, 11.683f, 8.293f, 11.293f)\n                lineTo(14.293f, 5.293f)\n                curveTo(14.683f, 4.902f, 15.317f, 4.902f, 15.707f, 5.293f)\n                close()\n            }\n        }.build()\n\n        return _Back!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Back: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/BrowserGoogleChrome.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.BrowserGoogleChrome: ImageVector\n    get() {\n        if (_BrowserGoogleChrome != null) {\n            return _BrowserGoogleChrome!!\n        }\n        _BrowserGoogleChrome = ImageVector.Builder(\n            name = \"BrowserGoogleChrome\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(12f, 18.677f)\n                curveTo(15.688f, 18.677f, 18.677f, 15.687f, 18.677f, 11.999f)\n                curveTo(18.677f, 8.312f, 15.688f, 5.322f, 12f, 5.322f)\n                curveTo(8.313f, 5.322f, 5.323f, 8.312f, 5.323f, 11.999f)\n                curveTo(5.323f, 15.687f, 8.313f, 18.677f, 12f, 18.677f)\n            }\n            path(fill = SolidColor(Color(0xFF229342))) {\n                moveTo(3.365f, 8.718f)\n                curveTo(2.867f, 7.856f, 2.281f, 6.95f, 1.608f, 6.002f)\n                curveTo(0.555f, 7.826f, 0f, 9.895f, 0f, 12.002f)\n                curveTo(0f, 14.108f, 0.555f, 16.178f, 1.608f, 18.002f)\n                curveTo(2.661f, 19.826f, 4.176f, 21.341f, 6.001f, 22.394f)\n                curveTo(7.825f, 23.447f, 9.895f, 24.001f, 12.001f, 24f)\n                curveTo(13.105f, 22.451f, 13.855f, 21.334f, 14.251f, 20.649f)\n                curveTo(15.01f, 19.334f, 15.992f, 17.451f, 17.197f, 15.001f)\n                verticalLineTo(15f)\n                curveTo(16.67f, 15.912f, 15.913f, 16.67f, 15.001f, 17.197f)\n                curveTo(14.089f, 17.724f, 13.054f, 18.001f, 12.001f, 18.001f)\n                curveTo(10.947f, 18.002f, 9.912f, 17.724f, 9f, 17.198f)\n                curveTo(8.088f, 16.671f, 7.33f, 15.913f, 6.804f, 15.001f)\n                curveTo(5.168f, 11.95f, 4.021f, 9.855f, 3.365f, 8.718f)\n                close()\n            }\n            path(fill = SolidColor(Color(0xFFFBC116))) {\n                moveTo(12.001f, 24f)\n                curveTo(13.577f, 24f, 15.137f, 23.69f, 16.593f, 23.087f)\n                curveTo(18.049f, 22.484f, 19.372f, 21.6f, 20.486f, 20.486f)\n                curveTo(21.601f, 19.371f, 22.484f, 18.048f, 23.087f, 16.592f)\n                curveTo(23.69f, 15.136f, 24f, 13.576f, 24f, 12f)\n                curveTo(24f, 9.893f, 23.444f, 7.824f, 22.391f, 6f)\n                curveTo(20.118f, 5.776f, 18.44f, 5.664f, 17.358f, 5.664f)\n                curveTo(16.131f, 5.664f, 14.345f, 5.776f, 12f, 6f)\n                lineTo(11.998f, 6.001f)\n                curveTo(13.052f, 6f, 14.087f, 6.277f, 14.999f, 6.804f)\n                curveTo(15.912f, 7.33f, 16.67f, 8.087f, 17.196f, 9f)\n                curveTo(17.723f, 9.912f, 18.001f, 10.947f, 18.001f, 12f)\n                curveTo(18.001f, 13.054f, 17.723f, 14.088f, 17.196f, 15.001f)\n                lineTo(12.001f, 24f)\n                close()\n            }\n            path(fill = SolidColor(Color(0xFF1A73E8))) {\n                moveTo(12f, 16.751f)\n                curveTo(14.624f, 16.751f, 16.75f, 14.624f, 16.75f, 12.001f)\n                curveTo(16.75f, 9.377f, 14.624f, 7.25f, 12f, 7.25f)\n                curveTo(9.377f, 7.25f, 7.25f, 9.377f, 7.25f, 12.001f)\n                curveTo(7.25f, 14.624f, 9.377f, 16.751f, 12f, 16.751f)\n                close()\n            }\n            path(fill = SolidColor(Color(0xFFE33B2E))) {\n                moveTo(12f, 6f)\n                horizontalLineTo(22.391f)\n                curveTo(21.338f, 4.176f, 19.824f, 2.661f, 17.999f, 1.608f)\n                curveTo(16.175f, 0.554f, 14.106f, -0f, 11.999f, -0f)\n                curveTo(9.893f, 0f, 7.824f, 0.555f, 6f, 1.608f)\n                curveTo(4.175f, 2.662f, 2.661f, 4.177f, 1.608f, 6.002f)\n                lineTo(6.804f, 15.001f)\n                lineTo(6.805f, 15.002f)\n                curveTo(6.278f, 14.09f, 6f, 13.055f, 6f, 12.001f)\n                curveTo(6f, 10.948f, 6.277f, 9.913f, 6.803f, 9.001f)\n                curveTo(7.33f, 8.088f, 8.088f, 7.331f, 9f, 6.804f)\n                curveTo(9.912f, 6.277f, 10.947f, 6f, 12f, 6f)\n                lineTo(12f, 6f)\n                close()\n            }\n        }.build()\n\n        return _BrowserGoogleChrome!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _BrowserGoogleChrome: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/BrowserMicrosoftEdge.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.BrowserMicrosoftEdge: ImageVector\n    get() {\n        if (_BrowserMicrosoftEdge != null) {\n            return _BrowserMicrosoftEdge!!\n        }\n        _BrowserMicrosoftEdge = ImageVector.Builder(\n            name = \"BrowserMicrosoftEdge\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF0C59A4),\n                        1f to Color(0xFF114A8B)\n                    ),\n                    start = Offset(5.504f, 16.596f),\n                    end = Offset(22.218f, 16.596f)\n                )\n            ) {\n                moveTo(21.656f, 17.859f)\n                curveTo(21.337f, 18.028f, 21.009f, 18.178f, 20.672f, 18.3f)\n                curveTo(19.594f, 18.703f, 18.459f, 18.909f, 17.306f, 18.909f)\n                curveTo(12.872f, 18.909f, 9.009f, 15.863f, 9.009f, 11.944f)\n                curveTo(9.019f, 10.875f, 9.609f, 9.891f, 10.547f, 9.384f)\n                curveTo(6.534f, 9.553f, 5.503f, 13.734f, 5.503f, 16.181f)\n                curveTo(5.503f, 23.109f, 11.887f, 23.813f, 13.266f, 23.813f)\n                curveTo(14.006f, 23.813f, 15.122f, 23.597f, 15.797f, 23.381f)\n                lineTo(15.919f, 23.344f)\n                curveTo(18.506f, 22.453f, 20.7f, 20.709f, 22.163f, 18.394f)\n                curveTo(22.275f, 18.216f, 22.219f, 17.991f, 22.05f, 17.878f)\n                curveTo(21.928f, 17.803f, 21.778f, 17.794f, 21.656f, 17.859f)\n                close()\n            }\n            path(\n                fill = Brush.radialGradient(\n                    colorStops = arrayOf(\n                        0.72f to Color.Transparent.copy(alpha = 0f),\n                        0.95f to Color.Black.copy(alpha = 0.5294118f),\n                        1f to Color.Black\n                    ),\n                    center = Offset(14.737f, 16.728f),\n                    radius = 8.941f\n                ),\n                fillAlpha = 0.35f,\n                strokeAlpha = 0.35f\n            ) {\n                moveTo(21.656f, 17.859f)\n                curveTo(21.337f, 18.028f, 21.009f, 18.178f, 20.672f, 18.3f)\n                curveTo(19.594f, 18.703f, 18.459f, 18.909f, 17.306f, 18.909f)\n                curveTo(12.872f, 18.909f, 9.009f, 15.863f, 9.009f, 11.944f)\n                curveTo(9.019f, 10.875f, 9.609f, 9.891f, 10.547f, 9.384f)\n                curveTo(6.534f, 9.553f, 5.503f, 13.734f, 5.503f, 16.181f)\n                curveTo(5.503f, 23.109f, 11.887f, 23.813f, 13.266f, 23.813f)\n                curveTo(14.006f, 23.813f, 15.122f, 23.597f, 15.797f, 23.381f)\n                lineTo(15.919f, 23.344f)\n                curveTo(18.506f, 22.453f, 20.7f, 20.709f, 22.163f, 18.394f)\n                curveTo(22.275f, 18.216f, 22.219f, 17.991f, 22.05f, 17.878f)\n                curveTo(21.928f, 17.803f, 21.778f, 17.794f, 21.656f, 17.859f)\n                close()\n            }\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF1B9DE2),\n                        0.16f to Color(0xFF1595DF),\n                        0.67f to Color(0xFF0680D7),\n                        1f to Color(0xFF0078D4)\n                    ),\n                    start = Offset(14.322f, 9.351f),\n                    end = Offset(3.881f, 20.724f)\n                )\n            ) {\n                moveTo(9.909f, 22.631f)\n                curveTo(9.075f, 22.116f, 8.353f, 21.431f, 7.781f, 20.634f)\n                curveTo(5.316f, 17.259f, 6.056f, 12.525f, 9.431f, 10.059f)\n                curveTo(9.788f, 9.806f, 10.153f, 9.572f, 10.547f, 9.384f)\n                curveTo(10.838f, 9.244f, 11.334f, 9f, 12f, 9.009f)\n                curveTo(12.947f, 9.019f, 13.838f, 9.469f, 14.409f, 10.228f)\n                curveTo(14.784f, 10.734f, 15f, 11.344f, 15.009f, 11.981f)\n                curveTo(15.009f, 11.962f, 17.306f, 4.519f, 7.509f, 4.519f)\n                curveTo(3.394f, 4.519f, 0.009f, 8.428f, 0.009f, 11.85f)\n                curveTo(-0.009f, 13.659f, 0.384f, 15.459f, 1.144f, 17.1f)\n                curveTo(3.731f, 22.612f, 10.031f, 25.313f, 15.806f, 23.391f)\n                curveTo(13.828f, 24.009f, 11.672f, 23.737f, 9.909f, 22.631f)\n                close()\n            }\n            path(\n                fill = Brush.radialGradient(\n                    colorStops = arrayOf(\n                        0.76f to Color.Transparent.copy(alpha = 0f),\n                        0.95f to Color.Black.copy(alpha = 0.49803922f),\n                        1f to Color.Black\n                    ),\n                    center = Offset(6.62f, 18.657f),\n                    radius = 13.443f\n                ),\n                fillAlpha = 0.41f,\n                strokeAlpha = 0.41f\n            ) {\n                moveTo(9.909f, 22.631f)\n                curveTo(9.075f, 22.116f, 8.353f, 21.431f, 7.781f, 20.634f)\n                curveTo(5.316f, 17.259f, 6.056f, 12.525f, 9.431f, 10.059f)\n                curveTo(9.788f, 9.806f, 10.153f, 9.572f, 10.547f, 9.384f)\n                curveTo(10.838f, 9.244f, 11.334f, 9f, 12f, 9.009f)\n                curveTo(12.947f, 9.019f, 13.838f, 9.469f, 14.409f, 10.228f)\n                curveTo(14.784f, 10.734f, 15f, 11.344f, 15.009f, 11.981f)\n                curveTo(15.009f, 11.962f, 17.306f, 4.519f, 7.509f, 4.519f)\n                curveTo(3.394f, 4.519f, 0.009f, 8.428f, 0.009f, 11.85f)\n                curveTo(-0.009f, 13.659f, 0.384f, 15.459f, 1.144f, 17.1f)\n                curveTo(3.731f, 22.612f, 10.031f, 25.313f, 15.806f, 23.391f)\n                curveTo(13.828f, 24.009f, 11.672f, 23.737f, 9.909f, 22.631f)\n                close()\n            }\n            path(\n                fill = Brush.radialGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF35C1F1),\n                        0.11f to Color(0xFF34C1ED),\n                        0.23f to Color(0xFF2FC2DF),\n                        0.31f to Color(0xFF2BC3D2),\n                        0.67f to Color(0xFF36C752)\n                    ),\n                    center = Offset(2.424f, 4.443f),\n                    radius = 18.989f\n                )\n            ) {\n                moveTo(14.278f, 13.959f)\n                curveTo(14.203f, 14.053f, 13.969f, 14.194f, 13.969f, 14.494f)\n                curveTo(13.969f, 14.738f, 14.128f, 14.972f, 14.409f, 15.169f)\n                curveTo(15.759f, 16.106f, 18.3f, 15.984f, 18.309f, 15.984f)\n                curveTo(19.313f, 15.984f, 20.288f, 15.712f, 21.15f, 15.206f)\n                curveTo(22.913f, 14.175f, 24f, 12.291f, 24f, 10.247f)\n                curveTo(24.028f, 8.147f, 23.25f, 6.75f, 22.941f, 6.131f)\n                curveTo(20.953f, 2.241f, 16.659f, 0f, 12f, 0f)\n                curveTo(5.438f, 0f, 0.094f, 5.269f, 0f, 11.831f)\n                curveTo(0.047f, 8.409f, 3.45f, 5.644f, 7.5f, 5.644f)\n                curveTo(7.828f, 5.644f, 9.703f, 5.672f, 11.438f, 6.591f)\n                curveTo(12.966f, 7.397f, 13.772f, 8.363f, 14.325f, 9.328f)\n                curveTo(14.906f, 10.331f, 15.009f, 11.587f, 15.009f, 12.094f)\n                curveTo(15.009f, 12.591f, 14.756f, 13.341f, 14.278f, 13.959f)\n                close()\n            }\n            path(\n                fill = Brush.radialGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF66EB6E),\n                        1f to Color(0x0066EB6E)\n                    ),\n                    center = Offset(22.506f, 7.257f),\n                    radius = 9.124f\n                )\n            ) {\n                moveTo(14.278f, 13.959f)\n                curveTo(14.203f, 14.053f, 13.969f, 14.194f, 13.969f, 14.494f)\n                curveTo(13.969f, 14.738f, 14.128f, 14.972f, 14.409f, 15.169f)\n                curveTo(15.759f, 16.106f, 18.3f, 15.984f, 18.309f, 15.984f)\n                curveTo(19.313f, 15.984f, 20.288f, 15.712f, 21.15f, 15.206f)\n                curveTo(22.913f, 14.175f, 24f, 12.291f, 24f, 10.247f)\n                curveTo(24.028f, 8.147f, 23.25f, 6.75f, 22.941f, 6.131f)\n                curveTo(20.953f, 2.241f, 16.659f, 0f, 12f, 0f)\n                curveTo(5.438f, 0f, 0.094f, 5.269f, 0f, 11.831f)\n                curveTo(0.047f, 8.409f, 3.45f, 5.644f, 7.5f, 5.644f)\n                curveTo(7.828f, 5.644f, 9.703f, 5.672f, 11.438f, 6.591f)\n                curveTo(12.966f, 7.397f, 13.772f, 8.363f, 14.325f, 9.328f)\n                curveTo(14.906f, 10.331f, 15.009f, 11.587f, 15.009f, 12.094f)\n                curveTo(15.009f, 12.591f, 14.756f, 13.341f, 14.278f, 13.959f)\n                close()\n            }\n        }.build()\n\n        return _BrowserMicrosoftEdge!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _BrowserMicrosoftEdge: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/BrowserMozillaFirefox.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.PathData\nimport androidx.compose.ui.graphics.vector.group\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.BrowserMozillaFirefox: ImageVector\n    get() {\n        if (_BrowserMozillaFirefox != null) {\n            return _BrowserMozillaFirefox!!\n        }\n        _BrowserMozillaFirefox = ImageVector.Builder(\n            name = \"BrowserMozillaFirefox\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            group(\n                clipPathData = PathData {\n                    moveTo(0f, 0f)\n                    horizontalLineToRelative(24f)\n                    verticalLineToRelative(24f)\n                    horizontalLineToRelative(-24f)\n                    close()\n                }\n            ) {\n                path(\n                    fill = Brush.linearGradient(\n                        colorStops = arrayOf(\n                            0.048f to Color(0xFFFFF44F),\n                            0.111f to Color(0xFFFFE847),\n                            0.225f to Color(0xFFFFC830),\n                            0.368f to Color(0xFFFF980E),\n                            0.401f to Color(0xFFFF8B16),\n                            0.462f to Color(0xFFFF672A),\n                            0.534f to Color(0xFFFF3647),\n                            0.705f to Color(0xFFE31587)\n                        ),\n                        start = Offset(21.172f, 3.71f),\n                        end = Offset(1.904f, 22.3f)\n                    )\n                ) {\n                    moveTo(22.708f, 8.034f)\n                    curveTo(22.204f, 6.82f, 21.181f, 5.51f, 20.379f, 5.096f)\n                    curveTo(20.951f, 6.203f, 21.347f, 7.391f, 21.555f, 8.619f)\n                    lineTo(21.557f, 8.639f)\n                    curveTo(20.245f, 5.367f, 18.019f, 4.048f, 16.202f, 1.175f)\n                    curveTo(16.108f, 1.029f, 16.017f, 0.88f, 15.928f, 0.731f)\n                    curveTo(15.883f, 0.652f, 15.84f, 0.572f, 15.801f, 0.491f)\n                    curveTo(15.725f, 0.345f, 15.667f, 0.191f, 15.627f, 0.031f)\n                    curveTo(15.627f, 0.024f, 15.625f, 0.017f, 15.62f, 0.011f)\n                    curveTo(15.615f, 0.006f, 15.608f, 0.002f, 15.601f, 0.001f)\n                    curveTo(15.594f, -0f, 15.586f, -0f, 15.579f, 0.001f)\n                    curveTo(15.578f, 0.001f, 15.575f, 0.004f, 15.573f, 0.005f)\n                    curveTo(15.572f, 0.005f, 15.568f, 0.008f, 15.565f, 0.009f)\n                    lineTo(15.57f, 0.001f)\n                    curveTo(12.654f, 1.708f, 11.665f, 4.868f, 11.574f, 6.449f)\n                    curveTo(10.41f, 6.529f, 9.297f, 6.958f, 8.38f, 7.68f)\n                    curveTo(8.284f, 7.599f, 8.184f, 7.523f, 8.08f, 7.453f)\n                    curveTo(7.816f, 6.528f, 7.805f, 5.548f, 8.048f, 4.616f)\n                    curveTo(6.977f, 5.135f, 6.026f, 5.87f, 5.254f, 6.775f)\n                    horizontalLineTo(5.249f)\n                    curveTo(4.789f, 6.192f, 4.821f, 4.27f, 4.847f, 3.868f)\n                    curveTo(4.711f, 3.923f, 4.581f, 3.992f, 4.46f, 4.074f)\n                    curveTo(4.054f, 4.364f, 3.674f, 4.689f, 3.325f, 5.046f)\n                    curveTo(2.928f, 5.449f, 2.565f, 5.884f, 2.24f, 6.348f)\n                    verticalLineTo(6.349f)\n                    verticalLineTo(6.347f)\n                    curveTo(1.494f, 7.405f, 0.965f, 8.6f, 0.683f, 9.864f)\n                    lineTo(0.668f, 9.941f)\n                    curveTo(0.625f, 10.181f, 0.587f, 10.423f, 0.553f, 10.665f)\n                    curveTo(0.553f, 10.674f, 0.552f, 10.682f, 0.551f, 10.691f)\n                    curveTo(0.449f, 11.219f, 0.386f, 11.754f, 0.362f, 12.291f)\n                    verticalLineTo(12.351f)\n                    curveTo(0.37f, 21.286f, 10.048f, 26.862f, 17.782f, 22.388f)\n                    curveTo(19.254f, 21.536f, 20.521f, 20.372f, 21.493f, 18.976f)\n                    curveTo(22.465f, 17.581f, 23.119f, 15.989f, 23.408f, 14.314f)\n                    curveTo(23.428f, 14.164f, 23.443f, 14.016f, 23.461f, 13.864f)\n                    curveTo(23.7f, 11.889f, 23.441f, 9.884f, 22.708f, 8.034f)\n                    close()\n                    moveTo(9.33f, 17.119f)\n                    curveTo(9.385f, 17.145f, 9.435f, 17.174f, 9.491f, 17.198f)\n                    lineTo(9.499f, 17.204f)\n                    curveTo(9.443f, 17.176f, 9.386f, 17.148f, 9.33f, 17.119f)\n                    close()\n                    moveTo(21.558f, 8.641f)\n                    verticalLineTo(8.63f)\n                    lineTo(21.56f, 8.642f)\n                    lineTo(21.558f, 8.641f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.129f to Color(0xFFFFBD4F),\n                            0.186f to Color(0xFFFFAC31),\n                            0.247f to Color(0xFFFF9D17),\n                            0.283f to Color(0xFFFF980E),\n                            0.403f to Color(0xFFFF563B),\n                            0.467f to Color(0xFFFF3750),\n                            0.71f to Color(0xFFF5156C),\n                            0.782f to Color(0xFFEB0878),\n                            0.86f to Color(0xFFE50080)\n                        ),\n                        center = Offset(20.282f, 2.658f),\n                        radius = 24.197f\n                    )\n                ) {\n                    moveTo(22.708f, 8.034f)\n                    curveTo(22.204f, 6.82f, 21.181f, 5.51f, 20.379f, 5.096f)\n                    curveTo(20.951f, 6.203f, 21.347f, 7.391f, 21.555f, 8.619f)\n                    verticalLineTo(8.63f)\n                    lineTo(21.557f, 8.642f)\n                    curveTo(22.452f, 11.203f, 22.322f, 14.009f, 21.196f, 16.476f)\n                    curveTo(19.866f, 19.33f, 16.646f, 22.256f, 11.605f, 22.114f)\n                    curveTo(6.16f, 21.959f, 1.363f, 17.918f, 0.467f, 12.625f)\n                    curveTo(0.304f, 11.791f, 0.467f, 11.368f, 0.549f, 10.689f)\n                    curveTo(0.437f, 11.216f, 0.374f, 11.752f, 0.362f, 12.291f)\n                    verticalLineTo(12.351f)\n                    curveTo(0.37f, 21.286f, 10.048f, 26.862f, 17.782f, 22.388f)\n                    curveTo(19.254f, 21.536f, 20.521f, 20.372f, 21.493f, 18.976f)\n                    curveTo(22.465f, 17.581f, 23.119f, 15.989f, 23.408f, 14.314f)\n                    curveTo(23.428f, 14.164f, 23.443f, 14.016f, 23.461f, 13.864f)\n                    curveTo(23.7f, 11.889f, 23.441f, 9.884f, 22.708f, 8.034f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.3f to Color(0xFF960E18),\n                            0.351f to Color(0xBCB11927),\n                            0.435f to Color(0x57DB293D),\n                            0.497f to Color(0x17F5334B),\n                            0.53f to Color(0x00FF3750)\n                        ),\n                        center = Offset(11.44f, 12.55f),\n                        radius = 24.197f\n                    )\n                ) {\n                    moveTo(22.708f, 8.034f)\n                    curveTo(22.204f, 6.82f, 21.181f, 5.51f, 20.379f, 5.096f)\n                    curveTo(20.951f, 6.203f, 21.347f, 7.391f, 21.555f, 8.619f)\n                    verticalLineTo(8.63f)\n                    lineTo(21.557f, 8.642f)\n                    curveTo(22.452f, 11.203f, 22.322f, 14.009f, 21.196f, 16.476f)\n                    curveTo(19.866f, 19.33f, 16.646f, 22.256f, 11.605f, 22.114f)\n                    curveTo(6.16f, 21.959f, 1.363f, 17.918f, 0.467f, 12.625f)\n                    curveTo(0.304f, 11.791f, 0.467f, 11.368f, 0.549f, 10.689f)\n                    curveTo(0.437f, 11.216f, 0.374f, 11.752f, 0.362f, 12.291f)\n                    verticalLineTo(12.351f)\n                    curveTo(0.37f, 21.286f, 10.048f, 26.862f, 17.782f, 22.388f)\n                    curveTo(19.254f, 21.536f, 20.521f, 20.372f, 21.493f, 18.976f)\n                    curveTo(22.465f, 17.581f, 23.119f, 15.989f, 23.408f, 14.314f)\n                    curveTo(23.428f, 14.164f, 23.443f, 14.016f, 23.461f, 13.864f)\n                    curveTo(23.7f, 11.889f, 23.441f, 9.884f, 22.708f, 8.034f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.132f to Color(0xFFFFF44F),\n                            0.252f to Color(0xFFFFDC3E),\n                            0.506f to Color(0xFFFF9D12),\n                            0.526f to Color(0xFFFF980E)\n                        ),\n                        center = Offset(14.357f, -2.833f),\n                        radius = 17.529f\n                    )\n                ) {\n                    moveTo(17.067f, 9.398f)\n                    curveTo(17.093f, 9.416f, 17.116f, 9.434f, 17.14f, 9.451f)\n                    curveTo(16.848f, 8.934f, 16.485f, 8.461f, 16.062f, 8.045f)\n                    curveTo(12.454f, 4.437f, 15.116f, 0.222f, 15.565f, 0.008f)\n                    lineTo(15.57f, 0.001f)\n                    curveTo(12.654f, 1.708f, 11.665f, 4.868f, 11.574f, 6.449f)\n                    curveTo(11.709f, 6.44f, 11.844f, 6.428f, 11.982f, 6.428f)\n                    curveTo(13.016f, 6.43f, 14.032f, 6.706f, 14.925f, 7.228f)\n                    curveTo(15.818f, 7.749f, 16.558f, 8.498f, 17.067f, 9.398f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.353f to Color(0xFF3A8EE6),\n                            0.472f to Color(0xFF5C79F0),\n                            0.669f to Color(0xFF9059FF),\n                            1f to Color(0xFFC139E6)\n                        ),\n                        center = Offset(8.763f, 18.871f),\n                        radius = 11.521f\n                    )\n                ) {\n                    moveTo(11.989f, 10.119f)\n                    curveTo(11.97f, 10.408f, 10.95f, 11.403f, 10.594f, 11.403f)\n                    curveTo(7.293f, 11.403f, 6.757f, 13.4f, 6.757f, 13.4f)\n                    curveTo(6.903f, 15.081f, 8.075f, 16.466f, 9.491f, 17.198f)\n                    curveTo(9.556f, 17.232f, 9.621f, 17.262f, 9.687f, 17.292f)\n                    curveTo(9.799f, 17.342f, 9.913f, 17.388f, 10.028f, 17.431f)\n                    curveTo(10.514f, 17.603f, 11.023f, 17.702f, 11.538f, 17.723f)\n                    curveTo(17.323f, 17.994f, 18.444f, 10.805f, 14.269f, 8.719f)\n                    curveTo(15.254f, 8.591f, 16.251f, 8.833f, 17.067f, 9.398f)\n                    curveTo(16.558f, 8.498f, 15.818f, 7.749f, 14.925f, 7.228f)\n                    curveTo(14.032f, 6.706f, 13.016f, 6.43f, 11.982f, 6.428f)\n                    curveTo(11.844f, 6.428f, 11.709f, 6.44f, 11.574f, 6.449f)\n                    curveTo(10.41f, 6.529f, 9.297f, 6.958f, 8.38f, 7.68f)\n                    curveTo(8.557f, 7.83f, 8.757f, 8.03f, 9.177f, 8.445f)\n                    curveTo(9.965f, 9.221f, 11.985f, 10.024f, 11.989f, 10.119f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.206f to Color(0x009059FF),\n                            0.278f to Color(0x108C4FF3),\n                            0.747f to Color(0x727716A8),\n                            0.975f to Color(0x996E008B)\n                        ),\n                        center = Offset(12.766f, 10.568f),\n                        radius = 6.108f\n                    )\n                ) {\n                    moveTo(11.989f, 10.119f)\n                    curveTo(11.97f, 10.408f, 10.95f, 11.403f, 10.594f, 11.403f)\n                    curveTo(7.293f, 11.403f, 6.757f, 13.4f, 6.757f, 13.4f)\n                    curveTo(6.903f, 15.081f, 8.075f, 16.466f, 9.491f, 17.198f)\n                    curveTo(9.556f, 17.232f, 9.621f, 17.262f, 9.687f, 17.292f)\n                    curveTo(9.799f, 17.342f, 9.913f, 17.388f, 10.028f, 17.431f)\n                    curveTo(10.514f, 17.603f, 11.023f, 17.702f, 11.538f, 17.723f)\n                    curveTo(17.323f, 17.994f, 18.444f, 10.805f, 14.269f, 8.719f)\n                    curveTo(15.254f, 8.591f, 16.251f, 8.833f, 17.067f, 9.398f)\n                    curveTo(16.558f, 8.498f, 15.818f, 7.749f, 14.925f, 7.228f)\n                    curveTo(14.032f, 6.706f, 13.016f, 6.43f, 11.982f, 6.428f)\n                    curveTo(11.844f, 6.428f, 11.709f, 6.44f, 11.574f, 6.449f)\n                    curveTo(10.41f, 6.529f, 9.297f, 6.958f, 8.38f, 7.68f)\n                    curveTo(8.557f, 7.83f, 8.757f, 8.03f, 9.177f, 8.445f)\n                    curveTo(9.965f, 9.221f, 11.985f, 10.024f, 11.989f, 10.119f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0f to Color(0xFFFFE226),\n                            0.121f to Color(0xFFFFDB27),\n                            0.295f to Color(0xFFFFC82A),\n                            0.502f to Color(0xFFFFA930),\n                            0.732f to Color(0xFFFF7E37),\n                            0.792f to Color(0xFFFF7139)\n                        ),\n                        center = Offset(11.134f, 1.668f),\n                        radius = 8.288f\n                    )\n                ) {\n                    moveTo(7.839f, 7.294f)\n                    curveTo(7.92f, 7.346f, 7.999f, 7.399f, 8.078f, 7.453f)\n                    curveTo(7.814f, 6.528f, 7.802f, 5.548f, 8.046f, 4.616f)\n                    curveTo(6.975f, 5.135f, 6.024f, 5.87f, 5.252f, 6.776f)\n                    curveTo(5.308f, 6.774f, 6.992f, 6.744f, 7.839f, 7.294f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.113f to Color(0xFFFFF44F),\n                            0.456f to Color(0xFFFF980E),\n                            0.622f to Color(0xFFFF5634),\n                            0.716f to Color(0xFFFF3647),\n                            0.904f to Color(0xFFE31587)\n                        ),\n                        center = Offset(17.649f, -3.589f),\n                        radius = 35.362f\n                    )\n                ) {\n                    moveTo(0.468f, 12.625f)\n                    curveTo(1.364f, 17.918f, 6.161f, 21.959f, 11.607f, 22.114f)\n                    curveTo(16.647f, 22.256f, 19.867f, 19.33f, 21.197f, 16.476f)\n                    curveTo(22.324f, 14.009f, 22.453f, 11.203f, 21.559f, 8.642f)\n                    verticalLineTo(8.631f)\n                    curveTo(21.559f, 8.623f, 21.557f, 8.618f, 21.559f, 8.62f)\n                    lineTo(21.561f, 8.64f)\n                    curveTo(21.972f, 11.328f, 20.605f, 13.933f, 18.467f, 15.694f)\n                    lineTo(18.461f, 15.709f)\n                    curveTo(14.296f, 19.101f, 10.31f, 17.755f, 9.503f, 17.206f)\n                    curveTo(9.446f, 17.179f, 9.39f, 17.151f, 9.334f, 17.122f)\n                    curveTo(6.905f, 15.961f, 5.902f, 13.749f, 6.117f, 11.851f)\n                    curveTo(5.541f, 11.86f, 4.974f, 11.701f, 4.486f, 11.394f)\n                    curveTo(3.998f, 11.087f, 3.61f, 10.645f, 3.368f, 10.122f)\n                    curveTo(4.005f, 9.731f, 4.732f, 9.511f, 5.479f, 9.481f)\n                    curveTo(6.226f, 9.451f, 6.968f, 9.612f, 7.635f, 9.951f)\n                    curveTo(9.01f, 10.575f, 10.574f, 10.636f, 11.993f, 10.122f)\n                    curveTo(11.988f, 10.027f, 9.969f, 9.223f, 9.181f, 8.448f)\n                    curveTo(8.76f, 8.033f, 8.56f, 7.833f, 8.383f, 7.683f)\n                    curveTo(8.288f, 7.602f, 8.188f, 7.526f, 8.084f, 7.456f)\n                    curveTo(8.015f, 7.409f, 7.937f, 7.358f, 7.844f, 7.297f)\n                    curveTo(6.998f, 6.747f, 5.314f, 6.777f, 5.258f, 6.778f)\n                    horizontalLineTo(5.253f)\n                    curveTo(4.793f, 6.195f, 4.825f, 4.273f, 4.852f, 3.871f)\n                    curveTo(4.716f, 3.926f, 4.586f, 3.995f, 4.464f, 4.077f)\n                    curveTo(4.058f, 4.367f, 3.678f, 4.692f, 3.33f, 5.049f)\n                    curveTo(2.931f, 5.45f, 2.567f, 5.885f, 2.24f, 6.348f)\n                    verticalLineTo(6.349f)\n                    verticalLineTo(6.347f)\n                    curveTo(1.494f, 7.405f, 0.965f, 8.6f, 0.683f, 9.864f)\n                    curveTo(0.677f, 9.888f, 0.265f, 11.69f, 0.468f, 12.625f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0f to Color(0xFFFFF44F),\n                            0.06f to Color(0xFFFFE847),\n                            0.168f to Color(0xFFFFC830),\n                            0.304f to Color(0xFFFF980E),\n                            0.356f to Color(0xFFFF8B16),\n                            0.455f to Color(0xFFFF672A),\n                            0.57f to Color(0xFFFF3647),\n                            0.737f to Color(0xFFE31587)\n                        ),\n                        center = Offset(14.674f, -1.623f),\n                        radius = 25.918f\n                    )\n                ) {\n                    moveTo(16.062f, 8.045f)\n                    curveTo(16.486f, 8.461f, 16.849f, 8.935f, 17.14f, 9.453f)\n                    curveTo(17.201f, 9.498f, 17.258f, 9.545f, 17.314f, 9.595f)\n                    curveTo(19.946f, 12.021f, 18.567f, 15.45f, 18.464f, 15.694f)\n                    curveTo(20.602f, 13.933f, 21.968f, 11.328f, 21.558f, 8.64f)\n                    curveTo(20.245f, 5.367f, 18.019f, 4.048f, 16.202f, 1.175f)\n                    curveTo(16.108f, 1.029f, 16.017f, 0.88f, 15.928f, 0.731f)\n                    curveTo(15.883f, 0.652f, 15.84f, 0.572f, 15.8f, 0.491f)\n                    curveTo(15.725f, 0.345f, 15.667f, 0.191f, 15.627f, 0.031f)\n                    curveTo(15.627f, 0.024f, 15.625f, 0.017f, 15.62f, 0.011f)\n                    curveTo(15.615f, 0.006f, 15.608f, 0.002f, 15.601f, 0.001f)\n                    curveTo(15.594f, -0f, 15.586f, -0f, 15.579f, 0.001f)\n                    curveTo(15.578f, 0.001f, 15.575f, 0.004f, 15.573f, 0.005f)\n                    curveTo(15.572f, 0.005f, 15.568f, 0.008f, 15.565f, 0.009f)\n                    curveTo(15.116f, 0.222f, 12.454f, 4.437f, 16.062f, 8.045f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.137f to Color(0xFFFFF44F),\n                            0.48f to Color(0xFFFF980E),\n                            0.592f to Color(0xFFFF5634),\n                            0.655f to Color(0xFFFF3647),\n                            0.904f to Color(0xFFE31587)\n                        ),\n                        center = Offset(10.939f, 4.738f),\n                        radius = 22.077f\n                    )\n                ) {\n                    moveTo(17.313f, 9.594f)\n                    curveTo(17.257f, 9.544f, 17.199f, 9.496f, 17.139f, 9.451f)\n                    curveTo(17.115f, 9.434f, 17.092f, 9.416f, 17.066f, 9.398f)\n                    curveTo(16.25f, 8.833f, 15.253f, 8.591f, 14.268f, 8.719f)\n                    curveTo(18.442f, 10.806f, 17.322f, 17.994f, 11.537f, 17.723f)\n                    curveTo(11.022f, 17.702f, 10.513f, 17.603f, 10.027f, 17.431f)\n                    curveTo(9.912f, 17.388f, 9.798f, 17.342f, 9.686f, 17.292f)\n                    curveTo(9.62f, 17.262f, 9.555f, 17.232f, 9.49f, 17.198f)\n                    lineTo(9.498f, 17.204f)\n                    curveTo(10.305f, 17.754f, 14.29f, 19.1f, 18.456f, 15.706f)\n                    lineTo(18.462f, 15.691f)\n                    curveTo(18.566f, 15.448f, 19.945f, 12.019f, 17.313f, 9.594f)\n                    close()\n                }\n                path(\n                    fill = Brush.radialGradient(\n                        colorStops = arrayOf(\n                            0.094f to Color(0xFFFFF44F),\n                            0.231f to Color(0xFFFFE141),\n                            0.509f to Color(0xFFFFAF1E),\n                            0.626f to Color(0xFFFF980E)\n                        ),\n                        center = Offset(16.767f, 6.03f),\n                        radius = 24.163f\n                    )\n                ) {\n                    moveTo(6.757f, 13.4f)\n                    curveTo(6.757f, 13.4f, 7.293f, 11.403f, 10.594f, 11.403f)\n                    curveTo(10.95f, 11.403f, 11.971f, 10.408f, 11.989f, 10.119f)\n                    curveTo(10.57f, 10.633f, 9.006f, 10.571f, 7.631f, 9.947f)\n                    curveTo(6.965f, 9.609f, 6.222f, 9.448f, 5.475f, 9.478f)\n                    curveTo(4.728f, 9.508f, 4.002f, 9.728f, 3.364f, 10.119f)\n                    curveTo(3.606f, 10.642f, 3.995f, 11.084f, 4.483f, 11.391f)\n                    curveTo(4.971f, 11.698f, 5.537f, 11.857f, 6.114f, 11.848f)\n                    curveTo(5.899f, 13.746f, 6.902f, 15.959f, 9.33f, 17.119f)\n                    curveTo(9.385f, 17.145f, 9.435f, 17.173f, 9.491f, 17.198f)\n                    curveTo(8.074f, 16.466f, 6.903f, 15.081f, 6.757f, 13.4f)\n                    close()\n                }\n                path(\n                    fill = Brush.linearGradient(\n                        colorStops = arrayOf(\n                            0.167f to Color(0xCCFFF44F),\n                            0.266f to Color(0xA1FFF44F),\n                            0.489f to Color(0x37FFF44F),\n                            0.6f to Color(0x00FFF44F)\n                        ),\n                        start = Offset(20.94f, 3.611f),\n                        end = Offset(4.545f, 20.005f)\n                    )\n                ) {\n                    moveTo(22.708f, 8.034f)\n                    curveTo(22.204f, 6.82f, 21.181f, 5.51f, 20.379f, 5.096f)\n                    curveTo(20.951f, 6.203f, 21.347f, 7.391f, 21.555f, 8.619f)\n                    lineTo(21.557f, 8.639f)\n                    curveTo(20.245f, 5.367f, 18.019f, 4.048f, 16.202f, 1.175f)\n                    curveTo(16.108f, 1.029f, 16.017f, 0.88f, 15.928f, 0.731f)\n                    curveTo(15.883f, 0.652f, 15.84f, 0.572f, 15.801f, 0.491f)\n                    curveTo(15.725f, 0.345f, 15.667f, 0.191f, 15.627f, 0.031f)\n                    curveTo(15.627f, 0.024f, 15.625f, 0.017f, 15.62f, 0.011f)\n                    curveTo(15.615f, 0.006f, 15.608f, 0.002f, 15.601f, 0.001f)\n                    curveTo(15.594f, -0f, 15.586f, -0f, 15.579f, 0.001f)\n                    curveTo(15.578f, 0.001f, 15.575f, 0.004f, 15.573f, 0.005f)\n                    curveTo(15.572f, 0.005f, 15.568f, 0.008f, 15.565f, 0.009f)\n                    lineTo(15.57f, 0.001f)\n                    curveTo(12.654f, 1.708f, 11.665f, 4.868f, 11.574f, 6.449f)\n                    curveTo(11.709f, 6.44f, 11.844f, 6.428f, 11.982f, 6.428f)\n                    curveTo(13.016f, 6.43f, 14.032f, 6.706f, 14.925f, 7.228f)\n                    curveTo(15.818f, 7.749f, 16.558f, 8.498f, 17.068f, 9.398f)\n                    curveTo(16.251f, 8.833f, 15.254f, 8.591f, 14.269f, 8.719f)\n                    curveTo(18.444f, 10.806f, 17.324f, 17.994f, 11.538f, 17.723f)\n                    curveTo(11.023f, 17.702f, 10.514f, 17.603f, 10.028f, 17.431f)\n                    curveTo(9.913f, 17.388f, 9.799f, 17.342f, 9.687f, 17.292f)\n                    curveTo(9.622f, 17.262f, 9.556f, 17.232f, 9.491f, 17.198f)\n                    lineTo(9.499f, 17.204f)\n                    curveTo(9.443f, 17.176f, 9.386f, 17.148f, 9.33f, 17.119f)\n                    curveTo(9.385f, 17.145f, 9.435f, 17.174f, 9.491f, 17.198f)\n                    curveTo(8.074f, 16.466f, 6.903f, 15.081f, 6.757f, 13.4f)\n                    curveTo(6.757f, 13.4f, 7.293f, 11.403f, 10.594f, 11.403f)\n                    curveTo(10.95f, 11.403f, 11.971f, 10.408f, 11.989f, 10.119f)\n                    curveTo(11.985f, 10.024f, 9.965f, 9.22f, 9.177f, 8.445f)\n                    curveTo(8.757f, 8.03f, 8.557f, 7.83f, 8.38f, 7.68f)\n                    curveTo(8.284f, 7.599f, 8.184f, 7.523f, 8.08f, 7.453f)\n                    curveTo(7.816f, 6.528f, 7.805f, 5.548f, 8.048f, 4.616f)\n                    curveTo(6.977f, 5.135f, 6.026f, 5.87f, 5.254f, 6.775f)\n                    horizontalLineTo(5.249f)\n                    curveTo(4.789f, 6.192f, 4.821f, 4.27f, 4.847f, 3.868f)\n                    curveTo(4.711f, 3.923f, 4.581f, 3.992f, 4.46f, 4.074f)\n                    curveTo(4.054f, 4.364f, 3.674f, 4.689f, 3.325f, 5.046f)\n                    curveTo(2.928f, 5.449f, 2.565f, 5.884f, 2.24f, 6.348f)\n                    verticalLineTo(6.349f)\n                    verticalLineTo(6.347f)\n                    curveTo(1.494f, 7.405f, 0.965f, 8.6f, 0.683f, 9.864f)\n                    lineTo(0.668f, 9.941f)\n                    curveTo(0.646f, 10.043f, 0.548f, 10.561f, 0.534f, 10.673f)\n                    curveTo(0.534f, 10.681f, 0.534f, 10.664f, 0.534f, 10.673f)\n                    curveTo(0.444f, 11.208f, 0.387f, 11.749f, 0.362f, 12.291f)\n                    verticalLineTo(12.351f)\n                    curveTo(0.37f, 21.286f, 10.048f, 26.862f, 17.782f, 22.388f)\n                    curveTo(19.254f, 21.536f, 20.521f, 20.372f, 21.493f, 18.976f)\n                    curveTo(22.465f, 17.581f, 23.119f, 15.989f, 23.408f, 14.314f)\n                    curveTo(23.428f, 14.164f, 23.443f, 14.016f, 23.461f, 13.864f)\n                    curveTo(23.7f, 11.889f, 23.441f, 9.884f, 22.708f, 8.034f)\n                    close()\n                }\n            }\n        }.build()\n\n        return _BrowserMozillaFirefox!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _BrowserMozillaFirefox: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/BrowserOpera.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.BrowserOpera: ImageVector\n    get() {\n        if (_BrowserOpera != null) {\n            return _BrowserOpera!!\n        }\n        _BrowserOpera = ImageVector.Builder(\n            name = \"BrowserOpera\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0.3f to Color(0xFFFF1B2D),\n                        0.438f to Color(0xFFFA1A2C),\n                        0.594f to Color(0xFFED1528),\n                        0.758f to Color(0xFFD60E21),\n                        0.927f to Color(0xFFB70519),\n                        1f to Color(0xFFA70014)\n                    ),\n                    start = Offset(999.904f, 39.12f),\n                    end = Offset(999.904f, 2365.08f)\n                )\n            ) {\n                moveTo(8.053f, 18.759f)\n                curveTo(6.722f, 17.194f, 5.869f, 14.878f, 5.813f, 12.281f)\n                verticalLineTo(11.719f)\n                curveTo(5.869f, 9.122f, 6.731f, 6.806f, 8.053f, 5.241f)\n                curveTo(9.778f, 3.009f, 12.309f, 2.006f, 15.169f, 2.006f)\n                curveTo(16.931f, 2.006f, 18.591f, 2.128f, 19.997f, 3.066f)\n                curveTo(17.888f, 1.163f, 15.103f, 0.009f, 12.047f, 0f)\n                horizontalLineTo(12f)\n                curveTo(5.372f, 0f, 0f, 5.372f, 0f, 12f)\n                curveTo(0f, 18.431f, 5.063f, 23.691f, 11.428f, 23.991f)\n                curveTo(11.616f, 24f, 11.813f, 24f, 12f, 24f)\n                curveTo(15.075f, 24f, 17.878f, 22.847f, 19.997f, 20.944f)\n                curveTo(18.591f, 21.881f, 17.025f, 21.919f, 15.262f, 21.919f)\n                curveTo(12.413f, 21.928f, 9.769f, 21f, 8.053f, 18.759f)\n                close()\n            }\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF9C0000),\n                        0.7f to Color(0xFFFF4B4B)\n                    ),\n                    start = Offset(805.237f, 19.369f),\n                    end = Offset(805.237f, 2076.56f)\n                )\n            ) {\n                moveTo(8.053f, 5.241f)\n                curveTo(9.15f, 3.937f, 10.575f, 3.159f, 12.131f, 3.159f)\n                curveTo(15.628f, 3.159f, 18.459f, 7.116f, 18.459f, 12.009f)\n                curveTo(18.459f, 16.903f, 15.628f, 20.859f, 12.131f, 20.859f)\n                curveTo(10.575f, 20.859f, 9.159f, 20.072f, 8.053f, 18.778f)\n                curveTo(9.778f, 21.009f, 12.337f, 22.434f, 15.188f, 22.434f)\n                curveTo(16.941f, 22.434f, 18.591f, 21.9f, 19.997f, 20.962f)\n                curveTo(22.453f, 18.75f, 24f, 15.553f, 24f, 12f)\n                curveTo(24f, 8.447f, 22.453f, 5.25f, 19.997f, 3.056f)\n                curveTo(18.591f, 2.119f, 16.95f, 1.584f, 15.188f, 1.584f)\n                curveTo(12.328f, 1.584f, 9.769f, 3f, 8.053f, 5.241f)\n                close()\n            }\n        }.build()\n\n        return _BrowserOpera!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _BrowserOpera: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Check.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Check: ImageVector\n    get() {\n        if (_Check != null) {\n            return _Check!!\n        }\n        _Check = ImageVector.Builder(\n            name = \"Check\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(5f, 12f)\n                lineTo(10f, 17f)\n                lineTo(20f, 7f)\n            }\n        }.build()\n\n        return _Check!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Check: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Clear.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Clear: ImageVector\n    get() {\n        if (_Clear != null) {\n            return _Clear!!\n        }\n        _Clear = ImageVector.Builder(\n            name = \"Clear\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(5.293f, 5.293f)\n                curveTo(5.683f, 4.902f, 6.317f, 4.902f, 6.707f, 5.293f)\n                lineTo(12f, 10.586f)\n                lineTo(17.293f, 5.293f)\n                curveTo(17.683f, 4.902f, 18.317f, 4.902f, 18.707f, 5.293f)\n                curveTo(19.098f, 5.683f, 19.098f, 6.317f, 18.707f, 6.707f)\n                lineTo(13.414f, 12f)\n                lineTo(18.707f, 17.293f)\n                curveTo(19.098f, 17.683f, 19.098f, 18.317f, 18.707f, 18.707f)\n                curveTo(18.317f, 19.098f, 17.683f, 19.098f, 17.293f, 18.707f)\n                lineTo(12f, 13.414f)\n                lineTo(6.707f, 18.707f)\n                curveTo(6.317f, 19.098f, 5.683f, 19.098f, 5.293f, 18.707f)\n                curveTo(4.902f, 18.317f, 4.902f, 17.683f, 5.293f, 17.293f)\n                lineTo(10.586f, 12f)\n                lineTo(5.293f, 6.707f)\n                curveTo(4.902f, 6.317f, 4.902f, 5.683f, 5.293f, 5.293f)\n                close()\n            }\n        }.build()\n\n        return _Clear!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Clear: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Clipboard.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Clipboard: ImageVector\n    get() {\n        if (_Clipboard != null) {\n            return _Clipboard!!\n        }\n        _Clipboard = ImageVector.Builder(\n            name = \"Clipboard\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(9f, 5f)\n                horizontalLineTo(7f)\n                curveTo(6.47f, 5f, 5.961f, 5.211f, 5.586f, 5.586f)\n                curveTo(5.211f, 5.961f, 5f, 6.47f, 5f, 7f)\n                verticalLineTo(19f)\n                curveTo(5f, 19.53f, 5.211f, 20.039f, 5.586f, 20.414f)\n                curveTo(5.961f, 20.789f, 6.47f, 21f, 7f, 21f)\n                horizontalLineTo(17f)\n                curveTo(17.53f, 21f, 18.039f, 20.789f, 18.414f, 20.414f)\n                curveTo(18.789f, 20.039f, 19f, 19.53f, 19f, 19f)\n                verticalLineTo(7f)\n                curveTo(19f, 6.47f, 18.789f, 5.961f, 18.414f, 5.586f)\n                curveTo(18.039f, 5.211f, 17.53f, 5f, 17f, 5f)\n                horizontalLineTo(15f)\n                moveTo(9f, 5f)\n                curveTo(9f, 4.47f, 9.211f, 3.961f, 9.586f, 3.586f)\n                curveTo(9.961f, 3.211f, 10.47f, 3f, 11f, 3f)\n                horizontalLineTo(13f)\n                curveTo(13.53f, 3f, 14.039f, 3.211f, 14.414f, 3.586f)\n                curveTo(14.789f, 3.961f, 15f, 4.47f, 15f, 5f)\n                moveTo(9f, 5f)\n                curveTo(9f, 5.53f, 9.211f, 6.039f, 9.586f, 6.414f)\n                curveTo(9.961f, 6.789f, 10.47f, 7f, 11f, 7f)\n                horizontalLineTo(13f)\n                curveTo(13.53f, 7f, 14.039f, 6.789f, 14.414f, 6.414f)\n                curveTo(14.789f, 6.039f, 15f, 5.53f, 15f, 5f)\n            }\n        }.build()\n\n        return _Clipboard!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Clipboard: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Clock.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Clock: ImageVector\n    get() {\n        if (_Clock != null) {\n            return _Clock!!\n        }\n        _Clock = ImageVector.Builder(\n            name = \"Clock\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(20f, 12f)\n                curveTo(20f, 9.878f, 19.157f, 7.843f, 17.657f, 6.343f)\n                curveTo(16.157f, 4.842f, 14.122f, 4f, 12f, 4f)\n                curveTo(9.878f, 4f, 7.843f, 4.842f, 6.343f, 6.343f)\n                curveTo(4.842f, 7.843f, 4f, 9.878f, 4f, 12f)\n                curveTo(4f, 13.051f, 4.207f, 14.091f, 4.609f, 15.061f)\n                curveTo(5.011f, 16.032f, 5.6f, 16.914f, 6.343f, 17.657f)\n                curveTo(7.086f, 18.4f, 7.968f, 18.989f, 8.938f, 19.391f)\n                curveTo(9.909f, 19.793f, 10.949f, 20f, 12f, 20f)\n                lineTo(12.394f, 19.99f)\n                curveTo(13.31f, 19.945f, 14.212f, 19.742f, 15.061f, 19.391f)\n                curveTo(16.032f, 18.989f, 16.914f, 18.4f, 17.657f, 17.657f)\n                curveTo(18.4f, 16.914f, 18.989f, 16.032f, 19.391f, 15.061f)\n                curveTo(19.793f, 14.091f, 20f, 13.051f, 20f, 12f)\n                close()\n                moveTo(11f, 7f)\n                curveTo(11f, 6.448f, 11.448f, 6f, 12f, 6f)\n                curveTo(12.552f, 6f, 13f, 6.448f, 13f, 7f)\n                verticalLineTo(11.586f)\n                lineTo(15.707f, 14.293f)\n                curveTo(16.098f, 14.684f, 16.098f, 15.316f, 15.707f, 15.707f)\n                curveTo(15.316f, 16.098f, 14.684f, 16.098f, 14.293f, 15.707f)\n                lineTo(11.293f, 12.707f)\n                curveTo(11.105f, 12.519f, 11f, 12.265f, 11f, 12f)\n                verticalLineTo(7f)\n                close()\n                moveTo(22f, 12f)\n                curveTo(22f, 13.313f, 21.742f, 14.614f, 21.239f, 15.827f)\n                curveTo(20.737f, 17.04f, 20f, 18.143f, 19.071f, 19.071f)\n                curveTo(18.143f, 20f, 17.04f, 20.737f, 15.827f, 21.239f)\n                curveTo(14.766f, 21.679f, 13.637f, 21.932f, 12.492f, 21.988f)\n                lineTo(12f, 22f)\n                curveTo(10.687f, 22f, 9.386f, 21.742f, 8.173f, 21.239f)\n                curveTo(6.96f, 20.737f, 5.857f, 20f, 4.929f, 19.071f)\n                curveTo(4f, 18.143f, 3.263f, 17.04f, 2.761f, 15.827f)\n                curveTo(2.258f, 14.614f, 2f, 13.313f, 2f, 12f)\n                curveTo(2f, 9.348f, 3.053f, 6.804f, 4.929f, 4.929f)\n                curveTo(6.804f, 3.053f, 9.348f, 2f, 12f, 2f)\n                curveTo(14.652f, 2f, 17.196f, 3.053f, 19.071f, 4.929f)\n                curveTo(20.947f, 6.804f, 22f, 9.348f, 22f, 12f)\n                close()\n            }\n        }.build()\n\n        return _Clock!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Clock: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Colors.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Colors: ImageVector\n    get() {\n        if (_Colors != null) {\n            return _Colors!!\n        }\n        _Colors = ImageVector.Builder(\n            name = \"Colors\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(12f, 21f)\n                curveTo(9.613f, 21f, 7.324f, 20.052f, 5.636f, 18.364f)\n                curveTo(3.948f, 16.676f, 3f, 14.387f, 3f, 12f)\n                curveTo(3f, 9.613f, 3.948f, 7.324f, 5.636f, 5.636f)\n                curveTo(7.324f, 3.948f, 9.613f, 3f, 12f, 3f)\n                curveTo(16.97f, 3f, 21f, 6.582f, 21f, 11f)\n                curveTo(21f, 12.06f, 20.526f, 13.078f, 19.682f, 13.828f)\n                curveTo(18.838f, 14.578f, 17.693f, 15f, 16.5f, 15f)\n                horizontalLineTo(14f)\n                curveTo(13.554f, 14.993f, 13.118f, 15.135f, 12.762f, 15.404f)\n                curveTo(12.406f, 15.673f, 12.15f, 16.053f, 12.035f, 16.484f)\n                curveTo(11.92f, 16.916f, 11.953f, 17.373f, 12.128f, 17.783f)\n                curveTo(12.302f, 18.194f, 12.609f, 18.534f, 13f, 18.75f)\n                curveTo(13.2f, 18.934f, 13.337f, 19.176f, 13.392f, 19.442f)\n                curveTo(13.446f, 19.708f, 13.417f, 19.985f, 13.306f, 20.233f)\n                curveTo(13.196f, 20.482f, 13.011f, 20.689f, 12.776f, 20.827f)\n                curveTo(12.542f, 20.964f, 12.271f, 21.025f, 12f, 21f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(7.5f, 10.5f)\n                curveTo(7.5f, 10.765f, 7.605f, 11.02f, 7.793f, 11.207f)\n                curveTo(7.98f, 11.395f, 8.235f, 11.5f, 8.5f, 11.5f)\n                curveTo(8.765f, 11.5f, 9.02f, 11.395f, 9.207f, 11.207f)\n                curveTo(9.395f, 11.02f, 9.5f, 10.765f, 9.5f, 10.5f)\n                curveTo(9.5f, 10.235f, 9.395f, 9.98f, 9.207f, 9.793f)\n                curveTo(9.02f, 9.605f, 8.765f, 9.5f, 8.5f, 9.5f)\n                curveTo(8.235f, 9.5f, 7.98f, 9.605f, 7.793f, 9.793f)\n                curveTo(7.605f, 9.98f, 7.5f, 10.235f, 7.5f, 10.5f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(11.5f, 7.5f)\n                curveTo(11.5f, 7.765f, 11.605f, 8.02f, 11.793f, 8.207f)\n                curveTo(11.98f, 8.395f, 12.235f, 8.5f, 12.5f, 8.5f)\n                curveTo(12.765f, 8.5f, 13.02f, 8.395f, 13.207f, 8.207f)\n                curveTo(13.395f, 8.02f, 13.5f, 7.765f, 13.5f, 7.5f)\n                curveTo(13.5f, 7.235f, 13.395f, 6.98f, 13.207f, 6.793f)\n                curveTo(13.02f, 6.605f, 12.765f, 6.5f, 12.5f, 6.5f)\n                curveTo(12.235f, 6.5f, 11.98f, 6.605f, 11.793f, 6.793f)\n                curveTo(11.605f, 6.98f, 11.5f, 7.235f, 11.5f, 7.5f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(15.5f, 10.5f)\n                curveTo(15.5f, 10.765f, 15.605f, 11.02f, 15.793f, 11.207f)\n                curveTo(15.98f, 11.395f, 16.235f, 11.5f, 16.5f, 11.5f)\n                curveTo(16.765f, 11.5f, 17.02f, 11.395f, 17.207f, 11.207f)\n                curveTo(17.395f, 11.02f, 17.5f, 10.765f, 17.5f, 10.5f)\n                curveTo(17.5f, 10.235f, 17.395f, 9.98f, 17.207f, 9.793f)\n                curveTo(17.02f, 9.605f, 16.765f, 9.5f, 16.5f, 9.5f)\n                curveTo(16.235f, 9.5f, 15.98f, 9.605f, 15.793f, 9.793f)\n                curveTo(15.605f, 9.98f, 15.5f, 10.235f, 15.5f, 10.5f)\n                close()\n            }\n        }.build()\n\n        return _Colors!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Colors: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Copy.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Copy: ImageVector\n    get() {\n        if (_Copy != null) {\n            return _Copy!!\n        }\n        _Copy = ImageVector.Builder(\n            name = \"Copy\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(16f, 8f)\n                verticalLineTo(6f)\n                curveTo(16f, 5.47f, 15.789f, 4.961f, 15.414f, 4.586f)\n                curveTo(15.039f, 4.211f, 14.53f, 4f, 14f, 4f)\n                horizontalLineTo(6f)\n                curveTo(5.47f, 4f, 4.961f, 4.211f, 4.586f, 4.586f)\n                curveTo(4.211f, 4.961f, 4f, 5.47f, 4f, 6f)\n                verticalLineTo(14f)\n                curveTo(4f, 14.53f, 4.211f, 15.039f, 4.586f, 15.414f)\n                curveTo(4.961f, 15.789f, 5.47f, 16f, 6f, 16f)\n                horizontalLineTo(8f)\n                moveTo(8f, 10f)\n                curveTo(8f, 9.47f, 8.211f, 8.961f, 8.586f, 8.586f)\n                curveTo(8.961f, 8.211f, 9.47f, 8f, 10f, 8f)\n                horizontalLineTo(18f)\n                curveTo(18.53f, 8f, 19.039f, 8.211f, 19.414f, 8.586f)\n                curveTo(19.789f, 8.961f, 20f, 9.47f, 20f, 10f)\n                verticalLineTo(18f)\n                curveTo(20f, 18.53f, 19.789f, 19.039f, 19.414f, 19.414f)\n                curveTo(19.039f, 19.789f, 18.53f, 20f, 18f, 20f)\n                horizontalLineTo(10f)\n                curveTo(9.47f, 20f, 8.961f, 19.789f, 8.586f, 19.414f)\n                curveTo(8.211f, 19.039f, 8f, 18.53f, 8f, 18f)\n                verticalLineTo(10f)\n                close()\n            }\n        }.build()\n\n        return _Copy!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Copy: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Data.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Data: ImageVector\n    get() {\n        if (_Data != null) {\n            return _Data!!\n        }\n        _Data = ImageVector.Builder(\n            name = \"Data\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(4f, 6f)\n                curveTo(4f, 6.796f, 4.843f, 7.559f, 6.343f, 8.121f)\n                curveTo(7.843f, 8.684f, 9.878f, 9f, 12f, 9f)\n                curveTo(14.122f, 9f, 16.157f, 8.684f, 17.657f, 8.121f)\n                curveTo(19.157f, 7.559f, 20f, 6.796f, 20f, 6f)\n                moveTo(4f, 6f)\n                curveTo(4f, 5.204f, 4.843f, 4.441f, 6.343f, 3.879f)\n                curveTo(7.843f, 3.316f, 9.878f, 3f, 12f, 3f)\n                curveTo(14.122f, 3f, 16.157f, 3.316f, 17.657f, 3.879f)\n                curveTo(19.157f, 4.441f, 20f, 5.204f, 20f, 6f)\n                moveTo(4f, 6f)\n                verticalLineTo(12f)\n                moveTo(20f, 6f)\n                verticalLineTo(12f)\n                moveTo(4f, 12f)\n                curveTo(4f, 12.796f, 4.843f, 13.559f, 6.343f, 14.121f)\n                curveTo(7.843f, 14.684f, 9.878f, 15f, 12f, 15f)\n                curveTo(14.122f, 15f, 16.157f, 14.684f, 17.657f, 14.121f)\n                curveTo(19.157f, 13.559f, 20f, 12.796f, 20f, 12f)\n                moveTo(4f, 12f)\n                verticalLineTo(18f)\n                curveTo(4f, 18.796f, 4.843f, 19.559f, 6.343f, 20.121f)\n                curveTo(7.843f, 20.684f, 9.878f, 21f, 12f, 21f)\n                curveTo(14.122f, 21f, 16.157f, 20.684f, 17.657f, 20.121f)\n                curveTo(19.157f, 19.559f, 20f, 18.796f, 20f, 18f)\n                verticalLineTo(12f)\n            }\n        }.build()\n\n        return _Data!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Data: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Delete.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Delete: ImageVector\n    get() {\n        if (_Delete != null) {\n            return _Delete!!\n        }\n        _Delete = ImageVector.Builder(\n            name = \"Delete\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(10f, 3.75f)\n                curveTo(9.934f, 3.75f, 9.87f, 3.776f, 9.823f, 3.823f)\n                curveTo(9.776f, 3.87f, 9.75f, 3.934f, 9.75f, 4f)\n                verticalLineTo(6.25f)\n                horizontalLineTo(14.25f)\n                verticalLineTo(4f)\n                curveTo(14.25f, 3.934f, 14.224f, 3.87f, 14.177f, 3.823f)\n                curveTo(14.13f, 3.776f, 14.066f, 3.75f, 14f, 3.75f)\n                horizontalLineTo(10f)\n                close()\n                moveTo(15.75f, 6.25f)\n                verticalLineTo(4f)\n                curveTo(15.75f, 3.536f, 15.566f, 3.091f, 15.237f, 2.763f)\n                curveTo(14.909f, 2.434f, 14.464f, 2.25f, 14f, 2.25f)\n                horizontalLineTo(10f)\n                curveTo(9.536f, 2.25f, 9.091f, 2.434f, 8.763f, 2.763f)\n                curveTo(8.434f, 3.091f, 8.25f, 3.536f, 8.25f, 4f)\n                verticalLineTo(6.25f)\n                horizontalLineTo(5.009f)\n                curveTo(5.003f, 6.25f, 4.998f, 6.25f, 4.993f, 6.25f)\n                horizontalLineTo(4f)\n                curveTo(3.586f, 6.25f, 3.25f, 6.586f, 3.25f, 7f)\n                curveTo(3.25f, 7.414f, 3.586f, 7.75f, 4f, 7.75f)\n                horizontalLineTo(4.31f)\n                lineTo(5.25f, 19.034f)\n                curveTo(5.259f, 19.751f, 5.548f, 20.437f, 6.055f, 20.944f)\n                curveTo(6.571f, 21.46f, 7.271f, 21.75f, 8f, 21.75f)\n                horizontalLineTo(16f)\n                curveTo(16.729f, 21.75f, 17.429f, 21.46f, 17.944f, 20.944f)\n                curveTo(18.452f, 20.437f, 18.741f, 19.751f, 18.75f, 19.034f)\n                lineTo(19.69f, 7.75f)\n                horizontalLineTo(20f)\n                curveTo(20.414f, 7.75f, 20.75f, 7.414f, 20.75f, 7f)\n                curveTo(20.75f, 6.586f, 20.414f, 6.25f, 20f, 6.25f)\n                horizontalLineTo(19.007f)\n                curveTo(19.002f, 6.25f, 18.997f, 6.25f, 18.991f, 6.25f)\n                horizontalLineTo(15.75f)\n                close()\n                moveTo(5.815f, 7.75f)\n                lineTo(6.747f, 18.938f)\n                curveTo(6.749f, 18.958f, 6.75f, 18.979f, 6.75f, 19f)\n                curveTo(6.75f, 19.331f, 6.882f, 19.649f, 7.116f, 19.884f)\n                curveTo(7.351f, 20.118f, 7.668f, 20.25f, 8f, 20.25f)\n                horizontalLineTo(16f)\n                curveTo(16.331f, 20.25f, 16.649f, 20.118f, 16.884f, 19.884f)\n                curveTo(17.118f, 19.649f, 17.25f, 19.331f, 17.25f, 19f)\n                curveTo(17.25f, 18.979f, 17.251f, 18.958f, 17.253f, 18.938f)\n                lineTo(18.185f, 7.75f)\n                horizontalLineTo(5.815f)\n                close()\n                moveTo(9.47f, 12.53f)\n                curveTo(9.177f, 12.237f, 9.177f, 11.763f, 9.47f, 11.47f)\n                curveTo(9.763f, 11.177f, 10.237f, 11.177f, 10.53f, 11.47f)\n                lineTo(12f, 12.939f)\n                lineTo(13.47f, 11.47f)\n                curveTo(13.763f, 11.177f, 14.237f, 11.177f, 14.53f, 11.47f)\n                curveTo(14.823f, 11.763f, 14.823f, 12.237f, 14.53f, 12.53f)\n                lineTo(13.061f, 14f)\n                lineTo(14.53f, 15.47f)\n                curveTo(14.823f, 15.763f, 14.823f, 16.237f, 14.53f, 16.53f)\n                curveTo(14.237f, 16.823f, 13.763f, 16.823f, 13.47f, 16.53f)\n                lineTo(12f, 15.061f)\n                lineTo(10.53f, 16.53f)\n                curveTo(10.237f, 16.823f, 9.763f, 16.823f, 9.47f, 16.53f)\n                curveTo(9.177f, 16.237f, 9.177f, 15.763f, 9.47f, 15.47f)\n                lineTo(10.939f, 14f)\n                lineTo(9.47f, 12.53f)\n                close()\n            }\n        }.build()\n\n        return _Delete!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Delete: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Down.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Down: ImageVector\n    get() {\n        if (_Down != null) {\n            return _Down!!\n        }\n        _Down = ImageVector.Builder(\n            name = \"Down\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(5.293f, 8.293f)\n                curveTo(5.683f, 7.902f, 6.317f, 7.902f, 6.707f, 8.293f)\n                lineTo(12f, 13.586f)\n                lineTo(17.293f, 8.293f)\n                curveTo(17.683f, 7.902f, 18.317f, 7.902f, 18.707f, 8.293f)\n                curveTo(19.098f, 8.683f, 19.098f, 9.317f, 18.707f, 9.707f)\n                lineTo(12.707f, 15.707f)\n                curveTo(12.317f, 16.098f, 11.683f, 16.098f, 11.293f, 15.707f)\n                lineTo(5.293f, 9.707f)\n                curveTo(4.902f, 9.317f, 4.902f, 8.683f, 5.293f, 8.293f)\n                close()\n            }\n        }.build()\n\n        return _Down!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Down: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/DownSpeed.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.DownSpeed: ImageVector\n    get() {\n        if (_DownSpeed != null) {\n            return _DownSpeed!!\n        }\n        _DownSpeed = ImageVector.Builder(\n            name = \"DownSpeed\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(11.292f, 4.306f)\n                curveTo(12.102f, 4.163f, 12.936f, 4.165f, 13.745f, 4.312f)\n                curveTo(14.555f, 4.459f, 15.328f, 4.749f, 16.02f, 5.168f)\n                curveTo(16.712f, 5.587f, 17.31f, 6.127f, 17.778f, 6.762f)\n                curveTo(18.247f, 7.397f, 18.575f, 8.113f, 18.739f, 8.87f)\n                curveTo(18.838f, 9.325f, 18.877f, 9.789f, 18.855f, 10.25f)\n                horizontalLineTo(19f)\n                curveTo(20.127f, 10.25f, 21.208f, 10.698f, 22.005f, 11.495f)\n                curveTo(22.802f, 12.292f, 23.25f, 13.373f, 23.25f, 14.5f)\n                curveTo(23.25f, 15.627f, 22.802f, 16.708f, 22.005f, 17.505f)\n                curveTo(21.208f, 18.302f, 20.127f, 18.75f, 19f, 18.75f)\n                curveTo(18.586f, 18.75f, 18.25f, 18.414f, 18.25f, 18f)\n                curveTo(18.25f, 17.586f, 18.586f, 17.25f, 19f, 17.25f)\n                curveTo(19.729f, 17.25f, 20.429f, 16.96f, 20.944f, 16.445f)\n                curveTo(21.46f, 15.929f, 21.75f, 15.229f, 21.75f, 14.5f)\n                curveTo(21.75f, 13.771f, 21.46f, 13.071f, 20.944f, 12.556f)\n                curveTo(20.429f, 12.04f, 19.729f, 11.75f, 19f, 11.75f)\n                horizontalLineTo(18f)\n                curveTo(17.772f, 11.75f, 17.557f, 11.646f, 17.414f, 11.469f)\n                curveTo(17.272f, 11.291f, 17.218f, 11.058f, 17.268f, 10.836f)\n                curveTo(17.39f, 10.292f, 17.392f, 9.733f, 17.274f, 9.189f)\n                curveTo(17.155f, 8.645f, 16.918f, 8.122f, 16.571f, 7.652f)\n                curveTo(16.224f, 7.182f, 15.774f, 6.773f, 15.243f, 6.451f)\n                curveTo(14.712f, 6.13f, 14.112f, 5.903f, 13.477f, 5.788f)\n                curveTo(12.842f, 5.672f, 12.188f, 5.671f, 11.552f, 5.783f)\n                curveTo(10.916f, 5.895f, 10.315f, 6.118f, 9.781f, 6.437f)\n                curveTo(8.704f, 7.08f, 7.978f, 8.068f, 7.732f, 9.164f)\n                curveTo(7.652f, 9.518f, 7.332f, 9.764f, 6.97f, 9.749f)\n                curveTo(6.069f, 9.713f, 5.186f, 9.978f, 4.474f, 10.494f)\n                curveTo(3.761f, 11.008f, 3.265f, 11.739f, 3.061f, 12.555f)\n                curveTo(2.857f, 13.37f, 2.956f, 14.229f, 3.342f, 14.986f)\n                curveTo(3.729f, 15.743f, 4.383f, 16.357f, 5.2f, 16.712f)\n                curveTo(5.579f, 16.878f, 5.753f, 17.32f, 5.588f, 17.7f)\n                curveTo(5.422f, 18.079f, 4.98f, 18.253f, 4.6f, 18.087f)\n                curveTo(3.475f, 17.597f, 2.556f, 16.744f, 2.006f, 15.668f)\n                curveTo(1.456f, 14.59f, 1.314f, 13.361f, 1.606f, 12.191f)\n                curveTo(1.898f, 11.022f, 2.605f, 9.994f, 3.595f, 9.278f)\n                curveTo(4.424f, 8.679f, 5.41f, 8.328f, 6.432f, 8.259f)\n                curveTo(6.874f, 6.976f, 7.79f, 5.879f, 9.012f, 5.149f)\n                curveTo(9.706f, 4.734f, 10.481f, 4.448f, 11.292f, 4.306f)\n                close()\n                moveTo(12f, 12.25f)\n                curveTo(12.414f, 12.25f, 12.75f, 12.586f, 12.75f, 13f)\n                verticalLineTo(20.189f)\n                lineTo(14.47f, 18.47f)\n                curveTo(14.763f, 18.177f, 15.237f, 18.177f, 15.53f, 18.47f)\n                curveTo(15.823f, 18.763f, 15.823f, 19.237f, 15.53f, 19.53f)\n                lineTo(12.53f, 22.53f)\n                curveTo(12.237f, 22.823f, 11.763f, 22.823f, 11.47f, 22.53f)\n                lineTo(8.47f, 19.53f)\n                curveTo(8.177f, 19.237f, 8.177f, 18.763f, 8.47f, 18.47f)\n                curveTo(8.763f, 18.177f, 9.237f, 18.177f, 9.53f, 18.47f)\n                lineTo(11.25f, 20.189f)\n                verticalLineTo(13f)\n                curveTo(11.25f, 12.586f, 11.586f, 12.25f, 12f, 12.25f)\n                close()\n            }\n        }.build()\n\n        return _DownSpeed!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _DownSpeed: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/DragAndDrop.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.DragAndDrop: ImageVector\n    get() {\n        if (_DragAndDrop != null) {\n            return _DragAndDrop!!\n        }\n        _DragAndDrop = ImageVector.Builder(\n            name = \"DragAndDrop\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(18f, 9f)\n                lineTo(21f, 12f)\n                moveTo(21f, 12f)\n                lineTo(18f, 15f)\n                moveTo(21f, 12f)\n                horizontalLineTo(15f)\n                moveTo(6f, 9f)\n                lineTo(3f, 12f)\n                moveTo(3f, 12f)\n                lineTo(6f, 15f)\n                moveTo(3f, 12f)\n                horizontalLineTo(9f)\n                moveTo(9f, 18f)\n                lineTo(12f, 21f)\n                moveTo(12f, 21f)\n                lineTo(15f, 18f)\n                moveTo(12f, 21f)\n                verticalLineTo(15f)\n                moveTo(15f, 6f)\n                lineTo(12f, 3f)\n                moveTo(12f, 3f)\n                lineTo(9f, 6f)\n                moveTo(12f, 3f)\n                verticalLineTo(9f)\n            }\n        }.build()\n\n        return _DragAndDrop!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _DragAndDrop: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Earth.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Earth: ImageVector\n    get() {\n        if (_Earth != null) {\n            return _Earth!!\n        }\n        _Earth = ImageVector.Builder(\n            name = \"Earth\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(3.6f, 9f)\n                horizontalLineTo(20.4f)\n                moveTo(3.6f, 15f)\n                horizontalLineTo(20.4f)\n                moveTo(11.5f, 3f)\n                curveTo(9.815f, 5.7f, 8.922f, 8.818f, 8.922f, 12f)\n                curveTo(8.922f, 15.182f, 9.815f, 18.3f, 11.5f, 21f)\n                moveTo(12.5f, 3f)\n                curveTo(14.185f, 5.7f, 15.078f, 8.818f, 15.078f, 12f)\n                curveTo(15.078f, 15.182f, 14.185f, 18.3f, 12.5f, 21f)\n                moveTo(3f, 12f)\n                curveTo(3f, 13.182f, 3.233f, 14.352f, 3.685f, 15.444f)\n                curveTo(4.137f, 16.536f, 4.8f, 17.528f, 5.636f, 18.364f)\n                curveTo(6.472f, 19.2f, 7.464f, 19.863f, 8.556f, 20.315f)\n                curveTo(9.648f, 20.767f, 10.818f, 21f, 12f, 21f)\n                curveTo(13.182f, 21f, 14.352f, 20.767f, 15.444f, 20.315f)\n                curveTo(16.536f, 19.863f, 17.528f, 19.2f, 18.364f, 18.364f)\n                curveTo(19.2f, 17.528f, 19.863f, 16.536f, 20.315f, 15.444f)\n                curveTo(20.767f, 14.352f, 21f, 13.182f, 21f, 12f)\n                curveTo(21f, 9.613f, 20.052f, 7.324f, 18.364f, 5.636f)\n                curveTo(16.676f, 3.948f, 14.387f, 3f, 12f, 3f)\n                curveTo(9.613f, 3f, 7.324f, 3.948f, 5.636f, 5.636f)\n                curveTo(3.948f, 7.324f, 3f, 9.613f, 3f, 12f)\n                close()\n            }\n        }.build()\n\n        return _Earth!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Earth: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Edit.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Edit: ImageVector\n    get() {\n        if (_Edit != null) {\n            return _Edit!!\n        }\n        _Edit = ImageVector.Builder(\n            name = \"Edit\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(3f, 17.46f)\n                verticalLineTo(20.5f)\n                curveTo(3f, 20.78f, 3.22f, 21f, 3.5f, 21f)\n                horizontalLineTo(6.54f)\n                curveTo(6.67f, 21f, 6.8f, 20.95f, 6.89f, 20.85f)\n                lineTo(17.81f, 9.94f)\n                lineTo(14.06f, 6.19f)\n                lineTo(3.15f, 17.1f)\n                curveTo(3.05f, 17.2f, 3f, 17.32f, 3f, 17.46f)\n                close()\n                moveTo(20.71f, 7.04f)\n                curveTo(20.803f, 6.947f, 20.876f, 6.838f, 20.926f, 6.717f)\n                curveTo(20.977f, 6.596f, 21.002f, 6.466f, 21.002f, 6.335f)\n                curveTo(21.002f, 6.204f, 20.977f, 6.074f, 20.926f, 5.953f)\n                curveTo(20.876f, 5.832f, 20.803f, 5.723f, 20.71f, 5.63f)\n                lineTo(18.37f, 3.29f)\n                curveTo(18.278f, 3.197f, 18.168f, 3.124f, 18.047f, 3.074f)\n                curveTo(17.926f, 3.023f, 17.796f, 2.998f, 17.665f, 2.998f)\n                curveTo(17.534f, 2.998f, 17.404f, 3.023f, 17.283f, 3.074f)\n                curveTo(17.162f, 3.124f, 17.052f, 3.197f, 16.96f, 3.29f)\n                lineTo(15.13f, 5.12f)\n                lineTo(18.88f, 8.87f)\n                lineTo(20.71f, 7.04f)\n                close()\n            }\n        }.build()\n\n        return _Edit!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Edit: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Exit.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Exit: ImageVector\n    get() {\n        if (_Exit != null) {\n            return _Exit!!\n        }\n        _Exit = ImageVector.Builder(\n            name = \"Exit\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 8f)\n                verticalLineTo(6f)\n                curveTo(14f, 5.47f, 13.789f, 4.961f, 13.414f, 4.586f)\n                curveTo(13.039f, 4.211f, 12.53f, 4f, 12f, 4f)\n                horizontalLineTo(5f)\n                curveTo(4.47f, 4f, 3.961f, 4.211f, 3.586f, 4.586f)\n                curveTo(3.211f, 4.961f, 3f, 5.47f, 3f, 6f)\n                verticalLineTo(18f)\n                curveTo(3f, 18.53f, 3.211f, 19.039f, 3.586f, 19.414f)\n                curveTo(3.961f, 19.789f, 4.47f, 20f, 5f, 20f)\n                horizontalLineTo(12f)\n                curveTo(12.53f, 20f, 13.039f, 19.789f, 13.414f, 19.414f)\n                curveTo(13.789f, 19.039f, 14f, 18.53f, 14f, 18f)\n                verticalLineTo(16f)\n                moveTo(9f, 12f)\n                horizontalLineTo(21f)\n                moveTo(21f, 12f)\n                lineTo(18f, 9f)\n                moveTo(21f, 12f)\n                lineTo(18f, 15f)\n            }\n        }.build()\n\n        return _Exit!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Exit: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/ExternalLink.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.ExternalLink: ImageVector\n    get() {\n        if (_ExternalLink != null) {\n            return _ExternalLink!!\n        }\n        _ExternalLink = ImageVector.Builder(\n            name = \"ExternalLink\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(12f, 6f)\n                horizontalLineTo(6f)\n                curveTo(5.47f, 6f, 4.961f, 6.211f, 4.586f, 6.586f)\n                curveTo(4.211f, 6.961f, 4f, 7.47f, 4f, 8f)\n                verticalLineTo(18f)\n                curveTo(4f, 18.53f, 4.211f, 19.039f, 4.586f, 19.414f)\n                curveTo(4.961f, 19.789f, 5.47f, 20f, 6f, 20f)\n                horizontalLineTo(16f)\n                curveTo(16.53f, 20f, 17.039f, 19.789f, 17.414f, 19.414f)\n                curveTo(17.789f, 19.039f, 18f, 18.53f, 18f, 18f)\n                verticalLineTo(12f)\n                moveTo(11f, 13f)\n                lineTo(20f, 4f)\n                moveTo(20f, 4f)\n                horizontalLineTo(15f)\n                moveTo(20f, 4f)\n                verticalLineTo(9f)\n            }\n        }.build()\n\n        return _ExternalLink!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _ExternalLink: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Fast.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Fast: ImageVector\n    get() {\n        if (_Fast != null) {\n            return _Fast!!\n        }\n        _Fast = ImageVector.Builder(\n            name = \"Fast\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(13f, 3f)\n                verticalLineTo(10f)\n                horizontalLineTo(19f)\n                lineTo(11f, 21f)\n                verticalLineTo(14f)\n                horizontalLineTo(5f)\n                lineTo(13f, 3f)\n                close()\n            }\n        }.build()\n\n        return _Fast!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Fast: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/File.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.File: ImageVector\n    get() {\n        if (_File != null) {\n            return _File!!\n        }\n        _File = ImageVector.Builder(\n            name = \"File\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 3f)\n                verticalLineTo(7f)\n                curveTo(14f, 7.265f, 14.105f, 7.52f, 14.293f, 7.707f)\n                curveTo(14.48f, 7.895f, 14.735f, 8f, 15f, 8f)\n                horizontalLineTo(19f)\n                moveTo(14f, 3f)\n                horizontalLineTo(7f)\n                curveTo(6.47f, 3f, 5.961f, 3.211f, 5.586f, 3.586f)\n                curveTo(5.211f, 3.961f, 5f, 4.47f, 5f, 5f)\n                verticalLineTo(19f)\n                curveTo(5f, 19.53f, 5.211f, 20.039f, 5.586f, 20.414f)\n                curveTo(5.961f, 20.789f, 6.47f, 21f, 7f, 21f)\n                horizontalLineTo(17f)\n                curveTo(17.53f, 21f, 18.039f, 20.789f, 18.414f, 20.414f)\n                curveTo(18.789f, 20.039f, 19f, 19.53f, 19f, 19f)\n                verticalLineTo(8f)\n                moveTo(14f, 3f)\n                lineTo(19f, 8f)\n            }\n        }.build()\n\n        return _File!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _File: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileApplication.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileApplication: ImageVector\n    get() {\n        if (_FileApplication != null) {\n            return _FileApplication!!\n        }\n        _FileApplication = ImageVector.Builder(\n            name = \"FileApplication\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 7f)\n                horizontalLineTo(20f)\n                moveTo(17f, 4f)\n                verticalLineTo(10f)\n                moveTo(4f, 5f)\n                curveTo(4f, 4.735f, 4.105f, 4.48f, 4.293f, 4.293f)\n                curveTo(4.48f, 4.105f, 4.735f, 4f, 5f, 4f)\n                horizontalLineTo(9f)\n                curveTo(9.265f, 4f, 9.52f, 4.105f, 9.707f, 4.293f)\n                curveTo(9.895f, 4.48f, 10f, 4.735f, 10f, 5f)\n                verticalLineTo(9f)\n                curveTo(10f, 9.265f, 9.895f, 9.52f, 9.707f, 9.707f)\n                curveTo(9.52f, 9.895f, 9.265f, 10f, 9f, 10f)\n                horizontalLineTo(5f)\n                curveTo(4.735f, 10f, 4.48f, 9.895f, 4.293f, 9.707f)\n                curveTo(4.105f, 9.52f, 4f, 9.265f, 4f, 9f)\n                verticalLineTo(5f)\n                close()\n                moveTo(4f, 15f)\n                curveTo(4f, 14.735f, 4.105f, 14.48f, 4.293f, 14.293f)\n                curveTo(4.48f, 14.105f, 4.735f, 14f, 5f, 14f)\n                horizontalLineTo(9f)\n                curveTo(9.265f, 14f, 9.52f, 14.105f, 9.707f, 14.293f)\n                curveTo(9.895f, 14.48f, 10f, 14.735f, 10f, 15f)\n                verticalLineTo(19f)\n                curveTo(10f, 19.265f, 9.895f, 19.52f, 9.707f, 19.707f)\n                curveTo(9.52f, 19.895f, 9.265f, 20f, 9f, 20f)\n                horizontalLineTo(5f)\n                curveTo(4.735f, 20f, 4.48f, 19.895f, 4.293f, 19.707f)\n                curveTo(4.105f, 19.52f, 4f, 19.265f, 4f, 19f)\n                verticalLineTo(15f)\n                close()\n                moveTo(14f, 15f)\n                curveTo(14f, 14.735f, 14.105f, 14.48f, 14.293f, 14.293f)\n                curveTo(14.48f, 14.105f, 14.735f, 14f, 15f, 14f)\n                horizontalLineTo(19f)\n                curveTo(19.265f, 14f, 19.52f, 14.105f, 19.707f, 14.293f)\n                curveTo(19.895f, 14.48f, 20f, 14.735f, 20f, 15f)\n                verticalLineTo(19f)\n                curveTo(20f, 19.265f, 19.895f, 19.52f, 19.707f, 19.707f)\n                curveTo(19.52f, 19.895f, 19.265f, 20f, 19f, 20f)\n                horizontalLineTo(15f)\n                curveTo(14.735f, 20f, 14.48f, 19.895f, 14.293f, 19.707f)\n                curveTo(14.105f, 19.52f, 14f, 19.265f, 14f, 19f)\n                verticalLineTo(15f)\n                close()\n            }\n        }.build()\n\n        return _FileApplication!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileApplication: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileDocument.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileDocument: ImageVector\n    get() {\n        if (_FileDocument != null) {\n            return _FileDocument!!\n        }\n        _FileDocument = ImageVector.Builder(\n            name = \"FileDocument\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(7f, 3.75f)\n                curveTo(6.668f, 3.75f, 6.351f, 3.882f, 6.116f, 4.116f)\n                curveTo(5.882f, 4.351f, 5.75f, 4.668f, 5.75f, 5f)\n                verticalLineTo(19f)\n                curveTo(5.75f, 19.331f, 5.882f, 19.649f, 6.116f, 19.884f)\n                curveTo(6.351f, 20.118f, 6.668f, 20.25f, 7f, 20.25f)\n                horizontalLineTo(17f)\n                curveTo(17.331f, 20.25f, 17.649f, 20.118f, 17.884f, 19.884f)\n                curveTo(18.118f, 19.649f, 18.25f, 19.331f, 18.25f, 19f)\n                verticalLineTo(8.75f)\n                horizontalLineTo(15f)\n                curveTo(14.536f, 8.75f, 14.091f, 8.566f, 13.763f, 8.237f)\n                curveTo(13.434f, 7.909f, 13.25f, 7.464f, 13.25f, 7f)\n                verticalLineTo(3.75f)\n                horizontalLineTo(7f)\n                close()\n                moveTo(14.75f, 4.811f)\n                lineTo(17.189f, 7.25f)\n                horizontalLineTo(15f)\n                curveTo(14.934f, 7.25f, 14.87f, 7.224f, 14.823f, 7.177f)\n                curveTo(14.776f, 7.13f, 14.75f, 7.066f, 14.75f, 7f)\n                verticalLineTo(4.811f)\n                close()\n                moveTo(5.055f, 3.055f)\n                curveTo(5.571f, 2.54f, 6.271f, 2.25f, 7f, 2.25f)\n                horizontalLineTo(14f)\n                curveTo(14.199f, 2.25f, 14.39f, 2.329f, 14.53f, 2.47f)\n                lineTo(19.53f, 7.47f)\n                curveTo(19.671f, 7.61f, 19.75f, 7.801f, 19.75f, 8f)\n                verticalLineTo(19f)\n                curveTo(19.75f, 19.729f, 19.46f, 20.429f, 18.944f, 20.944f)\n                curveTo(18.429f, 21.46f, 17.729f, 21.75f, 17f, 21.75f)\n                horizontalLineTo(7f)\n                curveTo(6.271f, 21.75f, 5.571f, 21.46f, 5.055f, 20.944f)\n                curveTo(4.54f, 20.429f, 4.25f, 19.729f, 4.25f, 19f)\n                verticalLineTo(5f)\n                curveTo(4.25f, 4.271f, 4.54f, 3.571f, 5.055f, 3.055f)\n                close()\n                moveTo(8.25f, 9f)\n                curveTo(8.25f, 8.586f, 8.586f, 8.25f, 9f, 8.25f)\n                horizontalLineTo(10f)\n                curveTo(10.414f, 8.25f, 10.75f, 8.586f, 10.75f, 9f)\n                curveTo(10.75f, 9.414f, 10.414f, 9.75f, 10f, 9.75f)\n                horizontalLineTo(9f)\n                curveTo(8.586f, 9.75f, 8.25f, 9.414f, 8.25f, 9f)\n                close()\n                moveTo(8.25f, 13f)\n                curveTo(8.25f, 12.586f, 8.586f, 12.25f, 9f, 12.25f)\n                horizontalLineTo(15f)\n                curveTo(15.414f, 12.25f, 15.75f, 12.586f, 15.75f, 13f)\n                curveTo(15.75f, 13.414f, 15.414f, 13.75f, 15f, 13.75f)\n                horizontalLineTo(9f)\n                curveTo(8.586f, 13.75f, 8.25f, 13.414f, 8.25f, 13f)\n                close()\n                moveTo(8.25f, 17f)\n                curveTo(8.25f, 16.586f, 8.586f, 16.25f, 9f, 16.25f)\n                horizontalLineTo(15f)\n                curveTo(15.414f, 16.25f, 15.75f, 16.586f, 15.75f, 17f)\n                curveTo(15.75f, 17.414f, 15.414f, 17.75f, 15f, 17.75f)\n                horizontalLineTo(9f)\n                curveTo(8.586f, 17.75f, 8.25f, 17.414f, 8.25f, 17f)\n                close()\n            }\n        }.build()\n\n        return _FileDocument!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileDocument: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileMusic.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileMusic: ImageVector\n    get() {\n        if (_FileMusic != null) {\n            return _FileMusic!!\n        }\n        _FileMusic = ImageVector.Builder(\n            name = \"FileMusic\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(8.25f, 4f)\n                curveTo(8.25f, 3.586f, 8.586f, 3.25f, 9f, 3.25f)\n                horizontalLineTo(19f)\n                curveTo(19.414f, 3.25f, 19.75f, 3.586f, 19.75f, 4f)\n                verticalLineTo(17f)\n                curveTo(19.75f, 17.995f, 19.355f, 18.948f, 18.652f, 19.652f)\n                curveTo(17.948f, 20.355f, 16.995f, 20.75f, 16f, 20.75f)\n                curveTo(15.005f, 20.75f, 14.052f, 20.355f, 13.348f, 19.652f)\n                curveTo(12.645f, 18.948f, 12.25f, 17.995f, 12.25f, 17f)\n                curveTo(12.25f, 16.005f, 12.645f, 15.052f, 13.348f, 14.348f)\n                curveTo(14.052f, 13.645f, 15.005f, 13.25f, 16f, 13.25f)\n                curveTo(16.816f, 13.25f, 17.605f, 13.516f, 18.25f, 14f)\n                verticalLineTo(8.75f)\n                horizontalLineTo(9.75f)\n                verticalLineTo(17f)\n                curveTo(9.75f, 17.995f, 9.355f, 18.948f, 8.652f, 19.652f)\n                curveTo(7.948f, 20.355f, 6.995f, 20.75f, 6f, 20.75f)\n                curveTo(5.005f, 20.75f, 4.052f, 20.355f, 3.348f, 19.652f)\n                curveTo(2.645f, 18.948f, 2.25f, 17.995f, 2.25f, 17f)\n                curveTo(2.25f, 16.005f, 2.645f, 15.052f, 3.348f, 14.348f)\n                curveTo(4.052f, 13.645f, 5.005f, 13.25f, 6f, 13.25f)\n                curveTo(6.816f, 13.25f, 7.605f, 13.516f, 8.25f, 14f)\n                verticalLineTo(4f)\n                close()\n                moveTo(9.75f, 7.25f)\n                horizontalLineTo(18.25f)\n                verticalLineTo(4.75f)\n                horizontalLineTo(9.75f)\n                verticalLineTo(7.25f)\n                close()\n                moveTo(8.25f, 17f)\n                curveTo(8.25f, 16.403f, 8.013f, 15.831f, 7.591f, 15.409f)\n                curveTo(7.169f, 14.987f, 6.597f, 14.75f, 6f, 14.75f)\n                curveTo(5.403f, 14.75f, 4.831f, 14.987f, 4.409f, 15.409f)\n                curveTo(3.987f, 15.831f, 3.75f, 16.403f, 3.75f, 17f)\n                curveTo(3.75f, 17.597f, 3.987f, 18.169f, 4.409f, 18.591f)\n                curveTo(4.831f, 19.013f, 5.403f, 19.25f, 6f, 19.25f)\n                curveTo(6.597f, 19.25f, 7.169f, 19.013f, 7.591f, 18.591f)\n                curveTo(8.013f, 18.169f, 8.25f, 17.597f, 8.25f, 17f)\n                close()\n                moveTo(18.25f, 17f)\n                curveTo(18.25f, 16.403f, 18.013f, 15.831f, 17.591f, 15.409f)\n                curveTo(17.169f, 14.987f, 16.597f, 14.75f, 16f, 14.75f)\n                curveTo(15.403f, 14.75f, 14.831f, 14.987f, 14.409f, 15.409f)\n                curveTo(13.987f, 15.831f, 13.75f, 16.403f, 13.75f, 17f)\n                curveTo(13.75f, 17.597f, 13.987f, 18.169f, 14.409f, 18.591f)\n                curveTo(14.831f, 19.013f, 15.403f, 19.25f, 16f, 19.25f)\n                curveTo(16.597f, 19.25f, 17.169f, 19.013f, 17.591f, 18.591f)\n                curveTo(18.013f, 18.169f, 18.25f, 17.597f, 18.25f, 17f)\n                close()\n            }\n        }.build()\n\n        return _FileMusic!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileMusic: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FilePicture.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FilePicture: ImageVector\n    get() {\n        if (_FilePicture != null) {\n            return _FilePicture!!\n        }\n        _FilePicture = ImageVector.Builder(\n            name = \"FilePicture\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(6f, 3.75f)\n                curveTo(5.403f, 3.75f, 4.831f, 3.987f, 4.409f, 4.409f)\n                curveTo(3.987f, 4.831f, 3.75f, 5.403f, 3.75f, 6f)\n                verticalLineTo(14.189f)\n                lineTo(7.47f, 10.47f)\n                lineTo(7.48f, 10.46f)\n                curveTo(8.057f, 9.905f, 8.753f, 9.58f, 9.5f, 9.58f)\n                curveTo(10.247f, 9.58f, 10.943f, 9.905f, 11.52f, 10.46f)\n                lineTo(11.53f, 10.47f)\n                lineTo(14f, 12.939f)\n                lineTo(14.47f, 12.47f)\n                lineTo(14.48f, 12.46f)\n                curveTo(15.057f, 11.905f, 15.753f, 11.58f, 16.5f, 11.58f)\n                curveTo(17.247f, 11.58f, 17.943f, 11.905f, 18.52f, 12.46f)\n                lineTo(18.53f, 12.47f)\n                lineTo(20.25f, 14.189f)\n                verticalLineTo(6f)\n                curveTo(20.25f, 5.403f, 20.013f, 4.831f, 19.591f, 4.409f)\n                curveTo(19.169f, 3.987f, 18.597f, 3.75f, 18f, 3.75f)\n                horizontalLineTo(6f)\n                close()\n                moveTo(21.75f, 6f)\n                curveTo(21.75f, 5.005f, 21.355f, 4.052f, 20.652f, 3.348f)\n                curveTo(19.948f, 2.645f, 18.995f, 2.25f, 18f, 2.25f)\n                horizontalLineTo(6f)\n                curveTo(5.005f, 2.25f, 4.052f, 2.645f, 3.348f, 3.348f)\n                curveTo(2.645f, 4.052f, 2.25f, 5.005f, 2.25f, 6f)\n                verticalLineTo(18f)\n                curveTo(2.25f, 18.995f, 2.645f, 19.948f, 3.348f, 20.652f)\n                curveTo(4.052f, 21.355f, 5.005f, 21.75f, 6f, 21.75f)\n                horizontalLineTo(18f)\n                curveTo(18.995f, 21.75f, 19.948f, 21.355f, 20.652f, 20.652f)\n                curveTo(21.355f, 19.948f, 21.75f, 18.995f, 21.75f, 18f)\n                verticalLineTo(6f)\n                close()\n                moveTo(20.25f, 16.311f)\n                lineTo(17.475f, 13.536f)\n                curveTo(17.125f, 13.201f, 16.788f, 13.08f, 16.5f, 13.08f)\n                curveTo(16.212f, 13.08f, 15.875f, 13.201f, 15.525f, 13.536f)\n                lineTo(15.061f, 14f)\n                lineTo(16.53f, 15.47f)\n                curveTo(16.823f, 15.763f, 16.823f, 16.237f, 16.53f, 16.53f)\n                curveTo(16.237f, 16.823f, 15.763f, 16.823f, 15.47f, 16.53f)\n                lineTo(10.475f, 11.536f)\n                curveTo(10.125f, 11.201f, 9.788f, 11.08f, 9.5f, 11.08f)\n                curveTo(9.212f, 11.08f, 8.875f, 11.201f, 8.525f, 11.536f)\n                lineTo(3.75f, 16.311f)\n                verticalLineTo(18f)\n                curveTo(3.75f, 18.597f, 3.987f, 19.169f, 4.409f, 19.591f)\n                curveTo(4.831f, 20.013f, 5.403f, 20.25f, 6f, 20.25f)\n                horizontalLineTo(18f)\n                curveTo(18.597f, 20.25f, 19.169f, 20.013f, 19.591f, 19.591f)\n                curveTo(20.013f, 19.169f, 20.25f, 18.597f, 20.25f, 18f)\n                verticalLineTo(16.311f)\n                close()\n                moveTo(14.25f, 8f)\n                curveTo(14.25f, 7.586f, 14.586f, 7.25f, 15f, 7.25f)\n                horizontalLineTo(15.01f)\n                curveTo(15.424f, 7.25f, 15.76f, 7.586f, 15.76f, 8f)\n                curveTo(15.76f, 8.414f, 15.424f, 8.75f, 15.01f, 8.75f)\n                horizontalLineTo(15f)\n                curveTo(14.586f, 8.75f, 14.25f, 8.414f, 14.25f, 8f)\n                close()\n            }\n        }.build()\n\n        return _FilePicture!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FilePicture: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileUnknown.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileUnknown: ImageVector\n    get() {\n        if (_FileUnknown != null) {\n            return _FileUnknown!!\n        }\n        _FileUnknown = ImageVector.Builder(\n            name = \"FileUnknown\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 3f)\n                verticalLineTo(7f)\n                curveTo(14f, 7.265f, 14.105f, 7.52f, 14.293f, 7.707f)\n                curveTo(14.48f, 7.895f, 14.735f, 8f, 15f, 8f)\n                horizontalLineTo(19f)\n                moveTo(14f, 3f)\n                horizontalLineTo(7f)\n                curveTo(6.47f, 3f, 5.961f, 3.211f, 5.586f, 3.586f)\n                curveTo(5.211f, 3.961f, 5f, 4.47f, 5f, 5f)\n                verticalLineTo(19f)\n                curveTo(5f, 19.53f, 5.211f, 20.039f, 5.586f, 20.414f)\n                curveTo(5.961f, 20.789f, 6.47f, 21f, 7f, 21f)\n                horizontalLineTo(17f)\n                curveTo(17.53f, 21f, 18.039f, 20.789f, 18.414f, 20.414f)\n                curveTo(18.789f, 20.039f, 19f, 19.53f, 19f, 19f)\n                verticalLineTo(8f)\n                moveTo(14f, 3f)\n                lineTo(19f, 8f)\n                moveTo(12f, 17f)\n                verticalLineTo(17.01f)\n                moveTo(12f, 14f)\n                curveTo(12.252f, 14f, 12.499f, 13.937f, 12.72f, 13.816f)\n                curveTo(12.941f, 13.695f, 13.128f, 13.521f, 13.264f, 13.309f)\n                curveTo(13.4f, 13.097f, 13.48f, 12.854f, 13.497f, 12.603f)\n                curveTo(13.514f, 12.352f, 13.468f, 12.101f, 13.363f, 11.872f)\n                curveTo(13.258f, 11.644f, 13.097f, 11.445f, 12.894f, 11.295f)\n                curveTo(12.692f, 11.145f, 12.456f, 11.049f, 12.206f, 11.014f)\n                curveTo(11.957f, 10.98f, 11.703f, 11.009f, 11.468f, 11.098f)\n                curveTo(11.232f, 11.187f, 11.023f, 11.335f, 10.86f, 11.526f)\n            }\n        }.build()\n\n        return _FileUnknown!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileUnknown: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileVideo.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileVideo: ImageVector\n    get() {\n        if (_FileVideo != null) {\n            return _FileVideo!!\n        }\n        _FileVideo = ImageVector.Builder(\n            name = \"FileVideo\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(15f, 10f)\n                lineTo(19.553f, 7.724f)\n                curveTo(19.705f, 7.648f, 19.875f, 7.612f, 20.045f, 7.62f)\n                curveTo(20.215f, 7.627f, 20.381f, 7.678f, 20.526f, 7.768f)\n                curveTo(20.671f, 7.857f, 20.79f, 7.982f, 20.873f, 8.131f)\n                curveTo(20.956f, 8.28f, 21f, 8.448f, 21f, 8.618f)\n                verticalLineTo(15.382f)\n                curveTo(21f, 15.552f, 20.956f, 15.72f, 20.873f, 15.869f)\n                curveTo(20.79f, 16.017f, 20.671f, 16.143f, 20.526f, 16.232f)\n                curveTo(20.381f, 16.322f, 20.215f, 16.373f, 20.045f, 16.381f)\n                curveTo(19.875f, 16.388f, 19.705f, 16.352f, 19.553f, 16.276f)\n                lineTo(15f, 14f)\n                verticalLineTo(10f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(3f, 8f)\n                curveTo(3f, 7.47f, 3.211f, 6.961f, 3.586f, 6.586f)\n                curveTo(3.961f, 6.211f, 4.47f, 6f, 5f, 6f)\n                horizontalLineTo(13f)\n                curveTo(13.53f, 6f, 14.039f, 6.211f, 14.414f, 6.586f)\n                curveTo(14.789f, 6.961f, 15f, 7.47f, 15f, 8f)\n                verticalLineTo(16f)\n                curveTo(15f, 16.53f, 14.789f, 17.039f, 14.414f, 17.414f)\n                curveTo(14.039f, 17.789f, 13.53f, 18f, 13f, 18f)\n                horizontalLineTo(5f)\n                curveTo(4.47f, 18f, 3.961f, 17.789f, 3.586f, 17.414f)\n                curveTo(3.211f, 17.039f, 3f, 16.53f, 3f, 16f)\n                verticalLineTo(8f)\n                close()\n            }\n        }.build()\n\n        return _FileVideo!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileVideo: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FileZip.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FileZip: ImageVector\n    get() {\n        if (_FileZip != null) {\n            return _FileZip!!\n        }\n        _FileZip = ImageVector.Builder(\n            name = \"FileZip\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(7f, 3.75f)\n                curveTo(6.668f, 3.75f, 6.351f, 3.882f, 6.116f, 4.116f)\n                curveTo(5.882f, 4.351f, 5.75f, 4.668f, 5.75f, 5f)\n                lineTo(5.75f, 19.001f)\n                curveTo(5.75f, 19.221f, 5.807f, 19.437f, 5.917f, 19.627f)\n                curveTo(6.027f, 19.817f, 6.185f, 19.976f, 6.375f, 20.086f)\n                curveTo(6.734f, 20.293f, 6.857f, 20.751f, 6.65f, 21.11f)\n                curveTo(6.442f, 21.469f, 5.984f, 21.592f, 5.625f, 21.385f)\n                curveTo(5.206f, 21.143f, 4.859f, 20.795f, 4.617f, 20.376f)\n                curveTo(4.376f, 19.958f, 4.249f, 19.483f, 4.25f, 19f)\n                curveTo(4.25f, 19f, 4.25f, 18.999f, 4.25f, 19f)\n                verticalLineTo(5f)\n                curveTo(4.25f, 4.271f, 4.54f, 3.571f, 5.055f, 3.055f)\n                curveTo(5.571f, 2.54f, 6.271f, 2.25f, 7f, 2.25f)\n                horizontalLineTo(14f)\n                curveTo(14.199f, 2.25f, 14.39f, 2.329f, 14.53f, 2.47f)\n                lineTo(19.53f, 7.47f)\n                curveTo(19.671f, 7.61f, 19.75f, 7.801f, 19.75f, 8f)\n                verticalLineTo(19f)\n                curveTo(19.75f, 19.729f, 19.46f, 20.429f, 18.944f, 20.944f)\n                curveTo(18.429f, 21.46f, 17.729f, 21.75f, 17f, 21.75f)\n                horizontalLineTo(16f)\n                curveTo(15.586f, 21.75f, 15.25f, 21.414f, 15.25f, 21f)\n                curveTo(15.25f, 20.586f, 15.586f, 20.25f, 16f, 20.25f)\n                horizontalLineTo(17f)\n                curveTo(17.331f, 20.25f, 17.649f, 20.118f, 17.884f, 19.884f)\n                curveTo(18.118f, 19.649f, 18.25f, 19.331f, 18.25f, 19f)\n                verticalLineTo(8.311f)\n                lineTo(13.689f, 3.75f)\n                horizontalLineTo(7f)\n                close()\n                moveTo(9.25f, 5f)\n                curveTo(9.25f, 4.586f, 9.586f, 4.25f, 10f, 4.25f)\n                horizontalLineTo(11f)\n                curveTo(11.414f, 4.25f, 11.75f, 4.586f, 11.75f, 5f)\n                curveTo(11.75f, 5.414f, 11.414f, 5.75f, 11f, 5.75f)\n                horizontalLineTo(10f)\n                curveTo(9.586f, 5.75f, 9.25f, 5.414f, 9.25f, 5f)\n                close()\n                moveTo(11.25f, 7f)\n                curveTo(11.25f, 6.586f, 11.586f, 6.25f, 12f, 6.25f)\n                horizontalLineTo(13f)\n                curveTo(13.414f, 6.25f, 13.75f, 6.586f, 13.75f, 7f)\n                curveTo(13.75f, 7.414f, 13.414f, 7.75f, 13f, 7.75f)\n                horizontalLineTo(12f)\n                curveTo(11.586f, 7.75f, 11.25f, 7.414f, 11.25f, 7f)\n                close()\n                moveTo(9.25f, 9f)\n                curveTo(9.25f, 8.586f, 9.586f, 8.25f, 10f, 8.25f)\n                horizontalLineTo(11f)\n                curveTo(11.414f, 8.25f, 11.75f, 8.586f, 11.75f, 9f)\n                curveTo(11.75f, 9.414f, 11.414f, 9.75f, 11f, 9.75f)\n                horizontalLineTo(10f)\n                curveTo(9.586f, 9.75f, 9.25f, 9.414f, 9.25f, 9f)\n                close()\n                moveTo(11.25f, 11f)\n                curveTo(11.25f, 10.586f, 11.586f, 10.25f, 12f, 10.25f)\n                horizontalLineTo(13f)\n                curveTo(13.414f, 10.25f, 13.75f, 10.586f, 13.75f, 11f)\n                curveTo(13.75f, 11.414f, 13.414f, 11.75f, 13f, 11.75f)\n                horizontalLineTo(12f)\n                curveTo(11.586f, 11.75f, 11.25f, 11.414f, 11.25f, 11f)\n                close()\n                moveTo(9.25f, 13f)\n                curveTo(9.25f, 12.586f, 9.586f, 12.25f, 10f, 12.25f)\n                horizontalLineTo(11f)\n                curveTo(11.414f, 12.25f, 11.75f, 12.586f, 11.75f, 13f)\n                curveTo(11.75f, 13.414f, 11.414f, 13.75f, 11f, 13.75f)\n                horizontalLineTo(10f)\n                curveTo(9.586f, 13.75f, 9.25f, 13.414f, 9.25f, 13f)\n                close()\n                moveTo(11.25f, 15f)\n                curveTo(11.25f, 14.586f, 11.586f, 14.25f, 12f, 14.25f)\n                horizontalLineTo(13f)\n                curveTo(13.414f, 14.25f, 13.75f, 14.586f, 13.75f, 15f)\n                curveTo(13.75f, 15.414f, 13.414f, 15.75f, 13f, 15.75f)\n                horizontalLineTo(12f)\n                curveTo(11.586f, 15.75f, 11.25f, 15.414f, 11.25f, 15f)\n                close()\n                moveTo(11f, 17.75f)\n                curveTo(10.668f, 17.75f, 10.351f, 17.882f, 10.116f, 18.116f)\n                curveTo(9.882f, 18.351f, 9.75f, 18.669f, 9.75f, 19f)\n                verticalLineTo(21f)\n                curveTo(9.75f, 21.066f, 9.776f, 21.13f, 9.823f, 21.177f)\n                curveTo(9.87f, 21.224f, 9.934f, 21.25f, 10f, 21.25f)\n                horizontalLineTo(12f)\n                curveTo(12.066f, 21.25f, 12.13f, 21.224f, 12.177f, 21.177f)\n                curveTo(12.224f, 21.13f, 12.25f, 21.066f, 12.25f, 21f)\n                verticalLineTo(19f)\n                curveTo(12.25f, 18.669f, 12.118f, 18.351f, 11.884f, 18.116f)\n                curveTo(11.649f, 17.882f, 11.332f, 17.75f, 11f, 17.75f)\n                close()\n                moveTo(9.055f, 17.056f)\n                curveTo(9.571f, 16.54f, 10.271f, 16.25f, 11f, 16.25f)\n                curveTo(11.729f, 16.25f, 12.429f, 16.54f, 12.944f, 17.056f)\n                curveTo(13.46f, 17.571f, 13.75f, 18.271f, 13.75f, 19f)\n                verticalLineTo(21f)\n                curveTo(13.75f, 21.464f, 13.566f, 21.909f, 13.237f, 22.237f)\n                curveTo(12.909f, 22.566f, 12.464f, 22.75f, 12f, 22.75f)\n                horizontalLineTo(10f)\n                curveTo(9.536f, 22.75f, 9.091f, 22.566f, 8.763f, 22.237f)\n                curveTo(8.434f, 21.909f, 8.25f, 21.464f, 8.25f, 21f)\n                verticalLineTo(19f)\n                curveTo(8.25f, 18.271f, 8.54f, 17.571f, 9.055f, 17.056f)\n                close()\n            }\n        }.build()\n\n        return _FileZip!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FileZip: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Flag.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Flag: ImageVector\n    get() {\n        if (_Flag != null) {\n            return _Flag!!\n        }\n        _Flag = ImageVector.Builder(\n            name = \"Flag\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(5f, 14f)\n                curveTo(5.935f, 13.084f, 7.191f, 12.571f, 8.5f, 12.571f)\n                curveTo(9.809f, 12.571f, 11.065f, 13.084f, 12f, 14f)\n                curveTo(12.935f, 14.916f, 14.191f, 15.429f, 15.5f, 15.429f)\n                curveTo(16.809f, 15.429f, 18.065f, 14.916f, 19f, 14f)\n                verticalLineTo(5f)\n                curveTo(18.065f, 5.916f, 16.809f, 6.429f, 15.5f, 6.429f)\n                curveTo(14.191f, 6.429f, 12.935f, 5.916f, 12f, 5f)\n                curveTo(11.065f, 4.084f, 9.809f, 3.571f, 8.5f, 3.571f)\n                curveTo(7.191f, 3.571f, 5.935f, 4.084f, 5f, 5f)\n                verticalLineTo(14f)\n                close()\n                moveTo(5f, 14f)\n                verticalLineTo(21f)\n            }\n        }.build()\n\n        return _Flag!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Flag: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Folder.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Folder: ImageVector\n    get() {\n        if (_Folder != null) {\n            return _Folder!!\n        }\n        _Folder = ImageVector.Builder(\n            name = \"Folder\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(5f, 4f)\n                horizontalLineTo(9f)\n                lineTo(12f, 7f)\n                horizontalLineTo(19f)\n                curveTo(19.53f, 7f, 20.039f, 7.211f, 20.414f, 7.586f)\n                curveTo(20.789f, 7.961f, 21f, 8.47f, 21f, 9f)\n                verticalLineTo(17f)\n                curveTo(21f, 17.53f, 20.789f, 18.039f, 20.414f, 18.414f)\n                curveTo(20.039f, 18.789f, 19.53f, 19f, 19f, 19f)\n                horizontalLineTo(5f)\n                curveTo(4.47f, 19f, 3.961f, 18.789f, 3.586f, 18.414f)\n                curveTo(3.211f, 18.039f, 3f, 17.53f, 3f, 17f)\n                verticalLineTo(6f)\n                curveTo(3f, 5.47f, 3.211f, 4.961f, 3.586f, 4.586f)\n                curveTo(3.961f, 4.211f, 4.47f, 4f, 5f, 4f)\n                close()\n            }\n        }.build()\n\n        return _Folder!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Folder: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FolderFinished.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FolderFinished: ImageVector\n    get() {\n        if (_FolderFinished != null) {\n            return _FolderFinished!!\n        }\n        _FolderFinished = ImageVector.Builder(\n            name = \"FolderFinished\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(11f, 19f)\n                horizontalLineTo(5f)\n                curveTo(4.47f, 19f, 3.961f, 18.789f, 3.586f, 18.414f)\n                curveTo(3.211f, 18.039f, 3f, 17.53f, 3f, 17f)\n                verticalLineTo(6f)\n                curveTo(3f, 5.47f, 3.211f, 4.961f, 3.586f, 4.586f)\n                curveTo(3.961f, 4.211f, 4.47f, 4f, 5f, 4f)\n                horizontalLineTo(9f)\n                lineTo(12f, 7f)\n                horizontalLineTo(19f)\n                curveTo(19.53f, 7f, 20.039f, 7.211f, 20.414f, 7.586f)\n                curveTo(20.789f, 7.961f, 21f, 8.47f, 21f, 9f)\n                verticalLineTo(13f)\n                moveTo(15f, 19f)\n                lineTo(17f, 21f)\n                lineTo(21f, 17f)\n            }\n        }.build()\n\n        return _FolderFinished!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FolderFinished: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/FolderUnfinished.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.FolderUnfinished: ImageVector\n    get() {\n        if (_FolderUnfinished != null) {\n            return _FolderUnfinished!!\n        }\n        _FolderUnfinished = ImageVector.Builder(\n            name = \"FolderUnfinished\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(12f, 19f)\n                horizontalLineTo(5f)\n                curveTo(4.47f, 19f, 3.961f, 18.789f, 3.586f, 18.414f)\n                curveTo(3.211f, 18.039f, 3f, 17.53f, 3f, 17f)\n                verticalLineTo(6f)\n                curveTo(3f, 5.47f, 3.211f, 4.961f, 3.586f, 4.586f)\n                curveTo(3.961f, 4.211f, 4.47f, 4f, 5f, 4f)\n                horizontalLineTo(9f)\n                lineTo(12f, 7f)\n                horizontalLineTo(19f)\n                curveTo(19.53f, 7f, 20.039f, 7.211f, 20.414f, 7.586f)\n                curveTo(20.789f, 7.961f, 21f, 8.47f, 21f, 9f)\n                verticalLineTo(12.5f)\n                moveTo(19f, 16f)\n                verticalLineTo(22f)\n                moveTo(19f, 22f)\n                lineTo(22f, 19f)\n                moveTo(19f, 22f)\n                lineTo(16f, 19f)\n            }\n        }.build()\n\n        return _FolderUnfinished!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _FolderUnfinished: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Grip.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Grip: ImageVector\n    get() {\n        if (_Grip != null) {\n            return _Grip!!\n        }\n        _Grip = ImageVector.Builder(\n            name = \"Grip\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8f, 5f)\n                curveTo(8f, 5.265f, 8.105f, 5.52f, 8.293f, 5.707f)\n                curveTo(8.48f, 5.895f, 8.735f, 6f, 9f, 6f)\n                curveTo(9.265f, 6f, 9.52f, 5.895f, 9.707f, 5.707f)\n                curveTo(9.895f, 5.52f, 10f, 5.265f, 10f, 5f)\n                curveTo(10f, 4.735f, 9.895f, 4.48f, 9.707f, 4.293f)\n                curveTo(9.52f, 4.105f, 9.265f, 4f, 9f, 4f)\n                curveTo(8.735f, 4f, 8.48f, 4.105f, 8.293f, 4.293f)\n                curveTo(8.105f, 4.48f, 8f, 4.735f, 8f, 5f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8f, 12f)\n                curveTo(8f, 12.265f, 8.105f, 12.52f, 8.293f, 12.707f)\n                curveTo(8.48f, 12.895f, 8.735f, 13f, 9f, 13f)\n                curveTo(9.265f, 13f, 9.52f, 12.895f, 9.707f, 12.707f)\n                curveTo(9.895f, 12.52f, 10f, 12.265f, 10f, 12f)\n                curveTo(10f, 11.735f, 9.895f, 11.48f, 9.707f, 11.293f)\n                curveTo(9.52f, 11.105f, 9.265f, 11f, 9f, 11f)\n                curveTo(8.735f, 11f, 8.48f, 11.105f, 8.293f, 11.293f)\n                curveTo(8.105f, 11.48f, 8f, 11.735f, 8f, 12f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8f, 19f)\n                curveTo(8f, 19.265f, 8.105f, 19.52f, 8.293f, 19.707f)\n                curveTo(8.48f, 19.895f, 8.735f, 20f, 9f, 20f)\n                curveTo(9.265f, 20f, 9.52f, 19.895f, 9.707f, 19.707f)\n                curveTo(9.895f, 19.52f, 10f, 19.265f, 10f, 19f)\n                curveTo(10f, 18.735f, 9.895f, 18.48f, 9.707f, 18.293f)\n                curveTo(9.52f, 18.105f, 9.265f, 18f, 9f, 18f)\n                curveTo(8.735f, 18f, 8.48f, 18.105f, 8.293f, 18.293f)\n                curveTo(8.105f, 18.48f, 8f, 18.735f, 8f, 19f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 5f)\n                curveTo(14f, 5.265f, 14.105f, 5.52f, 14.293f, 5.707f)\n                curveTo(14.48f, 5.895f, 14.735f, 6f, 15f, 6f)\n                curveTo(15.265f, 6f, 15.52f, 5.895f, 15.707f, 5.707f)\n                curveTo(15.895f, 5.52f, 16f, 5.265f, 16f, 5f)\n                curveTo(16f, 4.735f, 15.895f, 4.48f, 15.707f, 4.293f)\n                curveTo(15.52f, 4.105f, 15.265f, 4f, 15f, 4f)\n                curveTo(14.735f, 4f, 14.48f, 4.105f, 14.293f, 4.293f)\n                curveTo(14.105f, 4.48f, 14f, 4.735f, 14f, 5f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 12f)\n                curveTo(14f, 12.265f, 14.105f, 12.52f, 14.293f, 12.707f)\n                curveTo(14.48f, 12.895f, 14.735f, 13f, 15f, 13f)\n                curveTo(15.265f, 13f, 15.52f, 12.895f, 15.707f, 12.707f)\n                curveTo(15.895f, 12.52f, 16f, 12.265f, 16f, 12f)\n                curveTo(16f, 11.735f, 15.895f, 11.48f, 15.707f, 11.293f)\n                curveTo(15.52f, 11.105f, 15.265f, 11f, 15f, 11f)\n                curveTo(14.735f, 11f, 14.48f, 11.105f, 14.293f, 11.293f)\n                curveTo(14.105f, 11.48f, 14f, 11.735f, 14f, 12f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 19f)\n                curveTo(14f, 19.265f, 14.105f, 19.52f, 14.293f, 19.707f)\n                curveTo(14.48f, 19.895f, 14.735f, 20f, 15f, 20f)\n                curveTo(15.265f, 20f, 15.52f, 19.895f, 15.707f, 19.707f)\n                curveTo(15.895f, 19.52f, 16f, 19.265f, 16f, 19f)\n                curveTo(16f, 18.735f, 15.895f, 18.48f, 15.707f, 18.293f)\n                curveTo(15.52f, 18.105f, 15.265f, 18f, 15f, 18f)\n                curveTo(14.735f, 18f, 14.48f, 18.105f, 14.293f, 18.293f)\n                curveTo(14.105f, 18.48f, 14f, 18.735f, 14f, 19f)\n                close()\n            }\n        }.build()\n\n        return _Grip!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Grip: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Group.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Group: ImageVector\n    get() {\n        if (_Group != null) {\n            return _Group!!\n        }\n        _Group = ImageVector.Builder(\n            name = \"Group\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 1.5f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(7f, 18f)\n                verticalLineTo(17f)\n                curveTo(7f, 15.674f, 7.527f, 14.402f, 8.464f, 13.465f)\n                curveTo(9.402f, 12.527f, 10.674f, 12f, 12f, 12f)\n                moveTo(12f, 12f)\n                curveTo(13.326f, 12f, 14.598f, 12.527f, 15.535f, 13.465f)\n                curveTo(16.473f, 14.402f, 17f, 15.674f, 17f, 17f)\n                verticalLineTo(18f)\n                moveTo(12f, 12f)\n                curveTo(12.796f, 12f, 13.559f, 11.684f, 14.121f, 11.121f)\n                curveTo(14.684f, 10.559f, 15f, 9.796f, 15f, 9f)\n                curveTo(15f, 8.204f, 14.684f, 7.441f, 14.121f, 6.879f)\n                curveTo(13.559f, 6.316f, 12.796f, 6f, 12f, 6f)\n                curveTo(11.204f, 6f, 10.441f, 6.316f, 9.879f, 6.879f)\n                curveTo(9.316f, 7.441f, 9f, 8.204f, 9f, 9f)\n                curveTo(9f, 9.796f, 9.316f, 10.559f, 9.879f, 11.121f)\n                curveTo(10.441f, 11.684f, 11.204f, 12f, 12f, 12f)\n                close()\n                moveTo(1f, 18f)\n                verticalLineTo(17f)\n                curveTo(1f, 16.204f, 1.316f, 15.441f, 1.879f, 14.879f)\n                curveTo(2.441f, 14.316f, 3.204f, 14f, 4f, 14f)\n                moveTo(4f, 14f)\n                curveTo(4.53f, 14f, 5.039f, 13.789f, 5.414f, 13.414f)\n                curveTo(5.789f, 13.039f, 6f, 12.53f, 6f, 12f)\n                curveTo(6f, 11.47f, 5.789f, 10.961f, 5.414f, 10.586f)\n                curveTo(5.039f, 10.211f, 4.53f, 10f, 4f, 10f)\n                curveTo(3.47f, 10f, 2.961f, 10.211f, 2.586f, 10.586f)\n                curveTo(2.211f, 10.961f, 2f, 11.47f, 2f, 12f)\n                curveTo(2f, 12.53f, 2.211f, 13.039f, 2.586f, 13.414f)\n                curveTo(2.961f, 13.789f, 3.47f, 14f, 4f, 14f)\n                close()\n                moveTo(23f, 18f)\n                verticalLineTo(17f)\n                curveTo(23f, 16.204f, 22.684f, 15.441f, 22.121f, 14.879f)\n                curveTo(21.559f, 14.316f, 20.796f, 14f, 20f, 14f)\n                moveTo(20f, 14f)\n                curveTo(20.53f, 14f, 21.039f, 13.789f, 21.414f, 13.414f)\n                curveTo(21.789f, 13.039f, 22f, 12.53f, 22f, 12f)\n                curveTo(22f, 11.47f, 21.789f, 10.961f, 21.414f, 10.586f)\n                curveTo(21.039f, 10.211f, 20.53f, 10f, 20f, 10f)\n                curveTo(19.47f, 10f, 18.961f, 10.211f, 18.586f, 10.586f)\n                curveTo(18.211f, 10.961f, 18f, 11.47f, 18f, 12f)\n                curveTo(18f, 12.53f, 18.211f, 13.039f, 18.586f, 13.414f)\n                curveTo(18.961f, 13.789f, 19.47f, 14f, 20f, 14f)\n                close()\n            }\n        }.build()\n\n        return _Group!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Group: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Hearth.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Hearth: ImageVector\n    get() {\n        if (_Hearth != null) {\n            return _Hearth!!\n        }\n        _Hearth = ImageVector.Builder(\n            name = \"Hearth\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(19.5f, 12.572f)\n                lineTo(12f, 20f)\n                lineTo(4.5f, 12.572f)\n                curveTo(4.005f, 12.091f, 3.616f, 11.512f, 3.356f, 10.873f)\n                curveTo(3.096f, 10.233f, 2.971f, 9.547f, 2.989f, 8.857f)\n                curveTo(3.007f, 8.167f, 3.168f, 7.488f, 3.461f, 6.863f)\n                curveTo(3.755f, 6.239f, 4.174f, 5.681f, 4.694f, 5.227f)\n                curveTo(5.213f, 4.772f, 5.821f, 4.43f, 6.479f, 4.221f)\n                curveTo(7.137f, 4.013f, 7.831f, 3.944f, 8.517f, 4.017f)\n                curveTo(9.204f, 4.09f, 9.868f, 4.305f, 10.467f, 4.647f)\n                curveTo(11.066f, 4.989f, 11.588f, 5.452f, 12f, 6.006f)\n                curveTo(12.414f, 5.456f, 12.936f, 4.997f, 13.535f, 4.659f)\n                curveTo(14.134f, 4.32f, 14.797f, 4.108f, 15.481f, 4.038f)\n                curveTo(16.165f, 3.967f, 16.857f, 4.038f, 17.513f, 4.246f)\n                curveTo(18.169f, 4.455f, 18.774f, 4.797f, 19.292f, 5.25f)\n                curveTo(19.809f, 5.704f, 20.228f, 6.259f, 20.521f, 6.882f)\n                curveTo(20.813f, 7.504f, 20.975f, 8.181f, 20.994f, 8.869f)\n                curveTo(21.014f, 9.557f, 20.891f, 10.241f, 20.634f, 10.879f)\n                curveTo(20.377f, 11.517f, 19.991f, 12.096f, 19.5f, 12.578f)\n            }\n        }.build()\n\n        return _Hearth!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Hearth: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Info.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Info: ImageVector\n    get() {\n        if (_Info != null) {\n            return _Info!!\n        }\n        _Info = ImageVector.Builder(\n            name = \"Info\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(12f, 9f)\n                horizontalLineTo(12.01f)\n                moveTo(11f, 12f)\n                horizontalLineTo(12f)\n                verticalLineTo(16f)\n                horizontalLineTo(13f)\n                moveTo(3f, 12f)\n                curveTo(3f, 13.182f, 3.233f, 14.352f, 3.685f, 15.444f)\n                curveTo(4.137f, 16.536f, 4.8f, 17.528f, 5.636f, 18.364f)\n                curveTo(6.472f, 19.2f, 7.464f, 19.863f, 8.556f, 20.315f)\n                curveTo(9.648f, 20.767f, 10.818f, 21f, 12f, 21f)\n                curveTo(13.182f, 21f, 14.352f, 20.767f, 15.444f, 20.315f)\n                curveTo(16.536f, 19.863f, 17.528f, 19.2f, 18.364f, 18.364f)\n                curveTo(19.2f, 17.528f, 19.863f, 16.536f, 20.315f, 15.444f)\n                curveTo(20.767f, 14.352f, 21f, 13.182f, 21f, 12f)\n                curveTo(21f, 9.613f, 20.052f, 7.324f, 18.364f, 5.636f)\n                curveTo(16.676f, 3.948f, 14.387f, 3f, 12f, 3f)\n                curveTo(9.613f, 3f, 7.324f, 3.948f, 5.636f, 5.636f)\n                curveTo(3.948f, 7.324f, 3f, 9.613f, 3f, 12f)\n                close()\n            }\n        }.build()\n\n        return _Info!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Info: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Language.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Language: ImageVector\n    get() {\n        if (_Language != null) {\n            return _Language!!\n        }\n        _Language = ImageVector.Builder(\n            name = \"Language\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(12.87f, 15.07f)\n                lineTo(10.33f, 12.56f)\n                lineTo(10.36f, 12.53f)\n                curveTo(12.055f, 10.648f, 13.321f, 8.42f, 14.07f, 6f)\n                horizontalLineTo(17f)\n                verticalLineTo(4f)\n                horizontalLineTo(10f)\n                verticalLineTo(2f)\n                horizontalLineTo(8f)\n                verticalLineTo(4f)\n                horizontalLineTo(1f)\n                verticalLineTo(6f)\n                horizontalLineTo(12.17f)\n                curveTo(11.5f, 7.92f, 10.44f, 9.75f, 9f, 11.35f)\n                curveTo(8.07f, 10.32f, 7.3f, 9.19f, 6.69f, 8f)\n                horizontalLineTo(4.69f)\n                curveTo(5.42f, 9.63f, 6.42f, 11.17f, 7.67f, 12.56f)\n                lineTo(2.58f, 17.58f)\n                lineTo(4f, 19f)\n                lineTo(9f, 14f)\n                lineTo(12.11f, 17.11f)\n                lineTo(12.87f, 15.07f)\n                close()\n                moveTo(18.5f, 10f)\n                horizontalLineTo(16.5f)\n                lineTo(12f, 22f)\n                horizontalLineTo(14f)\n                lineTo(15.12f, 19f)\n                horizontalLineTo(19.87f)\n                lineTo(21f, 22f)\n                horizontalLineTo(23f)\n                lineTo(18.5f, 10f)\n                close()\n                moveTo(15.88f, 17f)\n                lineTo(17.5f, 12.67f)\n                lineTo(19.12f, 17f)\n                horizontalLineTo(15.88f)\n                close()\n            }\n        }.build()\n\n        return _Language!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Language: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/List.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.List: ImageVector\n    get() {\n        if (_List != null) {\n            return _List!!\n        }\n        _List = ImageVector.Builder(\n            name = \"List\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(9f, 6f)\n                horizontalLineTo(20f)\n                moveTo(9f, 12f)\n                horizontalLineTo(20f)\n                moveTo(9f, 18f)\n                horizontalLineTo(20f)\n                moveTo(5f, 6f)\n                verticalLineTo(6.01f)\n                moveTo(5f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(5f, 18f)\n                verticalLineTo(18.01f)\n            }\n        }.build()\n\n        return _List!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _List: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Lock.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Lock: ImageVector\n    get() {\n        if (_Lock != null) {\n            return _Lock!!\n        }\n        _Lock = ImageVector.Builder(\n            name = \"Lock\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8f, 11f)\n                verticalLineTo(7f)\n                curveTo(8f, 5.939f, 8.421f, 4.922f, 9.172f, 4.172f)\n                curveTo(9.922f, 3.421f, 10.939f, 3f, 12f, 3f)\n                curveTo(13.061f, 3f, 14.078f, 3.421f, 14.828f, 4.172f)\n                curveTo(15.579f, 4.922f, 16f, 5.939f, 16f, 7f)\n                verticalLineTo(11f)\n                moveTo(5f, 13f)\n                curveTo(5f, 12.47f, 5.211f, 11.961f, 5.586f, 11.586f)\n                curveTo(5.961f, 11.211f, 6.47f, 11f, 7f, 11f)\n                horizontalLineTo(17f)\n                curveTo(17.53f, 11f, 18.039f, 11.211f, 18.414f, 11.586f)\n                curveTo(18.789f, 11.961f, 19f, 12.47f, 19f, 13f)\n                verticalLineTo(19f)\n                curveTo(19f, 19.53f, 18.789f, 20.039f, 18.414f, 20.414f)\n                curveTo(18.039f, 20.789f, 17.53f, 21f, 17f, 21f)\n                horizontalLineTo(7f)\n                curveTo(6.47f, 21f, 5.961f, 20.789f, 5.586f, 20.414f)\n                curveTo(5.211f, 20.039f, 5f, 19.53f, 5f, 19f)\n                verticalLineTo(13f)\n                close()\n                moveTo(11f, 16f)\n                curveTo(11f, 16.265f, 11.105f, 16.52f, 11.293f, 16.707f)\n                curveTo(11.48f, 16.895f, 11.735f, 17f, 12f, 17f)\n                curveTo(12.265f, 17f, 12.52f, 16.895f, 12.707f, 16.707f)\n                curveTo(12.895f, 16.52f, 13f, 16.265f, 13f, 16f)\n                curveTo(13f, 15.735f, 12.895f, 15.48f, 12.707f, 15.293f)\n                curveTo(12.52f, 15.105f, 12.265f, 15f, 12f, 15f)\n                curveTo(11.735f, 15f, 11.48f, 15.105f, 11.293f, 15.293f)\n                curveTo(11.105f, 15.48f, 11f, 15.735f, 11f, 16f)\n                close()\n            }\n        }.build()\n\n        return _Lock!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Lock: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Menu.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Menu: ImageVector\n    get() {\n        if (_Menu != null) {\n            return _Menu!!\n        }\n        _Menu = ImageVector.Builder(\n            name = \"Menu\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(4f, 6f)\n                horizontalLineTo(20f)\n                moveTo(4f, 12f)\n                horizontalLineTo(20f)\n                moveTo(4f, 18f)\n                horizontalLineTo(20f)\n            }\n        }.build()\n\n        return _Menu!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Menu: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Minus.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Minus: ImageVector\n    get() {\n        if (_Minus != null) {\n            return _Minus!!\n        }\n        _Minus = ImageVector.Builder(\n            name = \"Minus\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(19f, 11f)\n                curveTo(19.552f, 11f, 20f, 11.448f, 20f, 12f)\n                curveTo(20f, 12.552f, 19.552f, 13f, 19f, 13f)\n                horizontalLineTo(5f)\n                curveTo(4.448f, 13f, 4f, 12.552f, 4f, 12f)\n                curveTo(4f, 11.448f, 4.448f, 11f, 5f, 11f)\n                horizontalLineTo(19f)\n                close()\n            }\n        }.build()\n\n        return _Minus!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Minus: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Network.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Network: ImageVector\n    get() {\n        if (_Network != null) {\n            return _Network!!\n        }\n        _Network = ImageVector.Builder(\n            name = \"Network\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(7f, 3f)\n                verticalLineTo(21f)\n                moveTo(7f, 3f)\n                lineTo(10f, 6f)\n                moveTo(7f, 3f)\n                lineTo(4f, 6f)\n                moveTo(20f, 18f)\n                lineTo(17f, 21f)\n                moveTo(17f, 21f)\n                lineTo(14f, 18f)\n                moveTo(17f, 21f)\n                verticalLineTo(3f)\n            }\n        }.build()\n\n        return _Network!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Network: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Next.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Next: ImageVector\n    get() {\n        if (_Next != null) {\n            return _Next!!\n        }\n        _Next = ImageVector.Builder(\n            name = \"Next\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(8.293f, 5.293f)\n                curveTo(8.683f, 4.902f, 9.317f, 4.902f, 9.707f, 5.293f)\n                lineTo(15.707f, 11.293f)\n                curveTo(16.098f, 11.683f, 16.098f, 12.317f, 15.707f, 12.707f)\n                lineTo(9.707f, 18.707f)\n                curveTo(9.317f, 19.098f, 8.683f, 19.098f, 8.293f, 18.707f)\n                curveTo(7.902f, 18.317f, 7.902f, 17.683f, 8.293f, 17.293f)\n                lineTo(13.586f, 12f)\n                lineTo(8.293f, 6.707f)\n                curveTo(7.902f, 6.317f, 7.902f, 5.683f, 8.293f, 5.293f)\n                close()\n            }\n        }.build()\n\n        return _Next!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Next: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/OpenSource.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.OpenSource: ImageVector\n    get() {\n        if (_OpenSource != null) {\n            return _OpenSource!!\n        }\n        _OpenSource = ImageVector.Builder(\n            name = \"OpenSource\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(9f, 19f)\n                curveTo(4.7f, 20.4f, 4.7f, 16.5f, 3f, 16f)\n                moveTo(15f, 21f)\n                verticalLineTo(17.5f)\n                curveTo(15f, 16.5f, 15.1f, 16.1f, 14.5f, 15.5f)\n                curveTo(17.3f, 15.2f, 20f, 14.1f, 20f, 9.5f)\n                curveTo(19.999f, 8.305f, 19.532f, 7.157f, 18.7f, 6.3f)\n                curveTo(19.09f, 5.262f, 19.055f, 4.112f, 18.6f, 3.1f)\n                curveTo(18.6f, 3.1f, 17.5f, 2.8f, 15.1f, 4.4f)\n                curveTo(13.067f, 3.871f, 10.933f, 3.871f, 8.9f, 4.4f)\n                curveTo(6.5f, 2.8f, 5.4f, 3.1f, 5.4f, 3.1f)\n                curveTo(4.945f, 4.112f, 4.91f, 5.262f, 5.3f, 6.3f)\n                curveTo(4.467f, 7.157f, 4.001f, 8.305f, 4f, 9.5f)\n                curveTo(4f, 14.1f, 6.7f, 15.2f, 9.5f, 15.5f)\n                curveTo(8.9f, 16.1f, 8.9f, 16.7f, 9f, 17.5f)\n                verticalLineTo(21f)\n            }\n        }.build()\n\n        return _OpenSource!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _OpenSource: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Pause.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Pause: ImageVector\n    get() {\n        if (_Pause != null) {\n            return _Pause!!\n        }\n        _Pause = ImageVector.Builder(\n            name = \"Pause\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(6f, 6f)\n                curveTo(6f, 5.735f, 6.105f, 5.48f, 6.293f, 5.293f)\n                curveTo(6.48f, 5.105f, 6.735f, 5f, 7f, 5f)\n                horizontalLineTo(9f)\n                curveTo(9.265f, 5f, 9.52f, 5.105f, 9.707f, 5.293f)\n                curveTo(9.895f, 5.48f, 10f, 5.735f, 10f, 6f)\n                verticalLineTo(18f)\n                curveTo(10f, 18.265f, 9.895f, 18.52f, 9.707f, 18.707f)\n                curveTo(9.52f, 18.895f, 9.265f, 19f, 9f, 19f)\n                horizontalLineTo(7f)\n                curveTo(6.735f, 19f, 6.48f, 18.895f, 6.293f, 18.707f)\n                curveTo(6.105f, 18.52f, 6f, 18.265f, 6f, 18f)\n                verticalLineTo(6f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(14f, 6f)\n                curveTo(14f, 5.735f, 14.105f, 5.48f, 14.293f, 5.293f)\n                curveTo(14.48f, 5.105f, 14.735f, 5f, 15f, 5f)\n                horizontalLineTo(17f)\n                curveTo(17.265f, 5f, 17.52f, 5.105f, 17.707f, 5.293f)\n                curveTo(17.895f, 5.48f, 18f, 5.735f, 18f, 6f)\n                verticalLineTo(18f)\n                curveTo(18f, 18.265f, 17.895f, 18.52f, 17.707f, 18.707f)\n                curveTo(17.52f, 18.895f, 17.265f, 19f, 17f, 19f)\n                horizontalLineTo(15f)\n                curveTo(14.735f, 19f, 14.48f, 18.895f, 14.293f, 18.707f)\n                curveTo(14.105f, 18.52f, 14f, 18.265f, 14f, 18f)\n                verticalLineTo(6f)\n                close()\n            }\n        }.build()\n\n        return _Pause!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Pause: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Permission.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Permission: ImageVector\n    get() {\n        if (_Permission != null) {\n            return _Permission!!\n        }\n        _Permission = ImageVector.Builder(\n            name = \"Permission\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(15f, 21f)\n                horizontalLineTo(6f)\n                curveTo(5.204f, 21f, 4.441f, 20.684f, 3.879f, 20.121f)\n                curveTo(3.316f, 19.559f, 3f, 18.796f, 3f, 18f)\n                verticalLineTo(17f)\n                horizontalLineTo(13f)\n                verticalLineTo(19f)\n                curveTo(13f, 19.53f, 13.211f, 20.039f, 13.586f, 20.414f)\n                curveTo(13.961f, 20.789f, 14.47f, 21f, 15f, 21f)\n                close()\n                moveTo(15f, 21f)\n                curveTo(15.53f, 21f, 16.039f, 20.789f, 16.414f, 20.414f)\n                curveTo(16.789f, 20.039f, 17f, 19.53f, 17f, 19f)\n                verticalLineTo(5f)\n                curveTo(17f, 4.604f, 17.117f, 4.218f, 17.337f, 3.889f)\n                curveTo(17.557f, 3.56f, 17.869f, 3.304f, 18.235f, 3.152f)\n                curveTo(18.6f, 3.001f, 19.002f, 2.961f, 19.39f, 3.038f)\n                curveTo(19.778f, 3.116f, 20.135f, 3.306f, 20.414f, 3.586f)\n                curveTo(20.694f, 3.865f, 20.884f, 4.222f, 20.962f, 4.61f)\n                curveTo(21.039f, 4.998f, 20.999f, 5.4f, 20.848f, 5.765f)\n                curveTo(20.696f, 6.131f, 20.44f, 6.443f, 20.111f, 6.663f)\n                curveTo(19.782f, 6.883f, 19.396f, 7f, 19f, 7f)\n                horizontalLineTo(17f)\n                moveTo(19f, 3f)\n                horizontalLineTo(8f)\n                curveTo(7.204f, 3f, 6.441f, 3.316f, 5.879f, 3.879f)\n                curveTo(5.316f, 4.441f, 5f, 5.204f, 5f, 6f)\n                verticalLineTo(17f)\n                moveTo(9f, 7f)\n                horizontalLineTo(13f)\n                moveTo(9f, 11f)\n                horizontalLineTo(13f)\n            }\n        }.build()\n\n        return _Permission!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Permission: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Plus.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Plus: ImageVector\n    get() {\n        if (_Plus != null) {\n            return _Plus!!\n        }\n        _Plus = ImageVector.Builder(\n            name = \"Plus\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(12f, 5f)\n                verticalLineTo(19f)\n                moveTo(5f, 12f)\n                horizontalLineTo(19f)\n            }\n        }.build()\n\n        return _Plus!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Plus: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/QuestionMark.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.QuestionMark: ImageVector\n    get() {\n        if (_QuestionMark != null) {\n            return _QuestionMark!!\n        }\n        _QuestionMark = ImageVector.Builder(\n            name = \"QuestionMark\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8f, 8f)\n                curveTo(8f, 7.204f, 8.369f, 6.441f, 9.025f, 5.879f)\n                curveTo(9.682f, 5.316f, 10.572f, 5f, 11.5f, 5f)\n                horizontalLineTo(12.5f)\n                curveTo(13.428f, 5f, 14.318f, 5.316f, 14.975f, 5.879f)\n                curveTo(15.631f, 6.441f, 16f, 7.204f, 16f, 8f)\n                curveTo(16.037f, 8.649f, 15.862f, 9.293f, 15.501f, 9.834f)\n                curveTo(15.14f, 10.375f, 14.613f, 10.784f, 14f, 11f)\n                curveTo(13.387f, 11.288f, 12.86f, 11.833f, 12.499f, 12.555f)\n                curveTo(12.138f, 13.276f, 11.963f, 14.134f, 12f, 15f)\n                moveTo(12f, 19f)\n                verticalLineTo(19.01f)\n            }\n        }.build()\n\n        return _QuestionMark!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _QuestionMark: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Queue.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Queue: ImageVector\n    get() {\n        if (_Queue != null) {\n            return _Queue!!\n        }\n        _Queue = ImageVector.Builder(\n            name = \"Queue\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(4f, 6f)\n                horizontalLineTo(2f)\n                verticalLineTo(20f)\n                curveTo(2f, 21.1f, 2.9f, 22f, 4f, 22f)\n                horizontalLineTo(18f)\n                verticalLineTo(20f)\n                horizontalLineTo(4f)\n                verticalLineTo(6f)\n                close()\n                moveTo(20f, 2f)\n                horizontalLineTo(8f)\n                curveTo(6.9f, 2f, 6f, 2.9f, 6f, 4f)\n                verticalLineTo(16f)\n                curveTo(6f, 17.1f, 6.9f, 18f, 8f, 18f)\n                horizontalLineTo(20f)\n                curveTo(21.1f, 18f, 22f, 17.1f, 22f, 16f)\n                verticalLineTo(4f)\n                curveTo(22f, 2.9f, 21.1f, 2f, 20f, 2f)\n                close()\n                moveTo(20f, 16f)\n                horizontalLineTo(8f)\n                verticalLineTo(4f)\n                horizontalLineTo(20f)\n                verticalLineTo(16f)\n                close()\n                moveTo(13f, 15f)\n                horizontalLineTo(15f)\n                verticalLineTo(11f)\n                horizontalLineTo(19f)\n                verticalLineTo(9f)\n                horizontalLineTo(15f)\n                verticalLineTo(5f)\n                horizontalLineTo(13f)\n                verticalLineTo(9f)\n                horizontalLineTo(9f)\n                verticalLineTo(11f)\n                horizontalLineTo(13f)\n                verticalLineTo(15f)\n                close()\n            }\n        }.build()\n\n        return _Queue!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Queue: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/QueueStart.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.QueueStart: ImageVector\n    get() {\n        if (_QueueStart != null) {\n            return _QueueStart!!\n        }\n        _QueueStart = ImageVector.Builder(\n            name = \"QueueStart\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(2f, 6f)\n                horizontalLineTo(4f)\n                verticalLineTo(20f)\n                horizontalLineTo(10f)\n                verticalLineTo(22f)\n                horizontalLineTo(4f)\n                curveTo(2.9f, 22f, 2f, 21.1f, 2f, 20f)\n                verticalLineTo(6f)\n                close()\n                moveTo(22f, 10f)\n                verticalLineTo(4f)\n                curveTo(22f, 2.9f, 21.1f, 2f, 20f, 2f)\n                horizontalLineTo(8f)\n                curveTo(6.9f, 2f, 6f, 2.9f, 6f, 4f)\n                verticalLineTo(16f)\n                curveTo(6f, 17.1f, 6.9f, 18f, 8f, 18f)\n                horizontalLineTo(10f)\n                verticalLineTo(16f)\n                horizontalLineTo(8f)\n                verticalLineTo(4f)\n                horizontalLineTo(20f)\n                verticalLineTo(10f)\n                horizontalLineTo(22f)\n                close()\n                moveTo(19f, 10f)\n                verticalLineTo(9f)\n                horizontalLineTo(15f)\n                verticalLineTo(5f)\n                horizontalLineTo(13f)\n                verticalLineTo(9f)\n                horizontalLineTo(9f)\n                verticalLineTo(11f)\n                horizontalLineTo(10.764f)\n                curveTo(11.313f, 10.386f, 12.111f, 10f, 13f, 10f)\n                horizontalLineTo(19f)\n                close()\n            }\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 1.2f\n            ) {\n                moveTo(21.963f, 17.357f)\n                lineTo(13.18f, 21.794f)\n                curveTo(12.914f, 21.928f, 12.6f, 21.735f, 12.6f, 21.437f)\n                verticalLineTo(12.563f)\n                curveTo(12.6f, 12.265f, 12.914f, 12.072f, 13.18f, 12.206f)\n                lineTo(21.963f, 16.643f)\n                curveTo(22.256f, 16.791f, 22.256f, 17.209f, 21.963f, 17.357f)\n                close()\n            }\n        }.build()\n\n        return _QueueStart!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _QueueStart: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/QueueStop.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.QueueStop: ImageVector\n    get() {\n        if (_QueueStop != null) {\n            return _QueueStop!!\n        }\n        _QueueStop = ImageVector.Builder(\n            name = \"QueueStop\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(2f, 6f)\n                horizontalLineTo(4f)\n                verticalLineTo(20f)\n                horizontalLineTo(10f)\n                verticalLineTo(22f)\n                horizontalLineTo(4f)\n                curveTo(2.9f, 22f, 2f, 21.1f, 2f, 20f)\n                verticalLineTo(6f)\n                close()\n                moveTo(22f, 10f)\n                verticalLineTo(4f)\n                curveTo(22f, 2.9f, 21.1f, 2f, 20f, 2f)\n                horizontalLineTo(8f)\n                curveTo(6.9f, 2f, 6f, 2.9f, 6f, 4f)\n                verticalLineTo(16f)\n                curveTo(6f, 17.1f, 6.9f, 18f, 8f, 18f)\n                horizontalLineTo(10f)\n                verticalLineTo(16f)\n                horizontalLineTo(8f)\n                verticalLineTo(4f)\n                horizontalLineTo(20f)\n                verticalLineTo(10f)\n                horizontalLineTo(22f)\n                close()\n                moveTo(19f, 10f)\n                verticalLineTo(9f)\n                horizontalLineTo(15f)\n                verticalLineTo(5f)\n                horizontalLineTo(13f)\n                verticalLineTo(9f)\n                horizontalLineTo(9f)\n                verticalLineTo(11f)\n                horizontalLineTo(10.764f)\n                curveTo(11.313f, 10.386f, 12.111f, 10f, 13f, 10f)\n                horizontalLineTo(19f)\n                close()\n            }\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(13.952f, 13.064f)\n                curveTo(13.716f, 13.064f, 13.491f, 13.158f, 13.324f, 13.324f)\n                curveTo(13.158f, 13.491f, 13.064f, 13.716f, 13.064f, 13.952f)\n                verticalLineTo(21.048f)\n                curveTo(13.064f, 21.284f, 13.158f, 21.509f, 13.324f, 21.676f)\n                curveTo(13.491f, 21.842f, 13.716f, 21.935f, 13.952f, 21.935f)\n                horizontalLineTo(21.048f)\n                curveTo(21.284f, 21.935f, 21.509f, 21.842f, 21.676f, 21.676f)\n                curveTo(21.842f, 21.509f, 21.935f, 21.284f, 21.935f, 21.048f)\n                verticalLineTo(13.952f)\n                curveTo(21.935f, 13.716f, 21.842f, 13.491f, 21.676f, 13.324f)\n                curveTo(21.509f, 13.158f, 21.284f, 13.064f, 21.048f, 13.064f)\n                horizontalLineTo(13.952f)\n                close()\n                moveTo(12.572f, 12.572f)\n                curveTo(12.938f, 12.206f, 13.434f, 12f, 13.952f, 12f)\n                horizontalLineTo(21.048f)\n                curveTo(21.566f, 12f, 22.062f, 12.206f, 22.428f, 12.572f)\n                curveTo(22.794f, 12.938f, 23f, 13.434f, 23f, 13.952f)\n                verticalLineTo(21.048f)\n                curveTo(23f, 21.566f, 22.794f, 22.062f, 22.428f, 22.428f)\n                curveTo(22.062f, 22.794f, 21.566f, 23f, 21.048f, 23f)\n                horizontalLineTo(13.952f)\n                curveTo(13.434f, 23f, 12.938f, 22.794f, 12.572f, 22.428f)\n                curveTo(12.206f, 22.062f, 12f, 21.566f, 12f, 21.048f)\n                verticalLineTo(13.952f)\n                curveTo(12f, 13.434f, 12.206f, 12.938f, 12.572f, 12.572f)\n                close()\n            }\n        }.build()\n\n        return _QueueStop!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _QueueStop: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Refresh.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Refresh: ImageVector\n    get() {\n        if (_Refresh != null) {\n            return _Refresh!!\n        }\n        _Refresh = ImageVector.Builder(\n            name = \"Refresh\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(21f, 10.873f)\n                curveTo(20.725f, 8.889f, 19.806f, 7.052f, 18.386f, 5.643f)\n                curveTo(16.966f, 4.233f, 15.123f, 3.331f, 13.14f, 3.075f)\n                curveTo(11.158f, 2.819f, 9.147f, 3.223f, 7.416f, 4.224f)\n                curveTo(5.685f, 5.226f, 4.331f, 6.77f, 3.563f, 8.619f)\n                moveTo(3f, 4.11f)\n                verticalLineTo(8.619f)\n                horizontalLineTo(7.5f)\n                moveTo(3f, 13.127f)\n                curveTo(3.275f, 15.111f, 4.194f, 16.948f, 5.614f, 18.358f)\n                curveTo(7.034f, 19.767f, 8.877f, 20.669f, 10.86f, 20.925f)\n                curveTo(12.842f, 21.181f, 14.853f, 20.777f, 16.584f, 19.776f)\n                curveTo(18.315f, 18.774f, 19.669f, 17.23f, 20.438f, 15.381f)\n                moveTo(21f, 19.89f)\n                verticalLineTo(15.381f)\n                horizontalLineTo(16.5f)\n            }\n        }.build()\n\n        return _Refresh!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Refresh: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Resume.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Resume: ImageVector\n    get() {\n        if (_Resume != null) {\n            return _Resume!!\n        }\n        _Resume = ImageVector.Builder(\n            name = \"Resume\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(6.634f, 3.345f)\n                curveTo(6.871f, 3.213f, 7.162f, 3.219f, 7.393f, 3.361f)\n                lineTo(20.393f, 11.361f)\n                curveTo(20.615f, 11.498f, 20.75f, 11.74f, 20.75f, 12f)\n                curveTo(20.75f, 12.26f, 20.615f, 12.502f, 20.393f, 12.639f)\n                lineTo(7.393f, 20.639f)\n                curveTo(7.162f, 20.781f, 6.871f, 20.787f, 6.634f, 20.655f)\n                curveTo(6.397f, 20.522f, 6.25f, 20.272f, 6.25f, 20f)\n                verticalLineTo(4f)\n                curveTo(6.25f, 3.728f, 6.397f, 3.478f, 6.634f, 3.345f)\n                close()\n                moveTo(7.75f, 5.342f)\n                verticalLineTo(18.658f)\n                lineTo(18.569f, 12f)\n                lineTo(7.75f, 5.342f)\n                close()\n            }\n        }.build()\n\n        return _Resume!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Resume: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Scheduler.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Scheduler: ImageVector\n    get() {\n        if (_Scheduler != null) {\n            return _Scheduler!!\n        }\n        _Scheduler = ImageVector.Builder(\n            name = \"Scheduler\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(7f, 2.25f)\n                curveTo(7.414f, 2.25f, 7.75f, 2.586f, 7.75f, 3f)\n                verticalLineTo(4.25f)\n                horizontalLineTo(14.25f)\n                verticalLineTo(3f)\n                curveTo(14.25f, 2.586f, 14.586f, 2.25f, 15f, 2.25f)\n                curveTo(15.414f, 2.25f, 15.75f, 2.586f, 15.75f, 3f)\n                verticalLineTo(4.25f)\n                horizontalLineTo(17f)\n                curveTo(17.729f, 4.25f, 18.429f, 4.54f, 18.944f, 5.055f)\n                curveTo(19.46f, 5.571f, 19.75f, 6.271f, 19.75f, 7f)\n                verticalLineTo(11f)\n                curveTo(19.75f, 11.414f, 19.414f, 11.75f, 19f, 11.75f)\n                horizontalLineTo(3.75f)\n                verticalLineTo(19f)\n                curveTo(3.75f, 19.331f, 3.882f, 19.649f, 4.116f, 19.884f)\n                curveTo(4.351f, 20.118f, 4.668f, 20.25f, 5f, 20.25f)\n                horizontalLineTo(11.795f)\n                curveTo(12.209f, 20.25f, 12.545f, 20.586f, 12.545f, 21f)\n                curveTo(12.545f, 21.414f, 12.209f, 21.75f, 11.795f, 21.75f)\n                horizontalLineTo(5f)\n                curveTo(4.271f, 21.75f, 3.571f, 21.46f, 3.055f, 20.944f)\n                curveTo(2.54f, 20.429f, 2.25f, 19.729f, 2.25f, 19f)\n                verticalLineTo(7f)\n                curveTo(2.25f, 6.271f, 2.54f, 5.571f, 3.055f, 5.055f)\n                curveTo(3.571f, 4.54f, 4.271f, 4.25f, 5f, 4.25f)\n                horizontalLineTo(6.25f)\n                verticalLineTo(3f)\n                curveTo(6.25f, 2.586f, 6.586f, 2.25f, 7f, 2.25f)\n                close()\n                moveTo(6.25f, 5.75f)\n                horizontalLineTo(5f)\n                curveTo(4.668f, 5.75f, 4.351f, 5.882f, 4.116f, 6.116f)\n                curveTo(3.882f, 6.351f, 3.75f, 6.668f, 3.75f, 7f)\n                verticalLineTo(10.25f)\n                horizontalLineTo(18.25f)\n                verticalLineTo(7f)\n                curveTo(18.25f, 6.668f, 18.118f, 6.351f, 17.884f, 6.116f)\n                curveTo(17.649f, 5.882f, 17.331f, 5.75f, 17f, 5.75f)\n                horizontalLineTo(15.75f)\n                verticalLineTo(7f)\n                curveTo(15.75f, 7.414f, 15.414f, 7.75f, 15f, 7.75f)\n                curveTo(14.586f, 7.75f, 14.25f, 7.414f, 14.25f, 7f)\n                verticalLineTo(5.75f)\n                horizontalLineTo(7.75f)\n                verticalLineTo(7f)\n                curveTo(7.75f, 7.414f, 7.414f, 7.75f, 7f, 7.75f)\n                curveTo(6.586f, 7.75f, 6.25f, 7.414f, 6.25f, 7f)\n                verticalLineTo(5.75f)\n                close()\n                moveTo(14.641f, 14.641f)\n                curveTo(15.532f, 13.75f, 16.74f, 13.25f, 18f, 13.25f)\n                curveTo(19.26f, 13.25f, 20.468f, 13.75f, 21.359f, 14.641f)\n                curveTo(22.25f, 15.532f, 22.75f, 16.74f, 22.75f, 18f)\n                curveTo(22.75f, 19.26f, 22.25f, 20.468f, 21.359f, 21.359f)\n                curveTo(20.468f, 22.25f, 19.26f, 22.75f, 18f, 22.75f)\n                curveTo(16.74f, 22.75f, 15.532f, 22.25f, 14.641f, 21.359f)\n                curveTo(13.75f, 20.468f, 13.25f, 19.26f, 13.25f, 18f)\n                curveTo(13.25f, 16.74f, 13.75f, 15.532f, 14.641f, 14.641f)\n                close()\n                moveTo(18f, 14.75f)\n                curveTo(17.138f, 14.75f, 16.311f, 15.092f, 15.702f, 15.702f)\n                curveTo(15.092f, 16.311f, 14.75f, 17.138f, 14.75f, 18f)\n                curveTo(14.75f, 18.862f, 15.092f, 19.689f, 15.702f, 20.298f)\n                curveTo(16.311f, 20.908f, 17.138f, 21.25f, 18f, 21.25f)\n                curveTo(18.862f, 21.25f, 19.689f, 20.908f, 20.298f, 20.298f)\n                curveTo(20.908f, 19.689f, 21.25f, 18.862f, 21.25f, 18f)\n                curveTo(21.25f, 17.138f, 20.908f, 16.311f, 20.298f, 15.702f)\n                curveTo(19.689f, 15.092f, 18.862f, 14.75f, 18f, 14.75f)\n                close()\n                moveTo(18f, 15.746f)\n                curveTo(18.414f, 15.746f, 18.75f, 16.082f, 18.75f, 16.496f)\n                verticalLineTo(17.689f)\n                lineTo(19.53f, 18.47f)\n                curveTo(19.823f, 18.763f, 19.823f, 19.237f, 19.53f, 19.53f)\n                curveTo(19.237f, 19.823f, 18.763f, 19.823f, 18.47f, 19.53f)\n                lineTo(17.47f, 18.53f)\n                curveTo(17.329f, 18.39f, 17.25f, 18.199f, 17.25f, 18f)\n                verticalLineTo(16.496f)\n                curveTo(17.25f, 16.082f, 17.586f, 15.746f, 18f, 15.746f)\n                close()\n            }\n        }.build()\n\n        return _Scheduler!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Scheduler: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Search.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Search: ImageVector\n    get() {\n        if (_Search != null) {\n            return _Search!!\n        }\n        _Search = ImageVector.Builder(\n            name = \"Search\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(7.034f, 2.84f)\n                curveTo(7.974f, 2.45f, 8.982f, 2.25f, 10f, 2.25f)\n                curveTo(11.018f, 2.25f, 12.026f, 2.45f, 12.966f, 2.84f)\n                curveTo(13.906f, 3.229f, 14.76f, 3.8f, 15.48f, 4.52f)\n                curveTo(16.2f, 5.24f, 16.771f, 6.094f, 17.16f, 7.034f)\n                curveTo(17.549f, 7.974f, 17.75f, 8.982f, 17.75f, 10f)\n                curveTo(17.75f, 11.018f, 17.549f, 12.026f, 17.16f, 12.966f)\n                curveTo(16.867f, 13.674f, 16.47f, 14.334f, 15.985f, 14.924f)\n                lineTo(21.53f, 20.47f)\n                curveTo(21.823f, 20.763f, 21.823f, 21.237f, 21.53f, 21.53f)\n                curveTo(21.237f, 21.823f, 20.763f, 21.823f, 20.47f, 21.53f)\n                lineTo(14.924f, 15.985f)\n                curveTo(14.334f, 16.47f, 13.674f, 16.867f, 12.966f, 17.16f)\n                curveTo(12.026f, 17.549f, 11.018f, 17.75f, 10f, 17.75f)\n                curveTo(8.982f, 17.75f, 7.974f, 17.549f, 7.034f, 17.16f)\n                curveTo(6.094f, 16.771f, 5.24f, 16.2f, 4.52f, 15.48f)\n                curveTo(3.8f, 14.76f, 3.229f, 13.906f, 2.84f, 12.966f)\n                curveTo(2.45f, 12.026f, 2.25f, 11.018f, 2.25f, 10f)\n                curveTo(2.25f, 8.982f, 2.45f, 7.974f, 2.84f, 7.034f)\n                curveTo(3.229f, 6.094f, 3.8f, 5.24f, 4.52f, 4.52f)\n                curveTo(5.24f, 3.8f, 6.094f, 3.229f, 7.034f, 2.84f)\n                close()\n                moveTo(10f, 3.75f)\n                curveTo(9.179f, 3.75f, 8.367f, 3.912f, 7.608f, 4.226f)\n                curveTo(6.85f, 4.54f, 6.161f, 5f, 5.581f, 5.581f)\n                curveTo(5f, 6.161f, 4.54f, 6.85f, 4.226f, 7.608f)\n                curveTo(3.912f, 8.367f, 3.75f, 9.179f, 3.75f, 10f)\n                curveTo(3.75f, 10.821f, 3.912f, 11.634f, 4.226f, 12.392f)\n                curveTo(4.54f, 13.15f, 5f, 13.839f, 5.581f, 14.419f)\n                curveTo(6.161f, 15f, 6.85f, 15.46f, 7.608f, 15.774f)\n                curveTo(8.367f, 16.088f, 9.179f, 16.25f, 10f, 16.25f)\n                curveTo(10.821f, 16.25f, 11.634f, 16.088f, 12.392f, 15.774f)\n                curveTo(13.15f, 15.46f, 13.839f, 15f, 14.419f, 14.419f)\n                curveTo(15f, 13.839f, 15.46f, 13.15f, 15.774f, 12.392f)\n                curveTo(16.088f, 11.634f, 16.25f, 10.821f, 16.25f, 10f)\n                curveTo(16.25f, 9.179f, 16.088f, 8.367f, 15.774f, 7.608f)\n                curveTo(15.46f, 6.85f, 15f, 6.161f, 14.419f, 5.581f)\n                curveTo(13.839f, 5f, 13.15f, 4.54f, 12.392f, 4.226f)\n                curveTo(11.634f, 3.912f, 10.821f, 3.75f, 10f, 3.75f)\n                close()\n            }\n        }.build()\n\n        return _Search!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Search: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/SelectAll.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.SelectAll: ImageVector\n    get() {\n        if (_SelectAll != null) {\n            return _SelectAll!!\n        }\n        _SelectAll = ImageVector.Builder(\n            name = \"SelectAll\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(4f, 12f)\n                horizontalLineTo(20f)\n                moveTo(12f, 4f)\n                verticalLineTo(20f)\n                moveTo(4f, 6f)\n                curveTo(4f, 5.47f, 4.211f, 4.961f, 4.586f, 4.586f)\n                curveTo(4.961f, 4.211f, 5.47f, 4f, 6f, 4f)\n                horizontalLineTo(18f)\n                curveTo(18.53f, 4f, 19.039f, 4.211f, 19.414f, 4.586f)\n                curveTo(19.789f, 4.961f, 20f, 5.47f, 20f, 6f)\n                verticalLineTo(18f)\n                curveTo(20f, 18.53f, 19.789f, 19.039f, 19.414f, 19.414f)\n                curveTo(19.039f, 19.789f, 18.53f, 20f, 18f, 20f)\n                horizontalLineTo(6f)\n                curveTo(5.47f, 20f, 4.961f, 19.789f, 4.586f, 19.414f)\n                curveTo(4.211f, 19.039f, 4f, 18.53f, 4f, 18f)\n                verticalLineTo(6f)\n                close()\n            }\n        }.build()\n\n        return _SelectAll!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _SelectAll: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/SelectInside.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.SelectInside: ImageVector\n    get() {\n        if (_SelectInside != null) {\n            return _SelectInside!!\n        }\n        _SelectInside = ImageVector.Builder(\n            name = \"SelectInside\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(5f, 22f)\n                curveTo(4.717f, 22f, 4.479f, 21.904f, 4.288f, 21.712f)\n                curveTo(4.097f, 21.52f, 4.001f, 21.283f, 4f, 21f)\n                curveTo(3.999f, 20.717f, 4.095f, 20.48f, 4.288f, 20.288f)\n                curveTo(4.481f, 20.096f, 4.718f, 20f, 5f, 20f)\n                horizontalLineTo(19f)\n                curveTo(19.283f, 20f, 19.521f, 20.096f, 19.713f, 20.288f)\n                curveTo(19.905f, 20.48f, 20.001f, 20.717f, 20f, 21f)\n                curveTo(19.999f, 21.283f, 19.903f, 21.52f, 19.712f, 21.713f)\n                curveTo(19.521f, 21.906f, 19.283f, 22.001f, 19f, 22f)\n                horizontalLineTo(5f)\n                close()\n                moveTo(12f, 18.575f)\n                curveTo(11.867f, 18.575f, 11.742f, 18.554f, 11.625f, 18.513f)\n                curveTo(11.508f, 18.472f, 11.4f, 18.401f, 11.3f, 18.3f)\n                lineTo(8.7f, 15.7f)\n                curveTo(8.517f, 15.517f, 8.421f, 15.288f, 8.413f, 15.013f)\n                curveTo(8.405f, 14.738f, 8.501f, 14.501f, 8.7f, 14.3f)\n                curveTo(8.883f, 14.117f, 9.113f, 14.021f, 9.388f, 14.012f)\n                curveTo(9.663f, 14.003f, 9.901f, 14.091f, 10.1f, 14.275f)\n                lineTo(11f, 15.15f)\n                verticalLineTo(8.85f)\n                lineTo(10.1f, 9.725f)\n                curveTo(9.917f, 9.908f, 9.688f, 10f, 9.413f, 10f)\n                curveTo(9.138f, 10f, 8.901f, 9.9f, 8.7f, 9.7f)\n                curveTo(8.517f, 9.517f, 8.425f, 9.283f, 8.425f, 9f)\n                curveTo(8.425f, 8.717f, 8.517f, 8.483f, 8.7f, 8.3f)\n                lineTo(11.3f, 5.7f)\n                curveTo(11.4f, 5.6f, 11.508f, 5.529f, 11.625f, 5.488f)\n                curveTo(11.742f, 5.447f, 11.867f, 5.426f, 12f, 5.425f)\n                curveTo(12.133f, 5.424f, 12.258f, 5.445f, 12.375f, 5.488f)\n                curveTo(12.492f, 5.531f, 12.6f, 5.601f, 12.7f, 5.7f)\n                lineTo(15.3f, 8.3f)\n                curveTo(15.483f, 8.483f, 15.579f, 8.713f, 15.587f, 8.988f)\n                curveTo(15.595f, 9.263f, 15.499f, 9.501f, 15.3f, 9.7f)\n                curveTo(15.117f, 9.883f, 14.888f, 9.979f, 14.613f, 9.988f)\n                curveTo(14.338f, 9.997f, 14.101f, 9.909f, 13.9f, 9.725f)\n                lineTo(13f, 8.85f)\n                verticalLineTo(15.15f)\n                lineTo(13.9f, 14.275f)\n                curveTo(14.083f, 14.092f, 14.313f, 14f, 14.588f, 14f)\n                curveTo(14.863f, 14f, 15.101f, 14.1f, 15.3f, 14.3f)\n                curveTo(15.483f, 14.483f, 15.575f, 14.717f, 15.575f, 15f)\n                curveTo(15.575f, 15.283f, 15.483f, 15.517f, 15.3f, 15.7f)\n                lineTo(12.7f, 18.3f)\n                curveTo(12.6f, 18.4f, 12.492f, 18.471f, 12.375f, 18.513f)\n                curveTo(12.258f, 18.555f, 12.133f, 18.576f, 12f, 18.575f)\n                close()\n                moveTo(5f, 4f)\n                curveTo(4.717f, 4f, 4.479f, 3.904f, 4.288f, 3.712f)\n                curveTo(4.097f, 3.52f, 4.001f, 3.283f, 4f, 3f)\n                curveTo(3.999f, 2.717f, 4.095f, 2.48f, 4.288f, 2.288f)\n                curveTo(4.481f, 2.096f, 4.718f, 2f, 5f, 2f)\n                horizontalLineTo(19f)\n                curveTo(19.283f, 2f, 19.521f, 2.096f, 19.713f, 2.288f)\n                curveTo(19.905f, 2.48f, 20.001f, 2.717f, 20f, 3f)\n                curveTo(19.999f, 3.283f, 19.903f, 3.52f, 19.712f, 3.713f)\n                curveTo(19.521f, 3.906f, 19.283f, 4.001f, 19f, 4f)\n                horizontalLineTo(5f)\n                close()\n            }\n        }.build()\n\n        return _SelectInside!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _SelectInside: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/SelectInvert.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.SelectInvert: ImageVector\n    get() {\n        if (_SelectInvert != null) {\n            return _SelectInvert!!\n        }\n        _SelectInvert = ImageVector.Builder(\n            name = \"SelectInvert\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(4f, 4f)\n                verticalLineTo(4.01f)\n                moveTo(8f, 4f)\n                verticalLineTo(4.01f)\n                moveTo(12f, 4f)\n                verticalLineTo(4.01f)\n                moveTo(16f, 4f)\n                verticalLineTo(4.01f)\n                moveTo(20f, 4f)\n                verticalLineTo(4.01f)\n                moveTo(4f, 8f)\n                verticalLineTo(8.01f)\n                moveTo(12f, 8f)\n                verticalLineTo(8.01f)\n                moveTo(20f, 8f)\n                verticalLineTo(8.01f)\n                moveTo(4f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(8f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(12f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(16f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(20f, 12f)\n                verticalLineTo(12.01f)\n                moveTo(4f, 16f)\n                verticalLineTo(16.01f)\n                moveTo(12f, 16f)\n                verticalLineTo(16.01f)\n                moveTo(20f, 16f)\n                verticalLineTo(16.01f)\n                moveTo(4f, 20f)\n                verticalLineTo(20.01f)\n                moveTo(8f, 20f)\n                verticalLineTo(20.01f)\n                moveTo(12f, 20f)\n                verticalLineTo(20.01f)\n                moveTo(16f, 20f)\n                verticalLineTo(20.01f)\n                moveTo(20f, 20f)\n                verticalLineTo(20.01f)\n            }\n        }.build()\n\n        return _SelectInvert!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _SelectInvert: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Settings.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Settings: ImageVector\n    get() {\n        if (_Settings != null) {\n            return _Settings!!\n        }\n        _Settings = ImageVector.Builder(\n            name = \"Settings\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(12.946f, 4.494f)\n                curveTo(12.705f, 3.502f, 11.295f, 3.502f, 11.054f, 4.494f)\n                lineTo(11.054f, 4.494f)\n                curveTo(10.658f, 6.123f, 8.797f, 6.894f, 7.363f, 6.023f)\n                lineTo(7.363f, 6.023f)\n                curveTo(6.49f, 5.491f, 5.493f, 6.49f, 6.023f, 7.362f)\n                curveTo(6.226f, 7.694f, 6.348f, 8.07f, 6.378f, 8.458f)\n                curveTo(6.408f, 8.847f, 6.346f, 9.237f, 6.197f, 9.596f)\n                curveTo(6.048f, 9.956f, 5.816f, 10.276f, 5.52f, 10.529f)\n                curveTo(5.224f, 10.782f, 4.872f, 10.962f, 4.494f, 11.054f)\n                curveTo(3.502f, 11.295f, 3.502f, 12.705f, 4.494f, 12.946f)\n                lineTo(4.494f, 12.946f)\n                curveTo(4.872f, 13.038f, 5.224f, 13.218f, 5.519f, 13.471f)\n                curveTo(5.815f, 13.725f, 6.047f, 14.044f, 6.195f, 14.404f)\n                curveTo(6.344f, 14.763f, 6.406f, 15.153f, 6.376f, 15.541f)\n                curveTo(6.346f, 15.929f, 6.225f, 16.305f, 6.023f, 16.637f)\n                curveTo(5.491f, 17.51f, 6.49f, 18.507f, 7.362f, 17.976f)\n                curveTo(7.694f, 17.774f, 8.07f, 17.653f, 8.458f, 17.622f)\n                curveTo(8.847f, 17.592f, 9.237f, 17.654f, 9.596f, 17.803f)\n                curveTo(9.956f, 17.952f, 10.276f, 18.184f, 10.529f, 18.48f)\n                curveTo(10.782f, 18.776f, 10.962f, 19.128f, 11.054f, 19.506f)\n                curveTo(11.295f, 20.498f, 12.705f, 20.498f, 12.946f, 19.506f)\n                lineTo(12.946f, 19.506f)\n                curveTo(13.038f, 19.128f, 13.218f, 18.776f, 13.471f, 18.481f)\n                curveTo(13.725f, 18.185f, 14.044f, 17.954f, 14.404f, 17.805f)\n                curveTo(14.763f, 17.656f, 15.153f, 17.594f, 15.541f, 17.624f)\n                curveTo(15.929f, 17.654f, 16.305f, 17.775f, 16.637f, 17.978f)\n                curveTo(17.51f, 18.508f, 18.507f, 17.51f, 17.976f, 16.638f)\n                curveTo(17.774f, 16.306f, 17.653f, 15.93f, 17.622f, 15.542f)\n                curveTo(17.592f, 15.153f, 17.654f, 14.763f, 17.803f, 14.404f)\n                curveTo(17.952f, 14.044f, 18.184f, 13.724f, 18.48f, 13.471f)\n                curveTo(18.776f, 13.218f, 19.128f, 13.038f, 19.506f, 12.946f)\n                curveTo(20.498f, 12.705f, 20.498f, 11.295f, 19.506f, 11.054f)\n                lineTo(19.506f, 11.054f)\n                curveTo(19.128f, 10.962f, 18.776f, 10.782f, 18.481f, 10.529f)\n                curveTo(18.185f, 10.275f, 17.954f, 9.956f, 17.805f, 9.596f)\n                curveTo(17.656f, 9.237f, 17.594f, 8.847f, 17.624f, 8.459f)\n                curveTo(17.654f, 8.071f, 17.775f, 7.695f, 17.978f, 7.363f)\n                curveTo(18.508f, 6.49f, 17.51f, 5.493f, 16.638f, 6.023f)\n                curveTo(16.306f, 6.226f, 15.93f, 6.348f, 15.542f, 6.378f)\n                curveTo(15.153f, 6.408f, 14.763f, 6.346f, 14.404f, 6.197f)\n                curveTo(14.044f, 6.048f, 13.724f, 5.816f, 13.471f, 5.52f)\n                curveTo(13.218f, 5.224f, 13.038f, 4.872f, 12.946f, 4.494f)\n                close()\n                moveTo(9.596f, 4.14f)\n                curveTo(10.208f, 1.62f, 13.792f, 1.62f, 14.404f, 4.14f)\n                lineTo(14.404f, 4.14f)\n                curveTo(14.44f, 4.289f, 14.511f, 4.428f, 14.611f, 4.544f)\n                curveTo(14.71f, 4.661f, 14.836f, 4.752f, 14.978f, 4.811f)\n                curveTo(15.119f, 4.87f, 15.273f, 4.894f, 15.426f, 4.882f)\n                curveTo(15.579f, 4.87f, 15.727f, 4.822f, 15.858f, 4.743f)\n                lineTo(15.858f, 4.742f)\n                curveTo(18.072f, 3.393f, 20.607f, 5.928f, 19.259f, 8.143f)\n                lineTo(19.258f, 8.143f)\n                curveTo(19.179f, 8.274f, 19.131f, 8.422f, 19.119f, 8.575f)\n                curveTo(19.108f, 8.727f, 19.132f, 8.881f, 19.191f, 9.022f)\n                curveTo(19.249f, 9.164f, 19.34f, 9.29f, 19.457f, 9.389f)\n                curveTo(19.573f, 9.489f, 19.711f, 9.56f, 19.86f, 9.596f)\n                curveTo(22.38f, 10.208f, 22.38f, 13.792f, 19.86f, 14.404f)\n                lineTo(19.86f, 14.404f)\n                curveTo(19.711f, 14.44f, 19.572f, 14.511f, 19.456f, 14.611f)\n                curveTo(19.339f, 14.71f, 19.248f, 14.836f, 19.189f, 14.978f)\n                curveTo(19.13f, 15.119f, 19.106f, 15.273f, 19.118f, 15.426f)\n                curveTo(19.13f, 15.579f, 19.177f, 15.727f, 19.257f, 15.858f)\n                lineTo(19.257f, 15.858f)\n                curveTo(20.607f, 18.072f, 18.072f, 20.607f, 15.857f, 19.259f)\n                lineTo(15.857f, 19.258f)\n                curveTo(15.726f, 19.179f, 15.578f, 19.131f, 15.425f, 19.119f)\n                curveTo(15.273f, 19.108f, 15.119f, 19.132f, 14.978f, 19.191f)\n                curveTo(14.836f, 19.249f, 14.71f, 19.34f, 14.611f, 19.457f)\n                curveTo(14.511f, 19.573f, 14.44f, 19.711f, 14.404f, 19.86f)\n                curveTo(13.792f, 22.38f, 10.208f, 22.38f, 9.596f, 19.86f)\n                lineTo(9.596f, 19.86f)\n                curveTo(9.56f, 19.711f, 9.489f, 19.572f, 9.389f, 19.456f)\n                curveTo(9.29f, 19.339f, 9.164f, 19.248f, 9.022f, 19.189f)\n                curveTo(8.881f, 19.13f, 8.727f, 19.106f, 8.574f, 19.118f)\n                curveTo(8.421f, 19.13f, 8.273f, 19.177f, 8.142f, 19.257f)\n                lineTo(8.142f, 19.257f)\n                curveTo(5.928f, 20.607f, 3.393f, 18.072f, 4.741f, 15.857f)\n                lineTo(4.741f, 15.857f)\n                curveTo(4.821f, 15.726f, 4.869f, 15.578f, 4.881f, 15.425f)\n                curveTo(4.893f, 15.273f, 4.868f, 15.119f, 4.81f, 14.978f)\n                curveTo(4.751f, 14.836f, 4.66f, 14.71f, 4.543f, 14.611f)\n                curveTo(4.427f, 14.511f, 4.289f, 14.44f, 4.14f, 14.404f)\n                curveTo(1.62f, 13.792f, 1.62f, 10.208f, 4.14f, 9.596f)\n                lineTo(4.14f, 9.596f)\n                curveTo(4.289f, 9.56f, 4.428f, 9.489f, 4.544f, 9.389f)\n                curveTo(4.661f, 9.29f, 4.752f, 9.164f, 4.811f, 9.022f)\n                curveTo(4.87f, 8.881f, 4.894f, 8.727f, 4.882f, 8.574f)\n                curveTo(4.87f, 8.421f, 4.822f, 8.273f, 4.743f, 8.142f)\n                lineTo(4.742f, 8.142f)\n                curveTo(3.394f, 5.928f, 5.927f, 3.393f, 8.143f, 4.741f)\n                curveTo(8.709f, 5.086f, 9.44f, 4.782f, 9.596f, 4.14f)\n                moveTo(9.348f, 9.348f)\n                curveTo(10.052f, 8.645f, 11.005f, 8.25f, 12f, 8.25f)\n                curveTo(12.995f, 8.25f, 13.948f, 8.645f, 14.652f, 9.348f)\n                curveTo(15.355f, 10.052f, 15.75f, 11.005f, 15.75f, 12f)\n                curveTo(15.75f, 12.995f, 15.355f, 13.948f, 14.652f, 14.652f)\n                curveTo(13.948f, 15.355f, 12.995f, 15.75f, 12f, 15.75f)\n                curveTo(11.005f, 15.75f, 10.052f, 15.355f, 9.348f, 14.652f)\n                curveTo(8.645f, 13.948f, 8.25f, 12.995f, 8.25f, 12f)\n                curveTo(8.25f, 11.005f, 8.645f, 10.052f, 9.348f, 9.348f)\n                close()\n                moveTo(12f, 9.75f)\n                curveTo(11.403f, 9.75f, 10.831f, 9.987f, 10.409f, 10.409f)\n                curveTo(9.987f, 10.831f, 9.75f, 11.403f, 9.75f, 12f)\n                curveTo(9.75f, 12.597f, 9.987f, 13.169f, 10.409f, 13.591f)\n                curveTo(10.831f, 14.013f, 11.403f, 14.25f, 12f, 14.25f)\n                curveTo(12.597f, 14.25f, 13.169f, 14.013f, 13.591f, 13.591f)\n                curveTo(14.013f, 13.169f, 14.25f, 12.597f, 14.25f, 12f)\n                curveTo(14.25f, 11.403f, 14.013f, 10.831f, 13.591f, 10.409f)\n                curveTo(13.169f, 9.987f, 12.597f, 9.75f, 12f, 9.75f)\n                close()\n            }\n        }.build()\n\n        return _Settings!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Settings: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Share.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Share: ImageVector\n    get() {\n        if (_Share != null) {\n            return _Share!!\n        }\n        _Share = ImageVector.Builder(\n            name = \"Share\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(12.69f, 3.317f)\n                curveTo(12.958f, 3.195f, 13.272f, 3.242f, 13.494f, 3.436f)\n                lineTo(21.494f, 10.436f)\n                curveTo(21.657f, 10.578f, 21.75f, 10.784f, 21.75f, 11f)\n                curveTo(21.75f, 11.216f, 21.657f, 11.422f, 21.494f, 11.564f)\n                lineTo(13.494f, 18.564f)\n                curveTo(13.272f, 18.758f, 12.958f, 18.805f, 12.69f, 18.683f)\n                curveTo(12.422f, 18.561f, 12.25f, 18.294f, 12.25f, 18f)\n                verticalLineTo(14.816f)\n                curveTo(11.47f, 14.941f, 10.65f, 15.237f, 9.821f, 15.653f)\n                curveTo(8.725f, 16.203f, 7.66f, 16.938f, 6.72f, 17.68f)\n                curveTo(5.781f, 18.421f, 4.982f, 19.156f, 4.416f, 19.697f)\n                curveTo(4.219f, 19.885f, 4.055f, 20.046f, 3.919f, 20.179f)\n                curveTo(3.857f, 20.239f, 3.802f, 20.293f, 3.752f, 20.341f)\n                curveTo(3.677f, 20.414f, 3.605f, 20.483f, 3.549f, 20.532f)\n                lineTo(3.547f, 20.534f)\n                curveTo(3.528f, 20.55f, 3.47f, 20.601f, 3.4f, 20.643f)\n                curveTo(3.379f, 20.656f, 3.338f, 20.679f, 3.284f, 20.701f)\n                curveTo(3.246f, 20.716f, 3.117f, 20.766f, 2.946f, 20.753f)\n                curveTo(2.716f, 20.736f, 2.446f, 20.601f, 2.315f, 20.31f)\n                curveTo(2.222f, 20.101f, 2.253f, 19.917f, 2.262f, 19.867f)\n                lineTo(2.263f, 19.861f)\n                lineTo(2.263f, 19.861f)\n                curveTo(3.223f, 14.755f, 5.645f, 8.745f, 12.25f, 7.374f)\n                verticalLineTo(4f)\n                curveTo(12.25f, 3.706f, 12.422f, 3.439f, 12.69f, 3.317f)\n                close()\n                moveTo(13.75f, 5.653f)\n                verticalLineTo(8f)\n                curveTo(13.75f, 8.37f, 13.481f, 8.684f, 13.116f, 8.741f)\n                curveTo(7.983f, 9.543f, 5.515f, 13.458f, 4.29f, 17.769f)\n                curveTo(4.731f, 17.374f, 5.237f, 16.94f, 5.791f, 16.503f)\n                curveTo(6.778f, 15.724f, 7.931f, 14.923f, 9.149f, 14.312f)\n                curveTo(10.361f, 13.704f, 11.682f, 13.261f, 12.994f, 13.25f)\n                curveTo(13.194f, 13.248f, 13.386f, 13.327f, 13.528f, 13.467f)\n                curveTo(13.67f, 13.608f, 13.75f, 13.8f, 13.75f, 14f)\n                verticalLineTo(16.347f)\n                lineTo(19.861f, 11f)\n                lineTo(13.75f, 5.653f)\n                close()\n                moveTo(2.555f, 19.408f)\n                curveTo(2.555f, 19.408f, 2.557f, 19.406f, 2.561f, 19.403f)\n                curveTo(2.557f, 19.406f, 2.555f, 19.408f, 2.555f, 19.408f)\n                close()\n            }\n        }.build()\n\n        return _Share!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Share: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Sort123.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Sort123: ImageVector\n    get() {\n        if (_Sort123 != null) {\n            return _Sort123!!\n        }\n        _Sort123 = ImageVector.Builder(\n            name = \"Sort123\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(10.79f, 5.513f)\n                curveTo(11.085f, 5.211f, 11.477f, 5.03f, 11.894f, 5.003f)\n                curveTo(12.311f, 4.977f, 12.722f, 5.106f, 13.052f, 5.367f)\n                lineTo(13.213f, 5.513f)\n                lineTo(23.498f, 16.013f)\n                lineTo(23.64f, 16.177f)\n                lineTo(23.733f, 16.312f)\n                lineTo(23.825f, 16.48f)\n                lineTo(23.854f, 16.543f)\n                lineTo(23.901f, 16.66f)\n                lineTo(23.955f, 16.849f)\n                lineTo(23.973f, 16.942f)\n                lineTo(23.99f, 17.047f)\n                lineTo(23.997f, 17.147f)\n                lineTo(24f, 17.25f)\n                lineTo(23.997f, 17.353f)\n                lineTo(23.988f, 17.455f)\n                lineTo(23.973f, 17.56f)\n                lineTo(23.955f, 17.651f)\n                lineTo(23.901f, 17.84f)\n                lineTo(23.854f, 17.957f)\n                lineTo(23.734f, 18.188f)\n                lineTo(23.623f, 18.346f)\n                lineTo(23.498f, 18.487f)\n                lineTo(23.337f, 18.632f)\n                lineTo(23.205f, 18.727f)\n                lineTo(23.04f, 18.822f)\n                lineTo(22.978f, 18.851f)\n                lineTo(22.864f, 18.899f)\n                lineTo(22.678f, 18.955f)\n                lineTo(22.588f, 18.972f)\n                lineTo(22.485f, 18.99f)\n                lineTo(22.387f, 18.997f)\n                lineTo(22.286f, 19f)\n                horizontalLineTo(1.717f)\n                curveTo(0.257f, 19f, -0.506f, 17.274f, 0.375f, 16.16f)\n                lineTo(0.505f, 16.013f)\n                lineTo(10.79f, 5.513f)\n                close()\n            }\n        }.build()\n\n        return _Sort123!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Sort123: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Sort321.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Sort321: ImageVector\n    get() {\n        if (_Sort321 != null) {\n            return _Sort321!!\n        }\n        _Sort321 = ImageVector.Builder(\n            name = \"Sort321\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(22.283f, 5f)\n                curveTo(23.743f, 5f, 24.506f, 6.726f, 23.625f, 7.84f)\n                lineTo(23.495f, 7.987f)\n                lineTo(13.209f, 18.487f)\n                curveTo(12.914f, 18.789f, 12.521f, 18.97f, 12.104f, 18.997f)\n                curveTo(11.688f, 19.023f, 11.276f, 18.894f, 10.946f, 18.633f)\n                lineTo(10.785f, 18.487f)\n                lineTo(0.499f, 7.987f)\n                lineTo(0.357f, 7.823f)\n                lineTo(0.264f, 7.688f)\n                lineTo(0.171f, 7.52f)\n                lineTo(0.142f, 7.457f)\n                lineTo(0.096f, 7.34f)\n                lineTo(0.041f, 7.151f)\n                lineTo(0.024f, 7.058f)\n                lineTo(0.007f, 6.953f)\n                lineTo(0f, 6.853f)\n                verticalLineTo(6.647f)\n                lineTo(0.009f, 6.545f)\n                lineTo(0.024f, 6.44f)\n                lineTo(0.041f, 6.349f)\n                lineTo(0.096f, 6.16f)\n                lineTo(0.142f, 6.043f)\n                lineTo(0.262f, 5.812f)\n                lineTo(0.374f, 5.655f)\n                lineTo(0.499f, 5.513f)\n                lineTo(0.66f, 5.368f)\n                lineTo(0.792f, 5.273f)\n                lineTo(0.957f, 5.179f)\n                lineTo(1.018f, 5.149f)\n                lineTo(1.133f, 5.102f)\n                lineTo(1.318f, 5.045f)\n                lineTo(1.409f, 5.028f)\n                lineTo(1.512f, 5.01f)\n                lineTo(1.61f, 5.003f)\n                lineTo(22.283f, 5f)\n                close()\n            }\n        }.build()\n\n        return _Sort321!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Sort321: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Speaker.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Speaker: ImageVector\n    get() {\n        if (_Speaker != null) {\n            return _Speaker!!\n        }\n        _Speaker = ImageVector.Builder(\n            name = \"Speaker\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(11.94f, 16.139f)\n                lineTo(16.839f, 18.968f)\n                lineTo(18.771f, 18.45f)\n                lineTo(14.63f, 2.996f)\n                lineTo(12.698f, 3.513f)\n                lineTo(9.869f, 8.412f)\n                lineTo(3.108f, 10.224f)\n                curveTo(2.045f, 10.509f, 1.409f, 11.611f, 1.694f, 12.673f)\n                lineTo(2.729f, 16.537f)\n                curveTo(3.014f, 17.6f, 4.116f, 18.236f, 5.178f, 17.951f)\n                lineTo(6.144f, 17.692f)\n                lineTo(6.921f, 20.59f)\n                curveTo(7.206f, 21.653f, 8.308f, 22.289f, 9.37f, 22.004f)\n                lineTo(11.302f, 21.487f)\n                lineTo(10.008f, 16.657f)\n                lineTo(11.94f, 16.139f)\n                close()\n                moveTo(9.832f, 20.638f)\n                lineTo(8.538f, 15.809f)\n                lineTo(12.109f, 14.852f)\n                lineTo(17.008f, 17.68f)\n                lineTo(17.301f, 17.602f)\n                lineTo(13.781f, 4.465f)\n                lineTo(13.488f, 4.544f)\n                lineTo(10.66f, 9.443f)\n                lineTo(3.418f, 11.383f)\n                curveTo(2.996f, 11.496f, 2.74f, 11.94f, 2.853f, 12.363f)\n                lineTo(3.888f, 16.226f)\n                curveTo(4.001f, 16.649f, 4.445f, 16.905f, 4.868f, 16.792f)\n                lineTo(6.993f, 16.223f)\n                lineTo(8.08f, 20.28f)\n                curveTo(8.193f, 20.702f, 8.637f, 20.958f, 9.06f, 20.845f)\n                lineTo(9.832f, 20.638f)\n                close()\n                moveTo(20.507f, 12.565f)\n                curveTo(20.194f, 12.806f, 19.824f, 12.992f, 19.409f, 13.103f)\n                lineTo(17.856f, 7.308f)\n                curveTo(18.271f, 7.196f, 18.684f, 7.172f, 19.076f, 7.224f)\n                curveTo(20.23f, 7.379f, 21.201f, 8.203f, 21.53f, 9.429f)\n                curveTo(21.858f, 10.655f, 21.429f, 11.853f, 20.507f, 12.565f)\n                close()\n                moveTo(19.436f, 8.57f)\n                lineTo(20.146f, 11.22f)\n                curveTo(20.424f, 10.811f, 20.517f, 10.285f, 20.371f, 9.739f)\n                curveTo(20.225f, 9.194f, 19.881f, 8.785f, 19.436f, 8.57f)\n                close()\n            }\n        }.build()\n\n        return _Speaker!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Speaker: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/SpeedLimiter.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.SpeedLimiter: ImageVector\n    get() {\n        if (_SpeedLimiter != null) {\n            return _SpeedLimiter!!\n        }\n        _SpeedLimiter = ImageVector.Builder(\n            name = \"SpeedLimiter\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(18f, 15f)\n                curveTo(18f, 17.6f, 16.8f, 19.9f, 14.9f, 21.3f)\n                lineTo(14.4f, 20.8f)\n                lineTo(12.3f, 18.7f)\n                lineTo(13.7f, 17.3f)\n                lineTo(14.9f, 18.5f)\n                curveTo(15.4f, 17.8f, 15.8f, 16.9f, 15.9f, 16f)\n                horizontalLineTo(14f)\n                verticalLineTo(14f)\n                horizontalLineTo(15.9f)\n                curveTo(15.7f, 13.1f, 15.4f, 12.3f, 14.9f, 11.5f)\n                lineTo(13.7f, 12.7f)\n                lineTo(12.3f, 11.3f)\n                lineTo(13.5f, 10.1f)\n                curveTo(12.8f, 9.6f, 11.9f, 9.2f, 11f, 9.1f)\n                verticalLineTo(11f)\n                horizontalLineTo(9f)\n                verticalLineTo(9.1f)\n                curveTo(8.1f, 9.3f, 7.3f, 9.6f, 6.5f, 10.1f)\n                lineTo(9.5f, 13.1f)\n                curveTo(9.7f, 13.1f, 9.8f, 13f, 10f, 13f)\n                curveTo(10.53f, 13f, 11.039f, 13.211f, 11.414f, 13.586f)\n                curveTo(11.789f, 13.961f, 12f, 14.47f, 12f, 15f)\n                curveTo(12f, 15.53f, 11.789f, 16.039f, 11.414f, 16.414f)\n                curveTo(11.039f, 16.789f, 10.53f, 17f, 10f, 17f)\n                curveTo(8.89f, 17f, 8f, 16.11f, 8f, 15f)\n                curveTo(8f, 14.8f, 8f, 14.7f, 8.1f, 14.5f)\n                lineTo(5.1f, 11.5f)\n                curveTo(4.6f, 12.2f, 4.2f, 13.1f, 4.1f, 14f)\n                horizontalLineTo(6f)\n                verticalLineTo(16f)\n                horizontalLineTo(4.1f)\n                curveTo(4.3f, 16.9f, 4.6f, 17.7f, 5.1f, 18.5f)\n                lineTo(6.3f, 17.3f)\n                lineTo(7.7f, 18.7f)\n                lineTo(5.1f, 21.3f)\n                curveTo(3.2f, 19.9f, 2f, 17.6f, 2f, 15f)\n                curveTo(2f, 10.58f, 5.58f, 7f, 10f, 7f)\n                curveTo(14.42f, 7f, 18f, 10.58f, 18f, 15f)\n                close()\n                moveTo(23f, 5f)\n                curveTo(23f, 3.34f, 21.66f, 2f, 20f, 2f)\n                curveTo(18.34f, 2f, 17f, 3.34f, 17f, 5f)\n                curveTo(17f, 6.3f, 17.84f, 7.4f, 19f, 7.82f)\n                verticalLineTo(11f)\n                horizontalLineTo(21f)\n                verticalLineTo(7.82f)\n                curveTo(22.16f, 7.4f, 23f, 6.3f, 23f, 5f)\n                close()\n                moveTo(20f, 6f)\n                curveTo(19.45f, 6f, 19f, 5.55f, 19f, 5f)\n                curveTo(19f, 4.45f, 19.45f, 4f, 20f, 4f)\n                curveTo(20.55f, 4f, 21f, 4.45f, 21f, 5f)\n                curveTo(21f, 5.55f, 20.55f, 6f, 20f, 6f)\n                close()\n            }\n        }.build()\n\n        return _SpeedLimiter!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _SpeedLimiter: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Stop.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Stop: ImageVector\n    get() {\n        if (_Stop != null) {\n            return _Stop!!\n        }\n        _Stop = ImageVector.Builder(\n            name = \"Stop\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(7f, 5.75f)\n                curveTo(6.668f, 5.75f, 6.351f, 5.882f, 6.116f, 6.116f)\n                curveTo(5.882f, 6.351f, 5.75f, 6.668f, 5.75f, 7f)\n                verticalLineTo(17f)\n                curveTo(5.75f, 17.331f, 5.882f, 17.649f, 6.116f, 17.884f)\n                curveTo(6.351f, 18.118f, 6.668f, 18.25f, 7f, 18.25f)\n                horizontalLineTo(17f)\n                curveTo(17.331f, 18.25f, 17.649f, 18.118f, 17.884f, 17.884f)\n                curveTo(18.118f, 17.649f, 18.25f, 17.331f, 18.25f, 17f)\n                verticalLineTo(7f)\n                curveTo(18.25f, 6.668f, 18.118f, 6.351f, 17.884f, 6.116f)\n                curveTo(17.649f, 5.882f, 17.331f, 5.75f, 17f, 5.75f)\n                horizontalLineTo(7f)\n                close()\n                moveTo(5.055f, 5.055f)\n                curveTo(5.571f, 4.54f, 6.271f, 4.25f, 7f, 4.25f)\n                horizontalLineTo(17f)\n                curveTo(17.729f, 4.25f, 18.429f, 4.54f, 18.944f, 5.055f)\n                curveTo(19.46f, 5.571f, 19.75f, 6.271f, 19.75f, 7f)\n                verticalLineTo(17f)\n                curveTo(19.75f, 17.729f, 19.46f, 18.429f, 18.944f, 18.944f)\n                curveTo(18.429f, 19.46f, 17.729f, 19.75f, 17f, 19.75f)\n                horizontalLineTo(7f)\n                curveTo(6.271f, 19.75f, 5.571f, 19.46f, 5.055f, 18.944f)\n                curveTo(4.54f, 18.429f, 4.25f, 17.729f, 4.25f, 17f)\n                verticalLineTo(7f)\n                curveTo(4.25f, 6.271f, 4.54f, 5.571f, 5.055f, 5.055f)\n                close()\n            }\n        }.build()\n\n        return _Stop!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Stop: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/StopAll.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.StopAll: ImageVector\n    get() {\n        if (_StopAll != null) {\n            return _StopAll!!\n        }\n        _StopAll = ImageVector.Builder(\n            name = \"StopAll\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(10.098f, 2.437f)\n                curveTo(11.989f, 2.061f, 13.95f, 2.254f, 15.731f, 2.992f)\n                curveTo(17.513f, 3.73f, 19.035f, 4.98f, 20.107f, 6.583f)\n                curveTo(21.178f, 8.187f, 21.75f, 10.072f, 21.75f, 12f)\n                curveTo(21.75f, 12.414f, 21.414f, 12.75f, 21f, 12.75f)\n                curveTo(20.586f, 12.75f, 20.25f, 12.414f, 20.25f, 12f)\n                curveTo(20.25f, 10.368f, 19.766f, 8.773f, 18.86f, 7.417f)\n                curveTo(17.953f, 6.06f, 16.665f, 5.002f, 15.157f, 4.378f)\n                curveTo(13.65f, 3.754f, 11.991f, 3.59f, 10.391f, 3.909f)\n                curveTo(8.79f, 4.227f, 7.32f, 5.013f, 6.166f, 6.166f)\n                curveTo(5.013f, 7.32f, 4.227f, 8.79f, 3.909f, 10.391f)\n                curveTo(3.59f, 11.991f, 3.754f, 13.65f, 4.378f, 15.157f)\n                curveTo(5.002f, 16.665f, 6.06f, 17.953f, 7.417f, 18.86f)\n                curveTo(8.773f, 19.766f, 10.368f, 20.25f, 12f, 20.25f)\n                curveTo(12.414f, 20.25f, 12.75f, 20.586f, 12.75f, 21f)\n                curveTo(12.75f, 21.414f, 12.414f, 21.75f, 12f, 21.75f)\n                curveTo(10.072f, 21.75f, 8.187f, 21.178f, 6.583f, 20.107f)\n                curveTo(4.98f, 19.035f, 3.73f, 17.513f, 2.992f, 15.731f)\n                curveTo(2.254f, 13.95f, 2.061f, 11.989f, 2.437f, 10.098f)\n                curveTo(2.814f, 8.207f, 3.742f, 6.469f, 5.106f, 5.106f)\n                curveTo(6.469f, 3.742f, 8.207f, 2.814f, 10.098f, 2.437f)\n                close()\n                moveTo(12f, 6.25f)\n                curveTo(12.414f, 6.25f, 12.75f, 6.586f, 12.75f, 7f)\n                verticalLineTo(11.689f)\n                lineTo(13.53f, 12.47f)\n                curveTo(13.823f, 12.763f, 13.823f, 13.237f, 13.53f, 13.53f)\n                curveTo(13.237f, 13.823f, 12.763f, 13.823f, 12.47f, 13.53f)\n                lineTo(11.47f, 12.53f)\n                curveTo(11.329f, 12.39f, 11.25f, 12.199f, 11.25f, 12f)\n                verticalLineTo(7f)\n                curveTo(11.25f, 6.586f, 11.586f, 6.25f, 12f, 6.25f)\n                close()\n                moveTo(15.25f, 16f)\n                curveTo(15.25f, 15.586f, 15.586f, 15.25f, 16f, 15.25f)\n                horizontalLineTo(22f)\n                curveTo(22.414f, 15.25f, 22.75f, 15.586f, 22.75f, 16f)\n                verticalLineTo(22f)\n                curveTo(22.75f, 22.414f, 22.414f, 22.75f, 22f, 22.75f)\n                horizontalLineTo(16f)\n                curveTo(15.586f, 22.75f, 15.25f, 22.414f, 15.25f, 22f)\n                verticalLineTo(16f)\n                close()\n                moveTo(16.75f, 16.75f)\n                verticalLineTo(21.25f)\n                horizontalLineTo(21.25f)\n                verticalLineTo(16.75f)\n                horizontalLineTo(16.75f)\n                close()\n            }\n        }.build()\n\n        return _StopAll!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _StopAll: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Telegram.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Telegram: ImageVector\n    get() {\n        if (_Telegram != null) {\n            return _Telegram!!\n        }\n        _Telegram = ImageVector.Builder(\n            name = \"Telegram\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = Brush.linearGradient(\n                    colorStops = arrayOf(\n                        0f to Color(0xFF2AABEE),\n                        1f to Color(0xFF229ED9)\n                    ),\n                    start = Offset(1200f, 0f),\n                    end = Offset(1200f, 2400f)\n                )\n            ) {\n                moveTo(12f, 0f)\n                curveTo(8.818f, 0f, 5.764f, 1.265f, 3.516f, 3.515f)\n                curveTo(1.265f, 5.765f, 0.001f, 8.817f, 0f, 12f)\n                curveTo(0f, 15.181f, 1.266f, 18.236f, 3.516f, 20.485f)\n                curveTo(5.764f, 22.735f, 8.818f, 24f, 12f, 24f)\n                curveTo(15.182f, 24f, 18.236f, 22.735f, 20.484f, 20.485f)\n                curveTo(22.734f, 18.236f, 24f, 15.181f, 24f, 12f)\n                curveTo(24f, 8.819f, 22.734f, 5.764f, 20.484f, 3.515f)\n                curveTo(18.236f, 1.265f, 15.182f, 0f, 12f, 0f)\n                close()\n            }\n            path(fill = SolidColor(Color.White)) {\n                moveTo(5.432f, 11.873f)\n                curveTo(8.931f, 10.349f, 11.263f, 9.344f, 12.429f, 8.859f)\n                curveTo(15.763f, 7.473f, 16.455f, 7.232f, 16.907f, 7.224f)\n                curveTo(17.006f, 7.222f, 17.228f, 7.247f, 17.372f, 7.364f)\n                curveTo(17.492f, 7.462f, 17.526f, 7.595f, 17.542f, 7.689f)\n                curveTo(17.558f, 7.782f, 17.578f, 7.995f, 17.561f, 8.161f)\n                curveTo(17.381f, 10.058f, 16.599f, 14.663f, 16.202f, 16.788f)\n                curveTo(16.035f, 17.688f, 15.703f, 17.989f, 15.382f, 18.018f)\n                curveTo(14.685f, 18.083f, 14.156f, 17.558f, 13.481f, 17.116f)\n                curveTo(12.426f, 16.423f, 11.829f, 15.992f, 10.804f, 15.317f)\n                curveTo(9.619f, 14.536f, 10.387f, 14.107f, 11.063f, 13.406f)\n                curveTo(11.239f, 13.222f, 14.31f, 10.429f, 14.368f, 10.176f)\n                curveTo(14.376f, 10.144f, 14.383f, 10.026f, 14.312f, 9.964f)\n                curveTo(14.243f, 9.901f, 14.139f, 9.923f, 14.064f, 9.94f)\n                curveTo(13.958f, 9.964f, 12.272f, 11.079f, 9.002f, 13.285f)\n                curveTo(8.524f, 13.614f, 8.091f, 13.774f, 7.701f, 13.766f)\n                curveTo(7.273f, 13.757f, 6.448f, 13.524f, 5.835f, 13.325f)\n                curveTo(5.085f, 13.08f, 4.487f, 12.951f, 4.539f, 12.536f)\n                curveTo(4.566f, 12.32f, 4.864f, 12.099f, 5.432f, 11.873f)\n                close()\n            }\n        }.build()\n\n        return _Telegram!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Telegram: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Theme.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Theme: ImageVector\n    get() {\n        if (_Theme != null) {\n            return _Theme!!\n        }\n        _Theme = ImageVector.Builder(\n            name = \"Theme\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(8.2f, 13.2f)\n                curveTo(9.218f, 10.505f, 10.944f, 8.135f, 13.197f, 6.339f)\n                curveTo(15.45f, 4.544f, 18.146f, 3.39f, 21f, 3f)\n                curveTo(20.61f, 5.854f, 19.456f, 8.55f, 17.66f, 10.803f)\n                curveTo(15.865f, 13.056f, 13.495f, 14.782f, 10.8f, 15.8f)\n                moveTo(10.6f, 9f)\n                curveTo(12.543f, 9.897f, 14.103f, 11.457f, 15f, 13.4f)\n                moveTo(3f, 21f)\n                verticalLineTo(17f)\n                curveTo(3f, 16.209f, 3.235f, 15.436f, 3.674f, 14.778f)\n                curveTo(4.114f, 14.12f, 4.738f, 13.607f, 5.469f, 13.304f)\n                curveTo(6.2f, 13.002f, 7.004f, 12.922f, 7.78f, 13.077f)\n                curveTo(8.556f, 13.231f, 9.269f, 13.612f, 9.828f, 14.172f)\n                curveTo(10.388f, 14.731f, 10.769f, 15.444f, 10.923f, 16.22f)\n                curveTo(11.078f, 16.996f, 10.998f, 17.8f, 10.696f, 18.531f)\n                curveTo(10.393f, 19.262f, 9.88f, 19.886f, 9.222f, 20.326f)\n                curveTo(8.564f, 20.765f, 7.791f, 21f, 7f, 21f)\n                horizontalLineTo(3f)\n                close()\n            }\n        }.build()\n\n        return _Theme!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Theme: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Undo.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Undo: ImageVector\n    get() {\n        if (_Undo != null) {\n            return _Undo!!\n        }\n        _Undo = ImageVector.Builder(\n            name = \"Undo\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(9f, 14f)\n                lineTo(5f, 10f)\n                moveTo(5f, 10f)\n                lineTo(9f, 6f)\n                moveTo(5f, 10f)\n                horizontalLineTo(16f)\n                curveTo(17.061f, 10f, 18.078f, 10.421f, 18.828f, 11.172f)\n                curveTo(19.579f, 11.922f, 20f, 12.939f, 20f, 14f)\n                curveTo(20f, 15.061f, 19.579f, 16.078f, 18.828f, 16.828f)\n                curveTo(18.078f, 17.579f, 17.061f, 18f, 16f, 18f)\n                horizontalLineTo(15f)\n            }\n        }.build()\n\n        return _Undo!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Undo: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/Up.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.Up: ImageVector\n    get() {\n        if (_Up != null) {\n            return _Up!!\n        }\n        _Up = ImageVector.Builder(\n            name = \"Up\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(11.293f, 8.293f)\n                curveTo(11.683f, 7.902f, 12.317f, 7.902f, 12.707f, 8.293f)\n                lineTo(18.707f, 14.293f)\n                curveTo(19.098f, 14.683f, 19.098f, 15.317f, 18.707f, 15.707f)\n                curveTo(18.317f, 16.098f, 17.683f, 16.098f, 17.293f, 15.707f)\n                lineTo(12f, 10.414f)\n                lineTo(6.707f, 15.707f)\n                curveTo(6.317f, 16.098f, 5.683f, 16.098f, 5.293f, 15.707f)\n                curveTo(4.902f, 15.317f, 4.902f, 14.683f, 5.293f, 14.293f)\n                lineTo(11.293f, 8.293f)\n                close()\n            }\n        }.build()\n\n        return _Up!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _Up: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/VerticalDirection.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.StrokeJoin\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.VerticalDirection: ImageVector\n    get() {\n        if (_VerticalDirection != null) {\n            return _VerticalDirection!!\n        }\n        _VerticalDirection = ImageVector.Builder(\n            name = \"VerticalDirection\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                stroke = SolidColor(Color.White),\n                strokeLineWidth = 2f,\n                strokeLineCap = StrokeCap.Round,\n                strokeLineJoin = StrokeJoin.Round\n            ) {\n                moveTo(9f, 10f)\n                lineTo(12f, 7f)\n                lineTo(15f, 10f)\n                moveTo(9f, 14f)\n                lineTo(12f, 17f)\n                lineTo(15f, 14f)\n            }\n        }.build()\n\n        return _VerticalDirection!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _VerticalDirection: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/WindowClose.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.WindowClose: ImageVector\n    get() {\n        if (_WindowClose != null) {\n            return _WindowClose!!\n        }\n        _WindowClose = ImageVector.Builder(\n            name = \"WindowClose\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(0f, 22f)\n                lineTo(22f, 0f)\n                lineTo(24f, 2f)\n                lineTo(2f, 24f)\n                lineTo(0f, 22f)\n                close()\n            }\n            path(fill = SolidColor(Color.White)) {\n                moveTo(22f, 24f)\n                lineTo(0f, 2f)\n                lineTo(2f, 0f)\n                lineTo(24f, 22f)\n                lineTo(22f, 24f)\n                close()\n            }\n        }.build()\n\n        return _WindowClose!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _WindowClose: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/WindowFloating.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.WindowFloating: ImageVector\n    get() {\n        if (_WindowFloating != null) {\n            return _WindowFloating!!\n        }\n        _WindowFloating = ImageVector.Builder(\n            name = \"WindowFloating\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(6.545f, 0f)\n                verticalLineTo(6.545f)\n                horizontalLineTo(0f)\n                verticalLineTo(24f)\n                horizontalLineTo(17.455f)\n                verticalLineTo(17.455f)\n                horizontalLineTo(24f)\n                verticalLineTo(0f)\n                horizontalLineTo(6.545f)\n                close()\n                moveTo(21.818f, 2.182f)\n                horizontalLineTo(8.727f)\n                verticalLineTo(6.545f)\n                horizontalLineTo(17.455f)\n                verticalLineTo(15.273f)\n                horizontalLineTo(21.818f)\n                verticalLineTo(2.182f)\n                close()\n                moveTo(15.273f, 15.273f)\n                verticalLineTo(21.818f)\n                horizontalLineTo(2.182f)\n                verticalLineTo(8.727f)\n                horizontalLineTo(8.727f)\n                horizontalLineTo(15.273f)\n                verticalLineTo(15.273f)\n                close()\n            }\n        }.build()\n\n        return _WindowFloating!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _WindowFloating: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/WindowMaximize.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.WindowMaximize: ImageVector\n    get() {\n        if (_WindowMaximize != null) {\n            return _WindowMaximize!!\n        }\n        _WindowMaximize = ImageVector.Builder(\n            name = \"WindowMaximize\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color.White),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(21.818f, 2.182f)\n                horizontalLineTo(2.182f)\n                verticalLineTo(21.818f)\n                horizontalLineTo(21.818f)\n                verticalLineTo(2.182f)\n                close()\n                moveTo(0f, 0f)\n                verticalLineTo(24f)\n                horizontalLineTo(24f)\n                verticalLineTo(0f)\n                horizontalLineTo(0f)\n                close()\n            }\n        }.build()\n\n        return _WindowMaximize!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _WindowMaximize: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/kotlin/com/abdownloadmanager/resources/icons/WindowMinimize.kt",
    "content": "package com.abdownloadmanager.resources.icons\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ABDMIcons.WindowMinimize: ImageVector\n    get() {\n        if (_WindowMinimize != null) {\n            return _WindowMinimize!!\n        }\n        _WindowMinimize = ImageVector.Builder(\n            name = \"WindowMinimize\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(fill = SolidColor(Color.White)) {\n                moveTo(0f, 10.909f)\n                horizontalLineTo(24f)\n                verticalLineTo(13.091f)\n                horizontalLineTo(0f)\n                verticalLineTo(10.909f)\n                close()\n            }\n        }.build()\n\n        return _WindowMinimize!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _WindowMinimize: ImageVector? = null\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/credits/translators.json",
    "content": "{\n  \"ar\": [\n    {\n      \"name\": \"Hani Rouatbi (lamjed001)\",\n      \"link\": \"https://crowdin.com/profile/lamjed001\"\n    },\n    {\n      \"name\": \"BENZAZOU Omar (lamtarius)\",\n      \"link\": \"https://crowdin.com/profile/lamtarius\"\n    },\n    {\n      \"name\": \"abuda7m (abuda7mx)\",\n      \"link\": \"https://crowdin.com/profile/abuda7mx\"\n    },\n    {\n      \"name\": \"Mohamed Mahmoud (master3lwa)\",\n      \"link\": \"https://crowdin.com/profile/master3lwa\"\n    },\n    {\n      \"name\": \"amirhosseinshammari (hosseinSh1379)\",\n      \"link\": \"https://crowdin.com/profile/hosseinSh1379\"\n    },\n    {\n      \"name\": \"خالد العمري (KhaledAl2mri)\",\n      \"link\": \"https://crowdin.com/profile/KhaledAl2mri\"\n    }\n  ],\n  \"bn\": [\n    {\n      \"name\": \"Md Shariful Islam (Shariful7972)\",\n      \"link\": \"https://crowdin.com/profile/Shariful7972\"\n    }\n  ],\n  \"bqi\": [\n    {\n      \"name\": \"Hossein Abaspanah (hosseinabaspanah)\",\n      \"link\": \"https://crowdin.com/profile/hosseinabaspanah\"\n    }\n  ],\n  \"ckb\": [\n    {\n      \"name\": \"Halbast Abdullah (halbast)\",\n      \"link\": \"https://crowdin.com/profile/halbast\"\n    }\n  ],\n  \"de\": [\n    {\n      \"name\": \"u!^DEV (uDEV)\",\n      \"link\": \"https://crowdin.com/profile/uDEV\"\n    },\n    {\n      \"name\": \"xNino\",\n      \"link\": \"https://crowdin.com/profile/xNino\"\n    },\n    {\n      \"name\": \"Leon Aissa (Dimiikou)\",\n      \"link\": \"https://crowdin.com/profile/Dimiikou\"\n    },\n    {\n      \"name\": \"Lukas (lukasitaly)\",\n      \"link\": \"https://github.com/lukasitaly\"\n    }\n  ],\n  \"es-ES\": [\n    {\n      \"name\": \"c-sanchez\",\n      \"link\": \"https://crowdin.com/profile/c-sanchez\"\n    },\n    {\n      \"name\": \"seniordevops\",\n      \"link\": \"https://crowdin.com/profile/seniordevops\"\n    },\n    {\n      \"name\": \"Armin Deck (armindeck)\",\n      \"link\": \"https://crowdin.com/profile/armindeck\"\n    }\n  ],\n  \"fa\": [\n    {\n      \"name\": \"AmirHossein Abdolmotallebi (amira1376)\",\n      \"link\": \"https://crowdin.com/profile/amira1376\"\n    },\n    {\n      \"name\": \"HAM3D\",\n      \"link\": \"https://crowdin.com/profile/HAM3D\"\n    },\n    {\n      \"name\": \"Ehsan Narmani (kotlinx)\",\n      \"link\": \"https://crowdin.com/profile/kotlinx\"\n    }\n  ],\n  \"fi\": [\n    {\n      \"name\": \"Oskari Lavinto (olavinto)\",\n      \"link\": \"https://crowdin.com/profile/olavinto\"\n    }\n  ],\n  \"fr\": [\n    {\n      \"name\": \"Gooseman (realgooseman)\",\n      \"link\": \"https://crowdin.com/profile/realgooseman\"\n    },\n    {\n      \"name\": \"Hani Rouatbi (lamjed001)\",\n      \"link\": \"https://crowdin.com/profile/lamjed001\"\n    }\n  ],\n  \"hu\": [\n    {\n      \"name\": \"John Fowler (JohnFowler58)\",\n      \"link\": \"https://crowdin.com/profile/JohnFowler58\"\n    }\n  ],\n  \"id\": [\n    {\n      \"name\": \"Nicedward (dayat219onlyme)\",\n      \"link\": \"https://crowdin.com/profile/dayat219onlyme\"\n    },\n    {\n      \"name\": \"Christian Elbrianno (crse)\",\n      \"link\": \"https://crowdin.com/profile/crse\"\n    },\n    {\n      \"name\": \"kuydukuy (hilmy2nd)\",\n      \"link\": \"https://crowdin.com/profile/hilmy2nd\"\n    },\n    {\n      \"name\": \"andybr\",\n      \"link\": \"https://crowdin.com/profile/andybr\"\n    },\n    {\n      \"name\": \"exodius\",\n      \"link\": \"https://crowdin.com/profile/exodius\"\n    }\n  ],\n  \"it\": [\n    {\n      \"name\": \"Hanamichi27\",\n      \"link\": \"https://crowdin.com/profile/Hanamichi27\"\n    },\n    {\n      \"name\": \"ROBERTO BORIOTTI (bovirus)\",\n      \"link\": \"https://crowdin.com/profile/bovirus\"\n    },\n    {\n      \"name\": \"Lukas (lukasitaly)\",\n      \"link\": \"https://github.com/lukasitaly\"\n    }\n  ],\n  \"ja\": [\n    {\n      \"name\": \"ユタ (dsi3020)\",\n      \"link\": \"https://crowdin.com/profile/dsi3020\"\n    }\n  ],\n  \"ka\": [\n    {\n      \"name\": \"system32 (Symtax)\",\n      \"link\": \"https://crowdin.com/profile/Symtax\"\n    }\n  ],\n  \"ko\": [\n    {\n      \"name\": \"doda (ddarkr)\",\n      \"link\": \"https://crowdin.com/profile/ddarkr\"\n    },\n    {\n      \"name\": \"VenusGirl\",\n      \"link\": \"https://crowdin.com/profile/VenusGirl\"\n    }\n  ],\n  \"lt\": [\n    {\n      \"name\": \"arvaidasr\",\n      \"link\": \"https://crowdin.com/profile/arvaidasr\"\n    }\n  ],\n  \"pl\": [\n    {\n      \"name\": \"Patrxgt\",\n      \"link\": \"https://crowdin.com/profile/Patrxgt\"\n    }\n  ],\n  \"pt-BR\": [\n    {\n      \"name\": \"Guilherme (whatahelll)\",\n      \"link\": \"https://crowdin.com/profile/whatahelll\"\n    },\n    {\n      \"name\": \"Jean Pereira (JeanxPereira)\",\n      \"link\": \"https://crowdin.com/profile/JeanxPereira\"\n    },\n    {\n      \"name\": \"Davy J. (deaveipslon)\",\n      \"link\": \"https://crowdin.com/profile/deaveipslon\"\n    },\n    {\n      \"name\": \"Juliano Eduardo (zerocoolroot)\",\n      \"link\": \"https://crowdin.com/profile/zerocoolroot\"\n    },\n    {\n      \"name\": \"John Peter Sá (johnppetersa)\",\n      \"link\": \"https://crowdin.com/profile/johnppetersa\"\n    }\n  ],\n  \"ru\": [\n    {\n      \"name\": \"in-ferno\",\n      \"link\": \"https://crowdin.com/profile/in-ferno\"\n    },\n    {\n      \"name\": \"Semyon (maz1lovo)\",\n      \"link\": \"https://crowdin.com/profile/maz1lovo\"\n    }\n  ],\n  \"sq\": [\n    {\n      \"name\": \"Mario Balla (marjob1234)\",\n      \"link\": \"https://crowdin.com/profile/marjob1234\"\n    }\n  ],\n  \"th\": [\n    {\n      \"name\": \"Anucha (achn.syps)\",\n      \"link\": \"https://crowdin.com/profile/achn.syps\"\n    }\n  ],\n  \"tr\": [\n    {\n      \"name\": \"𝗛𝗼𝗹𝗶 (mikropsoft)\",\n      \"link\": \"https://crowdin.com/profile/mikropsoft\"\n    },\n    {\n      \"name\": \"Nightmare837 gaming (mutlupide)\",\n      \"link\": \"https://crowdin.com/profile/mutlupide\"\n    }\n  ],\n  \"uk\": [\n    {\n      \"name\": \"YALdysse\",\n      \"link\": \"https://crowdin.com/profile/YALdysse\"\n    },\n    {\n      \"name\": \"Misha Dyshlenko (lony_official)\",\n      \"link\": \"https://crowdin.com/profile/lony_official\"\n    }\n  ],\n  \"vi\": [\n    {\n      \"name\": \"Quân Xinh Tươi (chetaoquocte)\",\n      \"link\": \"https://crowdin.com/profile/chetaoquocte\"\n    },\n    {\n      \"name\": \"Zenfast\",\n      \"link\": \"https://crowdin.com/profile/Zenfast\"\n    },\n    {\n      \"name\": \"shellawa\",\n      \"link\": \"https://crowdin.com/profile/shellawa\"\n    }\n  ],\n  \"zh-CN\": [\n    {\n      \"name\": \"Ipmlosion\",\n      \"link\": \"https://crowdin.com/profile/Ipmlosion\"\n    },\n    {\n      \"name\": \"Pylogmon (pylogmon)\",\n      \"link\": \"https://crowdin.com/profile/pylogmon\"\n    },\n    {\n      \"name\": \"Pesy Wu (GamerNoTitle)\",\n      \"link\": \"https://crowdin.com/profile/GamerNoTitle\"\n    },\n    {\n      \"name\": \"Aira_Nadih\",\n      \"link\": \"https://crowdin.com/profile/Aira_Nadih\"\n    },\n    {\n      \"name\": \"Sendevia\",\n      \"link\": \"https://crowdin.com/profile/Sendevia\"\n    },\n    {\n      \"name\": \"none2003\",\n      \"link\": \"https://crowdin.com/profile/none2003\"\n    },\n    {\n      \"name\": \"pompurin404\",\n      \"link\": \"https://github.com/pompurin404\"\n    }\n  ],\n  \"zh-TW\": [\n    {\n      \"name\": \"ɴᴇᴋᴏ (NeKoOuO)\",\n      \"link\": \"https://crowdin.com/profile/NeKoOuO\"\n    },\n    {\n      \"name\": \"notlin4\",\n      \"link\": \"https://crowdin.com/profile/notlin4\"\n    },\n    {\n      \"name\": \"塔可努丨TechNoob (TN_TechNoob)\",\n      \"link\": \"https://crowdin.com/profile/TN_TechNoob\"\n    },\n    {\n      \"name\": \"none2003\",\n      \"link\": \"https://crowdin.com/profile/none2003\"\n    },\n    {\n      \"name\": \"jrthsr700tmax\",\n      \"link\": \"https://crowdin.com/profile/jrthsr700tmax\"\n    },\n    {\n      \"name\": \"abc0922001\",\n      \"link\": \"https://crowdin.com/profile/abc0922001\"\n    },\n    {\n      \"name\": \"Gholts\",\n      \"link\": \"https://crowdin.com/profile/Gholts\"\n    }\n  ]\n}\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ar_SA.properties",
    "content": "app_title=مدير التنزيل AB\nconfirm_auto_categorize_downloads_title=تصنيف التنزيلات تلقائياً\nconfirm_auto_categorize_downloads_description=سيتم إضافة أي عنصر غير مصنف تلقائيًا إلى الفئة ذات الصلة به.\nconfirm_reset_to_default_categories_title=إعادة تعيين الفئات الافتراضية\nconfirm_reset_to_default_categories_description=سيؤدي هذا إلى إزالة جميع الفئات واستعادة الفئات الافتراضية\\!\nconfirm_delete_download_items_title=تأكيد الحذف\nconfirm_delete_download_items_description=هل أنت متأكد من أنك تريد حذف {{count}} عنصرًا؟\nconfirm_delete_download_unfinished_items_description=هل أنت متأكد أنك تريد حذف {{count}} من التنزيلات غير المكتملة؟\nconfirm_delete_download_finished_and_unfinished_items_description=هل أنت متأكد أنك تريد حذف {{finishedCount}} التنزيلات المنتهية و{{unfinishedCount}} التنزيلات غير المكتملة؟\nalso_delete_file_from_disk=أيضا حذف الملف من القرص\nconfirm_delete_category_item_title=إزالة الفئة {{name}}\nconfirm_delete_category_item_description=هل أنت متأكد من أنك تريد حذف الفئة \"{{value}}\"؟\nyour_download_will_not_be_deleted=لن يتم حذف التنزيلات الخاصة بك\ndrag_the_file_to_another_app=سحب المِلف إلى تطبيق آخر\ndrop_link_or_file_here=أسقط الرابط أو المِلف هنا.\nnothing_will_be_imported=لن يتم استيراد أي شيء\nn_links_will_be_imported={{count}} روابط سيتم استيرادها\nn_items_selected={{count}} عناصر محددة\nwindow_close=إغلاق\nwindow_minimize=إخفاء\nwindow_maximize=تكبير\nwindow_restore=استعادة\ndelete=حذف\nremove=إزالة\ncancel=إلغاء\nclose=إغلاق\nmenu=القائمة\nmore_options=خيارات إضافية\nok=موافق\nadd=أضف\npaste=لصق\nchange=تغيير\nedit=تعديل\nchange_anyway=تغيير على أي حال\ndownload=تنزيل\nrefresh=تحديث\nsettings=الإعدادات\non_completion=عند الانتهاء\nunknown=غير معروف\nunknown_error=خطأ غير معروف\ndownload_item_not_found=لم يتم العثور على عنصر التنزيل\nname=الاسم\ndownload_link=رابط التنزيل\nnot_finished=لم ينتهي\nall=الكل\nfinished=انتهى\nUnfinished=غير مكتمل\ncanceled=ملغي\nerror=خطأ\npaused=متوقف\ndownloading=جاري التنزيل\nadded=أضيف بنجاح\nidle=خامل\npreparing_file=تحضير الملف\ncreating_file=إنشاء مِلف\nresuming=جارٍ الاستئناف\nretrying=إعادة المحاولة\nlist_is_empty=القائمة فارغة\\!\nsearch_in_the_list=البحث في القائمة\nsearch=البحث\nclear=مسح\ngeneral=العامة\nenabled=مفعّل\ndisabled=معطَّل\ndefault=افتراضي\nfile=الملف\ntasks=المهام\ntools=الأدوات\nhelp=المساعدة\nsystem=النظام\nall_missing_files=جميع الملفات المفقودة\nall_finished=الكل انتهى\nall_unfinished=الكل غير مكتمل\nentire_list=القائمة بأكملها\ndownload_browser_integration=تحميل ملحق الإندماج للمتصفح\nexit=خروج\nshow_downloads=عرض التنزيلات\nnew_download=تنزيل جديد\nstop_all=إيقاف الكل\nimport_from_clipboard=استيراد من الحافظة\nbatch_download=تنزيل دفعة\nopen=فتح\nshare=مشاركة\nopen_file=فتح الملف\nopen_folder=فتح المجلد\nresume=استئناف\npause=إيقاف مؤقت\nrestart_download=إعادة التنزيل\ncopy=نسخ\ncopy_link=نسخ الرابط\ncopy_as_curl=نسخ كـ cURL\nshow_properties=عرض الخصائص\nmove_to_queue=نقل إلى قائمة الانتظار\nmove_to_this_queue=نقل إلى هذا الطابور\nmove_to_category=نقل إلى فئة\nmove_to_this_category=نقل إلى هذه الفئة\ncategories=الفئات\nadd_category=إضافة فئة\nedit_category=تعديل الفئة\ndelete_category=حذف الفئة\ncategory_name=إسم الفئة\ncategory_download_location=موقع تنزيل الفئة\ncategory_download_location_description=عند اختيار هذه الفئة في \"إضافة تنزيل\"، استخدم هذا الدليل كـ \"موقع التنزيل\"\ncategory_file_types=أنواع ملفات الفئة\ncategory_file_types_description=ضع هذه الأنواع من الملفات تلقائيًا في هذه الفئة. (عند إضافة تنزيل جديد)\\nافصل امتدادات الملفات بمسافة (ext2, ext1 ...)\ncategory_url_patterns=أنماط الرابط\ncategory_url_patterns_description=ضع التنزيل تلقائيًا من هذه الروابط في هذه الفئة. (عند إضافة تنزيل جديد)\\nافصل الروابط بمسافة، يمكنك أيضًا استخدام * كرمز بديل\nauto_categorize_downloads=تصنيف التنزيلات تلقائيًا\nrestore_defaults=استعادة الإعدادات الافتراضية\nabout=حول\nversion_n=الإصدار {{value}}\ndeveloped_with_love_for_you=تم تطويره بـــ❤️ لك\ndonate=تبرّع\nvisit_the_project_website=زيارة موقع المشروع\nthis_is_a_free_and_open_source_software=هذا البرنامج مجاني ومفتوح المصدر\nview_the_source_code=الإطلاع على الكود المصدري\nthird_party_libraries=مكتبات الجهات الخارجية\npowered_by_open_source_software=مدعوم بواسطة برمجيات مفتوحة المصدر\nview_the_open_source_licenses=عرض تراخيص المصدر المفتوح\nsupport_and_community=الدعم والمجتمع\ntelegram=تيليجرام\nchannel=القناة\ngroup=المجموعة\nadd_download=إضافة تنزيل\nadd_multi_download_page_header=اختر العناصر التي تريد تنزيلها\nsave_to=حفظ في\nwhere_should_each_item_saved=أين يجب حفظ كل عنصر؟\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=هناك العديد من العناصر\\! يرجى تحديد الطريقة التي تريد حفظها بها\neach_item_on_its_own_category=كل عنصر في فئته الخاصة\neach_item_on_its_own_category_description=سيتم وضع كل عنصر في فئة تحتوي على نوع هذا المِلف\nall_items_in_one_category=كل العناصر في فئة واحدة\nall_items_in_one_category_description=سيتم حفظ كل الملفات في الفئة المختارة\nall_items_in_one_Location=كل العناصر في موقع واحد\nall_items_in_one_Location_description=سيتم حفظ كل العناصر في الدليل المحدد\nunselected_all_items_in_specific_location_description=سيتم حفظ جميع الملفات في موقع الفئة المحددة\nno_category_selected=لم يتم اختيار فئة\nno_categories_found=لم يتم العثور على فئات\ndownload_location=موقع التنزيل\nlocation=المكان\nselect_queue=اختر قائمة الانتظار\nwithout_queue=بدون قائمة انتظار\nuse_category=استخدام فئة\ncant_write_to_this_folder=لا يمكن الكتابة إلى هذا المجلد\nfile_name_already_exists=اسم المِلف موجود سابقا\ndownload_already_exists=الملف موجود بالفعل\ninvalid_file_name=اسم المِلف غير صالح\nshow_solutions=عرض الحلول...\nchange_solution=تغيير الحل\nselect_a_solution=اختر حلاً\nselect_download_strategy_description=الرابط الذي قدمته موجود بالفعل في قوائم التنزيل يرجى تحديد ما تريد القيام به\ndownload_strategy_add_a_numbered_file=إضافة ملف مرقَّم\ndownload_strategy_add_a_numbered_file_description=إضافة مؤشر في نهاية اسم ملف التنزيل\ndownload_strategy_override_existing_file=استبدال الملف الموجود\ndownload_strategy_override_existing_file_description=استبدال الملف القديم بالجديد\ndownload_strategy_update_download_link=تحديث التنزيل الحالي\ndownload_strategy_update_download_link_description=تحديث رابط التنزيل الحالي وبيانات اعتماده\ndownload_strategy_show_downloaded_file=عرض الملف الذي تم تنزيله\ndownload_strategy_show_downloaded_file_description=إظهار عنصر التنزيل الموجود بالفعل، حتى تتمكن من الضغط على استئناف أو فتحه\nbatch_download_link_help=أدخل رابطًا يحتوي على رموز بديلة (استخدم *)\ninvalid_url=رابط غير صالح\nlist_is_too_large_maximum_n_items_allowed=القائمة كبيرة جدًا\\! الحد الأقصى المسموح به هو {{count}} عنصرًا\nenter_range=أدخل النطاق\nrange_from=من\nrange_to=إلى\nbatch_download_wildcard_length=طول الرمز البديل\nfirst_link=أول رابط\nlast_link=آخر رابط\nopen_source_software_used_in_this_app=البرمجيات المفتوحة المصدر المستخدمة في هذا التطبيق\nlinks=الروابط\nwebsite=الموقع الإلكتروني\ndevelopers=المطورون\nsource_code=النص البرمجي المصدري\nlicense=الترخيص\nno_license_found=لم يعثر على ترخيص\norganization=المنظمة\nadd_new_queue=إضافة قائمة انتظار جديدة\nqueue_name=اسم قائمة الانتظار\nqueues=قوائم الانتظار\nstop_queue=إيقاف قائمة الانتظار\nstart_queue=بدء قائمة الانتظار\nclear_queue_items=قائمة الانتظار فارغة\nconfig=التكوين\nitems=العناصر\nmove_down=تحريك للأسفل\nmove_up=تحريك للأعلى\nremove_queue=إزالة قائمة الانتظار\nqueue_name_help=حدد اسمًا لهذه القائمة\nqueue_name_describe=اسم قائمة الانتظار هو {{value}}\nqueue_max_concurrent_download=أقصى عدد تنزيلات متزامنة\nqueue_max_concurrent_download_description=الحد الأقصى للتنزيل لهذه القائمة\nqueue_automatic_stop=إيقاف تلقائي\nqueue_automatic_stop_description=إيقاف تلقائي عند عدم وجود عنصر في قائمة الانتظار\nqueue_scheduler=جدولة\nqueue_enable_scheduler=تفعيل المجدول\nqueue_active_days=الأيام النشطة\nqueue_active_days_description=ما هي الأيام التي تعمل فيها الجدولة؟\nqueue_scheduler_enable_auto_start_time=تفعيل وقت البدء التلقائي\nqueue_scheduler_auto_start_time=وقت البدء التلقائي\nqueue_scheduler_enable_auto_stop_time=تمكين وقت التوقف التلقائي\nqueue_scheduler_auto_stop_time=وقت الإيقاف التلقائي\nqueue_shutdown_on_completion=إيقاف تشغيل النظام عند الانتهاء\nqueue_shutdown_on_completion_description=إيقاف تشغيل النظام تلقائياً عند اكتمال قائمة الانتظار هذه أو عند الوصول إلى وقت الانتهاء المقرر.\nappearance=المظهر\ndownload_engine=محرك التنزيل\nbrowser_integration=التكامل مع المتصفح\nsettings_download_max_retries_count=الحد الأقصى لمحاولات التنزيل\nsettings_download_max_retries_count_description=الحد الأقصى لعدد المرات التي سيحاول فيها التطبيق إعادة محاولة التنزيل الفاشل قبل الاستسلام\nsettings_download_max_retries_count_describe_no_retries=لن تتم محاولة إعادة التنزيلات الفاشلة\nsettings_download_max_retries_count_describe_n_retries=سيتم إعادة تجربة التنزيلات الفاشلة {{count}} مرة(ات)\nsettings_download_thread_count=عدد الخيوط\nsettings_download_thread_count_description=الحد الأقصى لعدد الخيوط لكل عنصر تنزيل\nsettings_download_thread_count_describe=يمكن أن يحتوي التنزيل على ما يصل إلى {{count}} من الخيوط\nsettings_download_thread_count_with_large_value_describe=تحذير\\: قد يؤدي تعيين عدد كبير من الخيوط إلى زيادة استهلاك موارد النظام، تقليل الأداء، أو التسبب في مشاكل الاتصال مع الخوادم. استخدم القيم العالية فقط إذا كنت تفهم التأثير المحتمل على نظامك وشبكتك.\nsettings_use_server_last_modified_time=استخدام وقت التعديل الأخير للخادم\nsettings_use_server_last_modified_time_description=عند تنزيل ملف، استخدم آخر وقت تعديل للخادم للملف المحلي\nsettings_append_extension_to_incomplete_downloads=إضافة الامتداد إلى التنزيلات غير المكتملة\nsettings_append_extension_to_incomplete_downloads_description=أضف الامتداد \"part.\" إلى التنزيلات غير المكتملة. يساعد ذلك في التعرف على الملفات التي لم تكتمل عملية تنزيلها ويمنع فتحها عن طريق الخطأ.\nsettings_use_sparse_file_allocation=تخصيص الملفات المتفرقة\nsettings_use_sparse_file_allocation_description=يقوم بإنشاء الملفات بكفاءة أكبر، خاصة على الأقراص الصلبة (SSD)، عن طريق تقليل كتابة البيانات غير الضرورية. يمكن أن يسرع هذا من بدء التنزيلات ويوفر مساحة على القرص. إذا بدأت التنزيلات ببطء، ففكر في تعطيل هذا الخيار، حيث قد لا يكون مدعومًا بشكل صحيح على بعض الأجهزة.\nsettings_ignore_ssl_certificates=تجاهل شهادات SSL\nsettings_ignore_ssl_certificates_description=تعطيل التحقق من شهادة SSL. استخدمها فقط إذا لزم الأمر، لأنه قد يعرض اتصالك لمخاطر أمنية.\nsettings_global_speed_limiter=محدد السرعة العام\nsettings_global_speed_limiter_description=حد سرعة التنزيل العام (0 يعني غير محدود)\nsettings_show_average_speed=عرض متوسط السرعة\nsettings_show_average_speed_description=عرض سرعة التنزيل كمعدل أو كدقة\nsettings_use_category_by_default=استخدام الفئة بشكل افتراضي\nsettings_use_category_by_default_description=استخدام الفئة بشكل افتراضي عند إضافة تنزيل.\nsettings_default_download_folder=مجلد التنزيل الافتراضي\nsettings_default_download_folder_description=عند إضافة تنزيل جديد يتم استخدام هذا الموقع بشكل افتراضي\nsettings_default_download_folder_describe=سيتم استخدام \"{{folder}}\"\nsettings_use_proxy=استخدام بروكسي\nsettings_use_proxy_description=استخدام البروكسي لتنزيل الملفات\nsettings_use_proxy_describe_no_proxy=لن يتم استخدام أي بروكسي\nsettings_use_proxy_describe_system_proxy=سيتم استخدام بروكسي النظام\nsettings_use_proxy_describe_manual_proxy=سيتم استخدام \"{{value}}\"\nsettings_use_proxy_describe_pac_proxy=سيتم استخدام ملف pac التالي \\: {{value}}\nsettings_track_deleted_files_on_disk=تتبع الملفات المحذوفة على القرص\nsettings_track_deleted_files_on_disk_description=إزالة الملفات تلقائياً من القائمة عند حذفها أو نقلها من دليل التنزيل.\nsettings_delete_partial_file_on_download_cancellation=حذف الملف الجزئي عند إلغاء التنزيل\nsettings_delete_partial_file_on_download_cancellation_description=عند إلغاء التنزيل، سيتم حذف الملف الذي تم تنزيله جزئيًا من القرص. يساعد ذلك في الحفاظ على نظافة مجلد التنزيلات وتقليل استهلاك المساحة غير الضرورية على القرص. ومع ذلك، سيُعاد التنزيل من البداية في المرة القادمة التي تبدأ فيها به.\nsettings_default_user_agent=وكيل المستخدم الافتراضي\nsettings_default_user_agent_description=حدد سلسلة وكيل المستخدم الافتراضية لتحديد كيفية تعريف الطلبات للخوادم. يمكن أن يساعد ذلك في الوصول إلى المحتوى المحسن لأجهزة معينة أو في التحايل على قيود التنزيل التي تفرضها مواقع ويب معينة.\nsettings_download_size_unit=وحدة حجم التنزيل\nsettings_download_size_unit_description=الوحدة المستخدمة لعرض حجم التنزيل\nsettings_download_speed_unit=وحدة سرعة التنزيل\nsettings_download_speed_unit_description=الوحدة المستخدمة لعرض سرعة التنزيل\nsettings_theme=النسق\nsettings_theme_description=اختر نسق للتطبيق\nsettings_default_dark_theme=الوضع الداكن الافتراضي\nsettings_default_dark_theme_description=ينطبق عندما يتبع التطبيق سمة النظام ويكون الوضع الداكن فعال\nsettings_default_light_theme=الوضع النهاري الافتراضي\nsettings_default_light_theme_description=ينطبق عندما يتبع التطبيق سمة النظام ويكون الوضع النهاري فعال\nsettings_font=الخط\nsettings_font_description=تغيير الخط المستخدم في واجهة التطبيق، قد لا تظهر بعض الخطوط بشكل صحيح في التطبيق.\nsettings_ui_scale=مقياس واجهة المستخدم\nsettings_ui_scale_description=ضبط حجم عناصر واجهة التطبيق\nsettings_language=اللغة\nsettings_compact_top_bar=شريط علوي مدمج\nsettings_compact_top_bar_description=دمج الشريط العلوي مع شريط العنوان عندما يكون للنافذة الرئيسة عرض كافٍ\nsettings_use_native_menu_bar=استخدام شريط القائمة الأصلية\nsettings_use_native_menu_bar_description=استخدام نمط شريط القائمة الافتراضي للنظام\nsettings_use_relative_date_time=استخدام التاريخ/الوقت النسبي\nsettings_use_relative_date_time_description=استخدام تنسيق التاريخ/الوقت النسبي في التطبيق (مثل \"منذ 2 يوم\" بدلاً من التاريخ/الوقت الدقيق)\nsettings_show_icon_labels=إظهار تسميات الأيقونات\nsettings_show_icon_labels_description=إظهار التسميات تحت الأيقونات عندما يكون ذلك ممكنا (مثل إجراءات شريط الأدوات الرئيسي)\nsettings_use_system_tray=استخدام أيقونة النظام\nsettings_use_system_tray_description=إظهار أيقونة النظام عند تشغيل التطبيق\nsettings_start_on_boot=بدء التشغيل عند الإقلاع\nsettings_start_on_boot_description=تشغيل التطبيق تلقائيًا عند تسجيل دخول المستخدم\nsettings_notification_sound=صوت الإشعار\nsettings_notification_sound_description=تشغيل الصوت عند وجود إشعار جديد\nsettings_browser_integration=التكامل مع المتصفح\nsettings_browser_integration_description=قبول التنزيلات من المتصفحات\nsettings_browser_integration_server_port=منفذ الخادم\nsettings_browser_integration_server_port_description=منفذ التكامل مع المتصفح\nsettings_browser_integration_server_port_describe=سيستمع التطبيق على المنفذ {{port}}\nsettings_dynamic_part_creation=إنشاء أجزاء ديناميكية\nsettings_dynamic_part_creation_description=عند الانتهاء من جزء، قم بإنشاء جزء آخر عن طريق تقسيم الأجزاء الأخرى لتحسين سرعة التنزيل\nsettings_show_completion_dialog=إظهار مربع حوار اكتمال التنزيل\nsettings_show_completion_dialog_description=إظهار مربع حوار \"اكتمال التنزيل\" تلقائياً عند انتهاء التنزيل.\nsettings_show_download_progress_dialog=إظهار مربع الحوار \"تقدم التنزيل\"\nsettings_show_download_progress_dialog_description=إظهار مربع الحوار \"تقدم التنزيل\" تلقائيًا عند بدء التنزيل.\nsettings_per_host_settings=إعدادات لكل مضيف\nsettings_per_host_settings_descriptions=سيتم تطبيق هذه الإعدادات تلقائياً على أي تنزيل جديد يطابق المضيف المحدد.\nsettings_download_max_concurrent_downloads=الحد الأقصى للتنزيلات المتزامنة\nsettings_download_max_concurrent_downloads_description=الحد الأقصى لعدد الملفات التي يمكن تنزيلها في الوقت نفسه (لا يتم احتساب التنزيلات التي تتم إدارتها عبر قوائم الانتظار؛ اضبط القيمة على 0 لعدم وجود حد أقصى)\ndownload_item_settings_speed_limit=الحد الأقصى للسرعة\ndownload_item_settings_speed_limit_description=الحد من سرعة التنزيل لهذا العنصر\ndownload_item_settings_show_download_completion_dialog=إظهار مربع حوار اكتمال التنزيل\ndownload_item_settings_show_download_completion_dialog_description=إظهار مربع حوار \"اكتمال التنزيل\" تلقائياً عند الانتهاء من هذا التنزيل.\ndownload_item_settings_shutdown_on_completion=إيقاف تشغيل النظام عند الانتهاء\ndownload_item_settings_shutdown_on_completion_description=إيقاف تشغيل النظام تلقائياً عند الانتهاء من هذا التنزيل.\ndownload_item_settings_thread_count=عدد الخيوط\ndownload_item_settings_thread_count_description=ما عدد الخيوط المستخدمة لتنزيل عنصر التنزيل هذا (0 افتراضيًا)\ndownload_item_settings_thread_count_describe={{count}} خيوط لهذا التنزيل\ndownload_item_settings_username_description=قدّم إسم المستخدم إذا كان الرابط محميًا\ndownload_item_settings_password_description=قدّم كلمة المرور إذا كان الرابط محميًا\ndownload_item_settings_download_page=صفحة التنزيل\ndownload_item_settings_download_page_description=صفحة الويب التي بدأ فيها هذا التحميل\ndownload_item_settings_file_checksum=تحقق مجموع الملف\ndownload_item_settings_file_checksum_description=سلسلة تجزئة يمكن استخدامها للتحقق من تنزيل الملف بشكل صحيح\ndownload_item_settings_user_agent=وكيل المستخدم\ndownload_item_settings_user_agent_description=وكيل المستخدم المخصص لهذا العنصر (اتركه فارغاً لاستخدام الافتراضي)\nfile_checksum=تحقق مجموع الملف\nfile_checksum_page=مدقق مجموع الملف\nfile_checksum_page_file_checksum_default_algorithm=الخوارزمية الافتراضية\nfile_checksum_page_file_checksum_default_algorithm_help=الخوارزمية الافتراضية المستخدمة لحساب مجموع الملف عند عدم توفيرها.\nstart=إبدأ\ncalculated_checksum=مجموع الملف المحسوب\nsaved_checksum=مجموع الملف المحفوظ\nchecksum_algorithm=الخوارزمية\nfile_not_found=لم يتم العثور على الملف\ndownload_not_finished=لم ينتهي التنزيل\ndone=تم\nwaiting=قيد الانتظار\nmatches=مطابق\nnot_matches=غير مطابق\ncopy_to_clipboard=نسخ إلى الحافظة\nusername=اسم المستخدم\npassword=كلمة المرور\naverage_speed=متوسط السرعة\nexact_speed=السرعة الدقيقة\nunlimited=غير محدود\nuse_global_settings=استخدم الإعدادات العامة\ncant_run_browser_integration=لا يمكن تشغيل التكامل مع المتصفح\ncant_open_file=لا يمكن فتح الملف\ncant_open_folder=لا يمكن فتح المجلد\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} سنوات\nrelative_time_long_months={{months}} أشهر\nrelative_time_long_days={{days}} أيام\nrelative_time_long_hours={{hours}} ساعات\nrelative_time_long_minutes={{minutes}} دقائق\nrelative_time_long_seconds={{seconds}} ثواني\nrelative_time_short_years={{years}} س\nrelative_time_short_months={{months}} ش\nrelative_time_short_days={{days}} ي\nrelative_time_short_hours={{hours}} سا\nrelative_time_short_minutes={{minutes}} دق\nrelative_time_short_seconds={{seconds}} ثانية\nrelative_time_left={{time}} متبقي\nrelative_time_ago=منذ {{time}}\nauto=تلقائي\nunspecified=غير محدد\ncustom=مخصص\nicon=أيقونة\nauthor=الكاتب\nlink=الرابط\nsize=الحجم\nstatus=الحالة\nparts_info_downloaded_size=تم التنزيل\nparts_info_total_size=الإجمالي\nspeed=السرعة\ntime_left=الوقت المتبقي\ndate_added=تاريخ الإضافة\ninfo=المعلومات\ndownload_page_downloaded_size=تم التنزيل\ndownload_page_download_completed=اكتمل التنزيل\nresume_support=دعم الاستئناف\nyes=نعم\nno=لا\nparts_info=معلومات عن الأجزاء\ndisconnected=قطع الاتصال\nreceiving_data=تلقي البيانات\nconnecting=إرسال الحصول على البيانات\nwarning=تحذير\nunsupported_resume_warning=هذا التنزيل لا يدعم الإستئناف\\! قد تحتاج إلى إعادة تنزيل في وقت لاحق في قائمة التنزيل\nstop_anyway=إيقاف على أي حال\ncustomize_columns=تخصيص الأعمدة\nreset=إعادة الضبط\nmonday=الاثنين\ntuesday=الثلاثاء\nwednesday=الأربعاء\nthursday=الخميس\nfriday=الجمعة\nsaturday=السبت\nsunday=الأحد\nproxy_open_system_proxy_settings=فتح إعدادات بروكسي النظام\nproxy_type=نوع البروكسي\nproxy_do_not_use_proxy_for=عدم استخدام البروكسي لـ\nproxy_do_not_use_proxy_for_description=قائمة الروابط التي قد لا يتم توجيهها عبر البروكسي\\nيمكنك استخدام الرمز * كرمز بديل\\nعلى سبيل المثال\\: 192.168.1.* example.com (يتم فصلها بمسافة)\nproxy_change_title=تغيير البروكسي\nchange_proxy=تغيير البروكسي\nproxy_no=بدون بروكسي\nproxy_system=بروكسي النظام\nproxy_manual=بروكسي يدوي\nproxy_pac=تكوين البروكسي تلقائيًا\nproxy_pac_url=رابط التكوين التلقائي للبروكسي\naddress=العنوان\nport=المنفذ\naddress_and_port=العنوان و المنفذ\nuse_authentication=استخدم المصادقة\nwarning_you_may_have_to_restart_the_download_later=قد تحتاج إلى إعادة تشغيل التنزيل لاحقًا\\!\nedit_download_title=تعديل التنزيل\nedit_download_update_from_download_page=التحديث من صفحة التنزيل\nedit_download_update_from_download_page_description=عندما تكون هذه النافذة مفتوحة، يمكنك الانتقال إلى صفحة التنزيل والنقر على زر التنزيل. سيقوم التطبيق بالتقاط وتحديث بيانات الاعتماد الجديدة حتى تتمكن من حفظها.\nedit_download_saved_download_item_size_not_match=حجم العنصر المحفوظ {{currentSize}}، لا يتطابق مع حجم العنصر الجديد {{newSize}}.\ntranslators_page_thanks=كامل التقدير لمن ساعدوا في ترجمة المشروع ❤️\ntranslators=المترجمون\nlanguage=اللغة\ntranslators_contribute_title=لتحسين الترجمة\ntranslators_contribute_description=هل ترغب في المساعدة في تحسين هذا المشروع؟ إذا لم تكن لغتك مدرجة أو تحتاج إلى بعض التعديلات، يمكنك المساهمة لجعلها أفضل\\!\ncontribute=ساهم\nmeet_the_translators=التعرف على المترجمين\nlocalized_by_translators=تمت الترجمة بواسطة المترجمين\nconfirm_exit=تأكيد الخروج\nconfirm_exit_description=هل أنت متأكد من أنك تريد الخروج من مدير التنزيل AB؟\\nسيتم إيقاف التنزيلات/قوائم الانتظار النشطة\\!\nupdate=تحديث\nupdate_updater=المحدث\nupdate_available=التحديث متاح\nupdate_error=خطأ فى التحديث\nupdate_available_suggest_to_to_update=يمكنك التحديث إلى أحدث إصدار للاستمتاع بالميزات الجديدة والتحسينات وتحسين الأداء.\nupdate_release_notes=ملاحظات الإصدار\nupdate_check_for_update=التحقق من وجود تحديث\nupdate_checking_for_update=جاري التحقق من وجود تحديث\nupdate_no_update=أنت تستخدم أحدث إصدار\nupdate_check_error=حدث خطأ أثناء التحقق من وجود تحديث\nupdate_app_updated_to_version_n=تم تحديث التطبيق إلى الإصدار {{version}}\ncreate_desktop_entry=إنشاء اختصار على سطح المكتب\nshutdown_alert=تنبيه إيقاف التشغيل\nsystem_shutdown_soon=سيتم إيقاف تشغيل النظام قريبًا\\!\nsystem_shutdown_failed=فشل إيقاف تشغيل النظام\\!\nsystem_shutdown_soon_description=سيتم إيقاف تشغيل النظام قريبًا. إذا كنت لا تزال تستخدم الحاسوب، يُرجى حفظ عملك أو إلغاء عملية الإيقاف.\nsystem_shutdown_reason_queue_completed=اكتملت جميع التنزيلات في قائمة الانتظار.\nsystem_shutdown_reason_queue_end_time_reached=تم الوصول إلى وقت الانتهاء من التنزيل المجدول لقائمة انتظار.\nsystem_shutdown_download_finished=اكتمل التنزيل.\nshutdown_now=إيقاف التشغيل الآن\nsettings_per_host_settings_new_host=<مضيف جديد>\nsettings_per_host_settings_not_selected=قم بإنشاء أو تحديد عنصر جديد أولاً\\!\nsettings_per_host_settings_host=مضيف\nsettings_per_host_settings_host_description=سيتم تطبيق هذه الإعدادات على التنزيلات المطابقة لاسم المضيف. الحروف البديلة (*) مدعومة (على سبيل المثال\\: example.com, *.example.com - استخدم واحد فقط).\nsettings_browser_in_launcher=أيقونة المتصفح في المشغّل\nsettings_browser_in_launcher_description=إظهار أو إخفاء أيقونة المتصفح في المشغّل (قائمة التطبيقات).\nsort_by=ترتيب حسب\nwelcome=مرحبًا\nnew_folder=مجلد جديد\nskip=تخطي\nlets_go=لنبدأ\nnext=التالي\nselect_all=تحديد الكل\nselect_inside=تحديد المحتويات\nselect_invert=عكس التحديد\nopen_settings=فتح الإعدادات\nback=رجوع\nservice_is_running=الخدمة قيد التشغيل\ninitial_setup_description=لنقم بالإعداد\ninitial_setup_notice=يمكنك تغيير هذه الإعدادات لاحقًا في أي وقت\npermission_granted=تم منح الإذن\npermission_not_granted=لم يتم منح الإذن\npermissions=الأذونات\ngive_permission=السماح بالإذن\ngive_storage_permission=السماح بالوصول إلى التخزين\nstorage_roots=جذور التخزين\npermissions_initial_title=لنقم بالإعداد\npermissions_initial_description=لكي يعمل التطبيق بشكل صحيح، يحتاج إلى بعض الأذونات. في الشاشة التالية ستتعرف على سبب كل إذن ويمكنك اختيار السماح به أو تخطيه.\npermissions_done_title=كل شيء جاهز\npermissions_done_description=كل شيء جاهز. تم منح جميع الأذونات المطلوبة والتطبيق مستعد للاستخدام.\npermissions_manage_storage_title=إدارة الوصول إلى التخزين\npermissions_manage_storage_reason=يسمح هذا الإذن للتطبيق بتغيير مجلد التنزيل، واكتشاف التنزيلات المكررة بدقة أكبر، وتفعيل بعض الميزات الإضافية. الإذن اختياري، لكنه موصى به للحصول على أفضل تجربة.\npermission_read_write_external_storage_title=قراءة وكتابة التخزين\npermission_read_write_external_storage_reason=يسمح هذا الإذن للتطبيق بحفظ وإدارة الملفات التي تم تنزيلها، وتغيير موقع التنزيل، وتحسين اكتشاف التنزيلات المكررة.\npermissions_post_notification_title=الوصول إلى الإشعارات\npermissions_post_notification_reason=يحتاج التطبيق إلى العمل في الخلفية لإدارة التنزيلات. تُستخدم الإشعارات لإبقائك على اطلاع والسماح للتطبيق بالعمل في الخلفية.\npermissions_ignore_battery_optimization_title=تجاهل تحسين استهلاك البطارية\npermissions_ignore_battery_optimization_reason=تقوم بعض الأجهزة بتقييد نشاط التطبيقات في الخلفية بشكل صارم للحفاظ على البطارية، مما قد يؤدي إلى إيقاف التنزيلات مؤقتًا أو إيقافها عند عدم فتح التطبيق. يمكنك، بشكل اختياري، استثناء التطبيق من تحسين استهلاك البطارية لضمان استمرار التنزيلات دون انقطاع\nopen_in_browser=فتح في المتصفح\nbrowser=المتصفح\nbrowser_new_tab=تبويب جديد\nbrowser_close_tab=إغلاق التبويب\nbrowser_open_in_new_tab=فتح في علامة تبويب جديدة\nbrowser_open_in_new_background_tab=فتح في علامة تبويب جديدة في الخلفية\nbrowser_no_tab_open=لا توجد علامات تبويب مفتوحة\nbrowser_tabs=علامات التبويب\nbrowser_paste_and_go=لصق والانتقال\nbrowser_bookmarks=المفضلة\nbrowser_add_bookmark=أضف إلى المفضلة\nbrowser_edit_bookmark=تحرير المفضلة\nbrowser_add_to_bookmarks=أضف إلى المفضلة\nbrowser_remove_from_bookmarks=إزالة من المفضلة\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/bn_BD.properties",
    "content": "app_title=এবি ডাউনলোড ম্যানেজার\nconfirm_auto_categorize_downloads_title=স্বয়ংক্রিয় ডাউনলোড শ্রেণীবদ্ধ\nconfirm_auto_categorize_downloads_description=কোনো অশ্রেণীভুক্ত আইটেম স্বয়ংক্রিয়ভাবে তার সম্পর্কিত বিভাগে যোগ হবেl\nconfirm_reset_to_default_categories_title=ডিফল্ট বিভাগগুলিতে রিসেট করুন\nconfirm_reset_to_default_categories_description=এটি সমস্ত বিভাগ মুছে ফেলবে এবং ডিফল্ট বিভাগগুলি ফিরিয়ে আনবে\\!\nconfirm_delete_download_items_title=মুছে ফেলা নিশ্চিত করুন\nconfirm_delete_download_items_description=আপনি কি {{count}} টি আইটেম মুছতে চান?\nconfirm_delete_download_unfinished_items_description=আপনি কি {{count}} টি অসমাপ্ত ডাউনলোড মুছতে চান?\nconfirm_delete_download_finished_and_unfinished_items_description=আপনি কি {{finished Count}} টি সমাপ্ত এবং {{unfinished Count}} টি অসমাপ্ত ডাউনলোড মুছতে চান?\nalso_delete_file_from_disk=এছাড়াও ডিস্ক থেকে ফাইল মুছে ফেলুন\nconfirm_delete_category_item_title={{name}} বিভাগ সরানো হচ্ছে\nconfirm_delete_category_item_description=আপনি কি \"{{value}}\" বিভাগটি মুছতে চান?\nyour_download_will_not_be_deleted=আপনার ডাউনলোড মুছে ফেলা হবে না\ndrag_the_file_to_another_app=ফাইলটি অন্য অ্যাপে টেনে আনুন\ndrop_link_or_file_here=লিঙ্ক বা ফাইল এখানে ড্রপ করুন\nnothing_will_be_imported=কোনো কিছুই ইম্পোর্ট করা হবে না\nn_links_will_be_imported={{count}} লিংক ইম্পোর্ট করা হবে\nn_items_selected={{count}} আইটেম নির্বাচিত\nwindow_close=বন্ধ করুন\nwindow_minimize=মিনিমাইজ করুন\nwindow_maximize=সর্বাধিক করুন\nwindow_restore=পুনরুদ্ধার করুন\ndelete=মুছে ফেলুন\nremove=অপসারণ করুন\ncancel=বাতিল করুন\nclose=বন্ধ করুন\nmenu=সূচি\nmore_options=আরও বিকল্প\nok=ঠিক আছে\nadd=যোগ করুন\npaste=পেস্ট করুন\nchange=পরিবর্তন করুন\nedit=সম্পাদন করুন\nchange_anyway=যেভাবেই হোক পরিবর্তন করুন\ndownload=ডাউনলোড করুন\nrefresh=রিফ্রেশ করুন\nsettings=সেটিংস সমূহ\non_completion=সমাপ্তির উপর\nunknown=অজানা\nunknown_error=অজানা ত্রুটি\ndownload_item_not_found=ডাউনলোড আইটেম পাওয়া যায়নি\nname=ফাইলের নাম\ndownload_link=ডাউনলোড লিঙ্ক লিখুন /পেস্ট করুন...\nnot_finished=সমাপ্ত হয়নি\nall=সব\nfinished=সম্পন্ন হয়েছে।\nUnfinished=অসমাপ্ত।\ncanceled=বাতিল করা হয়েছে\nerror=ত্রুটি\npaused=বিরতি দেওয়া হয়েছে\ndownloading=ডাউনলোড হচ্ছে\nadded=যুক্ত হয়েছে\nidle=আই.ডি.এল.ই\npreparing_file=ফাইল প্রস্তুত হচ্ছে\ncreating_file=ফাইল তৈরি হচ্ছে\nresuming=আবার শুরু হচ্ছে\nretrying=পুনরায় চেষ্টা করা হচ্ছে\nlist_is_empty=তালিকাটি ফাঁকা\\!\nsearch_in_the_list=তালিকায় খুঁজুন\nsearch=খুঁজুন\nclear=পরিষ্কার করুন\ngeneral=সাধারণ\nenabled=সক্রিয় হয়েছে।\ndisabled=অক্ষম রয়েছে।\ndefault=পূর্ব-নির্ধারিত\nfile=ফাইল\ntasks=কার্যাবলী\ntools=টুলস\nhelp=সাহায্য\nsystem=সিস্টেম\nall_missing_files=সব অনুপস্থিত ফাইল\nall_finished=সম্পন্ন সব\nall_unfinished=অসম্পূর্ণ সব\nentire_list=সম্পূর্ণ তালিকা\ndownload_browser_integration=ব্রাউজার ইন্টিগ্রেশন ডাউনলোড করুন\nexit=প্রস্থান করুন\nshow_downloads=ডাউনলোডগুলি দেখান\nnew_download=নতুন ডাউনলোড\nstop_all=সব বন্ধ করুন\nimport_from_clipboard=ক্লিপবোর্ড থেকে ইমপোর্ট করুন\nbatch_download=ব্যাচ ডাউনলোড করুন\nopen=খুলুন\nshare=শেয়ার করুন\nopen_file=ফাইল খুলুন\nopen_folder=ফোল্ডার খুলুন\nresume=পুনরায় শুরু করুন\npause=বিরতি\nrestart_download=ডাউনলোড পুনরায় চালু করুন\ncopy=অনুলিপি করুন\ncopy_link=লিঙ্ক কপি করুন\ncopy_as_curl=CURL হিসেবে কপি করুন\nshow_properties=বৈশিষ্ট্য প্রদর্শন করুন\nmove_to_queue=সারিতে সরান\nmove_to_this_queue=এই সারিতে সরান\nmove_to_category=শ্রেণীতে /ক্যাটাগরিতে সরান\nmove_to_this_category=এই শ্রেণীতে সরান\ncategories=শ্রেণীসমূহ\nadd_category=শ্রেণী /ক্যাটাগরি যুক্ত করুন\nedit_category=শ্রেণী /ক্যাটাগরি সম্পাদন করুন\ndelete_category=শ্রেণী /ক্যাটাগরি মুছুন\ncategory_name=শ্রেণী /ক্যাটাগরি নাম\ncategory_download_location=শ্রেণী /ক্যাটাগরি ডাউনলোড স্থান \ncategory_download_location_description=\"ডাউনলোড যোগ করুন\"-এ এই ক্যাটাগরিটি বেছে নেওয়া হলে এই ডিরেক্টরিটিকে \"ডাউনলোড স্থান\" হিসেবে ব্যবহার করুন\ncategory_file_types=শ্রেণী /ক্যাটাগরি ফাইলের ধরন\ncategory_file_types_description=স্বয়ংক্রিয়ভাবে এই বিভাগে এই ধরনের ফাইল রাখুন. (যখন আপনি নতুন ডাউনলোড যোগ করেন)\\nস্থান সহ পৃথক ফাইল এক্সটেনশন (ext1, ext2 ...)\ncategory_url_patterns=ইউআরএল প্যাটার্নস\ncategory_url_patterns_description=স্বয়ংক্রিয়ভাবে এই URL গুলি থেকে এই বিভাগে ডাউনলোড করুন৷ (যখন আপনি নতুন ডাউনলোড যোগ করেন)\\nস্পেস সহ আলাদা ইউআরএল, আপনি ওয়াইল্ডকার্ডের জন্য *ও ব্যবহার করতে পারেন\nauto_categorize_downloads=স্বয়ংক্রিয় শ্রেণীভুক্ত ডাউনলোড\nrestore_defaults=ডিফল্টকে পুনঃস্থাপন করুন\nabout=সম্পর্কে\nversion_n=সংস্করণ /ভার্সন {{value}}\ndeveloped_with_love_for_you=আপনার জন্য ❤️ (হৃদয়) দিয়ে ডেভেলপ করা হয়েছে।\ndonate=ডোনেট করুন\nvisit_the_project_website=প্রকল্পের ওয়েবসাইট দেখুন\nthis_is_a_free_and_open_source_software=এটি একটি বিনামূল্যের ও ওপেন সোর্স সফটওয়্যার\nview_the_source_code=সোর্স কোড দেখুন\nthird_party_libraries=তৃতীয় পক্ষের লাইব্রেরি\npowered_by_open_source_software=ওপেন সোর্স সফটওয়্যার দ্বারা চালিত\nview_the_open_source_licenses=ওপেন সোর্স লাইসেন্স দেখুন\nsupport_and_community=সমর্থন এবং কমিউনিটি\ntelegram=টেলিগ্রাম\nchannel=চ্যানেল\ngroup=গ্রুপ\nadd_download=ডাউনলোড যুক্ত করুন\nadd_multi_download_page_header=আপনি যে আইটেমগুলি ডাউনলোড করতে চান তা নির্বাচন করুন৷\nsave_to=এতে সংরক্ষণ করুন\nwhere_should_each_item_saved=প্রতিটি আইটেম কোথায় সংরক্ষণ করা উচিত?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=একাধিক আইটেম আছে\\! আপনি তাদের সংরক্ষণ করতে চান একটি উপায় নির্বাচন করুন\neach_item_on_its_own_category=প্রতিটি আইটেম নিজস্ব শ্রেণীতে\neach_item_on_its_own_category_description=প্রতিটি আইটেম একটি শ্রেণীতে স্থাপন করা হবে যে ফাইল টাইপ আছে\nall_items_in_one_category=এক বিভাগে সব আইটেম\nall_items_in_one_category_description=সমস্ত ফাইল নির্বাচিত ক্যাটাগরি স্থানে সংরক্ষণ করা হবে\nall_items_in_one_Location=সমস্ত আইটেম এক স্থানে\nall_items_in_one_Location_description=সমস্ত আইটেম নির্বাচিত ডিরেক্টরিতে সংরক্ষিত হবে\nunselected_all_items_in_specific_location_description=সমস্ত ফাইল নির্বাচিত ক্যাটাগরি স্থানে সংরক্ষণ করা হবে\nno_category_selected=কোন ক্যাটাগরি নির্বাচন করা হয়নি\nno_categories_found=কোন বিভাগ পাওয়া যায়নি\ndownload_location=ডাউনলোড এর স্থান\nlocation=অবস্থান বা স্থান\nselect_queue=সারি নির্বাচন করুন\nwithout_queue=সারি ছাড়া\nuse_category=শ্রেণী /ক্যাটাগরি ব্যবহার করুন\ncant_write_to_this_folder=এই ফোল্ডার কিছু রাখা যাচ্ছে না\nfile_name_already_exists=ফাইলের নাম ইতিমধ্যেই বিদ্যমান৷\ndownload_already_exists=ডাউনলোডটি ইতিমধ্যে রয়েছে\ninvalid_file_name=ফাইলের নামটি বৈধ নয়\nshow_solutions=সমাধান দেখান...\nchange_solution=সমাধান পরিবর্তন করুন\nselect_a_solution=একটি সমাধান নির্বাচন করুন\nselect_download_strategy_description=আপনার দেওয়া লিঙ্কটি ইতিমধ্যেই ডাউনলোড তালিকায় রয়েছে, আপনি কি করতে চান তা উল্লেখ করুন\ndownload_strategy_add_a_numbered_file=একটি সংখ্যাযুক্ত ফাইল যোগ করুন\ndownload_strategy_add_a_numbered_file_description=ডাউনলোড ফাইলের নাম শেষে একটি সূচী যোগ করুন\ndownload_strategy_override_existing_file=বিদ্যমান ফাইল ওভাররাইড করুন\ndownload_strategy_override_existing_file_description=বিদ্যমান ডাউনলোড সরান এবং সেই ফাইলটিতে লিখুন\ndownload_strategy_update_download_link=বিদ্যমান আপডেট ডাউনলোড করুন\ndownload_strategy_update_download_link_description=বিদ্যমান ডাউনলোড লিঙ্ক এবং এর প্রমাণপত্রাদি আপডেট করুন\ndownload_strategy_show_downloaded_file=ডাউনলোড করা ফাইল দেখান\ndownload_strategy_show_downloaded_file_description=ইতিমধ্যে বিদ্যমান ডাউনলোড আইটেম দেখান, যাতে আপনি পুনরারম্ভ করা টিপুন বা এটি খুলুন\nbatch_download_link_help=ওয়াইল্ডকার্ড রয়েছে এমন একটি লিঙ্ক লিখুন (* ব্যবহার করুন)\ninvalid_url=ইউ আর এল বৈধ নয়\nlist_is_too_large_maximum_n_items_allowed=তালিকাটা অনেক বড়\\! সর্বাধিক {{count}} টি আইটেম অনুমোদিত৷\nenter_range=পরিসীমা লিখুন\nrange_from=থেকে\nrange_to=প্রতি\nbatch_download_wildcard_length=ওয়াইল্ডকার্ডের দৈর্ঘ্য\nfirst_link=প্রথম লিঙ্ক\nlast_link=শেষ লিঙ্ক\nopen_source_software_used_in_this_app=এই অ্যাপে ওপেন সোর্স সফটওয়্যার ব্যবহার করা হয়েছে\nlinks=লিংকস\nwebsite=ওয়েবসাইট\ndevelopers=ডেভেলপার'স\nsource_code=সোর্স কোড\nlicense=লাইসেন্স/ অনুমতি পত্র\nno_license_found=কোনো লাইসেন্স পাওয়া যায়নি\norganization=সংস্থা\nadd_new_queue=সারিতে নতুন যোগ করুন\nqueue_name=সারির নাম\nqueues=সারিগুলো\nstop_queue=সারি থামান\nstart_queue=সারি শুরু করুন\nclear_queue_items=খালি সারি\nconfig=কনফিগ\nitems=আইটেম'স\nmove_down=নিচে নামান\nmove_up=উপরে তুলুন\nremove_queue=সারি অপসারণ করুন\nqueue_name_help=এই সারির জন্য একটি নাম উল্লেখ করুন\nqueue_name_describe=সারির নাম হল {{value}}\nqueue_max_concurrent_download=সর্বোচ্চ সমসাময়িক ডাউনলোড\nqueue_max_concurrent_download_description=এই সারির জন্য সর্বোচ্চ ডাউনলোড হচ্ছে\nqueue_automatic_stop=স্বয়ংক্রিয়ভাবে বন্ধ\nqueue_automatic_stop_description=কোন আইটেম না থাকলে সারি স্বয়ংক্রিয়ভাবে বন্ধ হবে\nqueue_scheduler=সময়সূচী\nqueue_enable_scheduler=সময়সূচী সক্রিয় করুন\nqueue_active_days=সক্রিয় দিনগুলি\nqueue_active_days_description=কোন দিন নির্ধারণকারী কাজ করে?\nqueue_scheduler_enable_auto_start_time=স্বয়ংক্রিয়ভাবে শুরুর সময় সক্রিয় করুন\nqueue_scheduler_auto_start_time=স্বয়ংক্রিয় শুরুর সময়\nqueue_scheduler_enable_auto_stop_time=স্বয়ংক্রিয়ভাবে বন্ধর সময় সক্রিয় করুন\nqueue_scheduler_auto_stop_time=স্বয়ংক্রিয়ভাবে থামার সময়\nqueue_shutdown_on_completion=সম্পূর্ণ হলে সিস্টেম বন্ধ করুন\nqueue_shutdown_on_completion_description=এই সারিটি সম্পন্ন হলে, সিস্টেমটি স্বয়ংক্রিয়ভাবে বন্ধ হয়ে যাবে অথবা নির্ধারিত শেষ সময় শেষ হলে।\nappearance=এপিয়ারেন্স / বাহ্যিক রুপ\ndownload_engine=ডাউনলোড ইঞ্জিন\nbrowser_integration=ব্রাউজার ইন্টিগ্রেশন\nsettings_download_max_retries_count=সর্বাধিক ডাউনলোড পুনঃপ্রচেষ্টা\nsettings_download_max_retries_count_description=অ্যাপটি ডাউনলোড ব্যর্থ হওয়ার পর হাল ছেড়ে দেওয়ার সর্বোচ্চ কতবার পুনরায় চেষ্টা করবে\nsettings_download_max_retries_count_describe_no_retries=ব্যর্থ ডাউনলোডগুলি পুনরায় চেষ্টা করা হবে না\nsettings_download_max_retries_count_describe_n_retries=ব্যর্থ ডাউনলোডগুলি {{count}} বার পুনরায় চেষ্টা করা হবে\nsettings_download_thread_count=থ্রেড গণনা\nsettings_download_thread_count_description=আইটেম প্রতি সর্বোচ্চ ডাউনলোড থ্রেড\nsettings_download_thread_count_describe=একটি ডাউনলোডে {{count}} টি পর্যন্ত থ্রেড থাকতে পারে৷\nsettings_download_thread_count_with_large_value_describe=সতর্কবার্তা\\: উচ্চ থ্রেড সংখ্যা নির্ধারণ করলে সিস্টেমের রিসোর্স ব্যবহার বৃদ্ধি পেতে পারে, কর্মক্ষমতা হ্রাস পেতে পারে, অথবা সার্ভারগুলোর সাথে সংযোগ সমস্যার সৃষ্টি হতে পারে। কেবল তখনই উচ্চ মান নির্ধারণ করুন যখন আপনি আপনার সিস্টেম এবং নেটওয়ার্কে এর সম্ভাব্য প্রভাব সম্পর্কে সচেতন থাকেন।\nsettings_use_server_last_modified_time=সার্ভারের শেষ-সংশোধিত সময় ব্যবহার করুন\nsettings_use_server_last_modified_time_description=একটি ফাইল ডাউনলোড করার সময়, স্থানীয় ফাইলের জন্য সার্ভারের সর্বশেষ পরিবর্তিত সময় ব্যবহার করুন\nsettings_append_extension_to_incomplete_downloads=অসম্পূর্ণ ডাউনলোডগুলিতে এক্সটেনশন যুক্ত করুন\nsettings_append_extension_to_incomplete_downloads_description=অসম্পূর্ণ ডাউনলোডগুলিতে \".part\" এক্সটেনশন যুক্ত করুন। এটি অসম্পূর্ণ ডাউনলোডগুলি সনাক্ত করতে সাহায্য করে এবং অসম্পূর্ণ ফাইলগুলি দুর্ঘটনাক্রমে খোলা রোধ করে।\nsettings_use_sparse_file_allocation=অল্পপবিমাণে বিক্ষিপ্ত ফাইল বরাদ্দ\nsettings_use_sparse_file_allocation_description=অপ্রয়োজনীয় ডেটা রাইটিং কমিয়ে বিশেষ করে SSD-তে আরও দক্ষতার সাথে ফাইল তৈরি করুন। এটি ডাউনলোড শুরুর গতি বাড়াতে পারে এবং ডিস্কের ব্যবহার কমাতে পারে। যদি ডাউনলোডগুলি ধীরে শুরু হয়, বা আপনি অস্বাভাবিক ডাউনলোডের গতি অনুভব করেন, তাহলে এই বিকল্পটি নিষ্ক্রিয় করার কথা বিবেচনা করুন, কারণ এটি কিছু ডিভাইসে সম্পূর্ণরূপে সমর্থিত নাও হতে পারে৷\nsettings_ignore_ssl_certificates=SSL সার্টিফিকেট উপেক্ষা করুন\nsettings_ignore_ssl_certificates_description=SSL সার্টিফিকেট যাচাইকরণ অক্ষম করুন। শুধুমাত্র প্রয়োজন হলেই ব্যবহার করুন, কারণ এটি আপনার সংযোগকে নিরাপত্তা ঝুঁকির মুখে ফেলতে পারে।\nsettings_global_speed_limiter=গ্লোবাল স্পীড লিমিটার\nsettings_global_speed_limiter_description=বিশ্বব্যাপী ডাউনলোড গতি সীমা (0 মানে সীমাহীন)\nsettings_show_average_speed=গড় গতি দেখান\nsettings_show_average_speed_description=গড় বা নির্ভুলতা ডাউনলোড গতি\nsettings_use_category_by_default=ডিফল্টরূপে বিভাগ ব্যবহার করুন\nsettings_use_category_by_default_description=ডাউনলোড যোগ করার সময় ডিফল্টরূপে বিভাগ ব্যবহার করুন।\nsettings_default_download_folder=ডিফল্ট ডাউনলোড ফোল্ডার হিসেবে\nsettings_default_download_folder_description=আপনি যখন একটি \"নতুন ডাউনলোড\" যোগ করেন, তখন এই অবস্থানটি ডিফল্টরূপে ব্যবহৃত হয়\nsettings_default_download_folder_describe=\"{{folder}}\" ব্যবহৃত হচ্ছে।\nsettings_use_proxy=প্রক্সি ব্যবহার করুন\nsettings_use_proxy_description=ফাইল ডাউনলোড করার জন্য প্রক্সি ব্যবহার করুন\nsettings_use_proxy_describe_no_proxy=কোনো প্রক্সি ব্যবহার ব্যবহৃত হচ্ছে না\nsettings_use_proxy_describe_system_proxy=সিস্টেম প্রক্সি ব্যবহার করা হবে৷\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" ব্যবহৃত হচ্ছে।\nsettings_use_proxy_describe_pac_proxy=pac ফাইল \"{{value}}\" ব্যবহার করা হবে\nsettings_track_deleted_files_on_disk=ডিস্কে মুছে ফেলা ফাইল ট্র্যাক করুন\nsettings_track_deleted_files_on_disk_description=ডাউনলোড ডিরেক্টরি থেকে ফাইলগুলি মুছে ফেলা বা সরানো হলে তালিকা থেকে স্বয়ংক্রিয়ভাবে মুছে ফেলুন।\nsettings_delete_partial_file_on_download_cancellation=ডাউনলোড বাতিল করার সময় আংশিক ফাইল মুছে ফেলুন\nsettings_delete_partial_file_on_download_cancellation_description=যখন কোনও ডাউনলোড বাতিল করা হয়, তখন আংশিকভাবে ডাউনলোড করা ফাইলটি ডিস্ক থেকে মুছে ফেলা হবে। এটি আপনার ডাউনলোড ফোল্ডারটি পরিষ্কার রাখতে সাহায্য করে এবং অপ্রয়োজনীয় ডিস্ক স্থান ব্যবহার হ্রাস করে। তবে, পরের বার যখন আপনি ডাউনলোড শুরু করবেন তখন ডাউনলোডটি শুরু থেকেই পুনরায় চালু হবে।\nsettings_default_user_agent=ডিফল্ট ব্যবহারকারী এজেন্ট\nsettings_default_user_agent_description=সার্ভারে অনুরোধগুলি কীভাবে শনাক্ত করা হয় তা নির্ধারণ করতে ডিফল্ট ব্যবহারকারী এজেন্ট স্ট্রিং নির্দিষ্ট করুন। এটি নির্দিষ্ট ডিভাইসের জন্য অপ্টিমাইজ করা সামগ্রী অ্যাক্সেস করতে বা নির্দিষ্ট ওয়েবসাইট দ্বারা আরোপিত ডাউনলোড সীমাবদ্ধতা এড়াতে সহায়তা করতে পারে।\nsettings_download_size_unit=ডাউনলোড করুন আকার ইউনিট\nsettings_download_size_unit_description=ডাউনলোডের আকার প্রদর্শনের জন্য ব্যবহৃত ইউনিট\nsettings_download_speed_unit=ডাউনলোড স্পিড ইউনিট\nsettings_download_speed_unit_description=ডাউনলোডের গতি প্রদর্শনের জন্য ব্যবহৃত ইউনিট\nsettings_theme=অ্যাপ্লিকেশন থিম\nsettings_theme_description=অ্যাপের জন্য একটি থিম নির্বাচন করুন\nsettings_default_dark_theme=ডিফল্ট ডার্ক থিম\nsettings_default_dark_theme_description=অ্যাপটি সিস্টেম থিম অনুসরণ করলে এবং ডার্ক মোড সক্রিয় থাকলে প্রযোজ্য হয়\nsettings_default_light_theme=ডিফল্ট সাদা থিম\nsettings_default_light_theme_description=অ্যাপটি সিস্টেম থিম অনুসরণ করলে এবং লাইট মোড সক্রিয় থাকলে প্রযোজ্য হয়\nsettings_font=হরফ\nsettings_font_description=অ্যাপ ইন্টারফেসে ব্যবহৃত ফন্ট পরিবর্তন করুন, কিছু ফন্ট অ্যাপে সঠিকভাবে প্রদর্শিত নাও হতে পারে\nsettings_ui_scale=ইউজার ইন্টারফেস স্কেল\nsettings_ui_scale_description=অ্যাপের ইন্টারফেস উপাদানের আকার সামঞ্জস্য করুন\nsettings_language=ভাষা\nsettings_compact_top_bar=কমপ্যাক্ট শীর্ষ বার\nsettings_compact_top_bar_description=প্রধান উইন্ডোর যথেষ্ট প্রস্থ থাকলে শীর্ষ বারটি শিরোনাম বারের সাথে মার্জ করুন\nsettings_use_native_menu_bar=নেটিভ মেনু বার ব্যবহার করুন\nsettings_use_native_menu_bar_description=সিস্টেমের ডিফল্ট মেনু বার স্টাইল ব্যবহার করুন\nsettings_use_relative_date_time=আপেক্ষিক তারিখ/সময় ব্যবহার করুন\nsettings_use_relative_date_time_description=অ্যাপে তারিখের জন্য আপেক্ষিক তারিখ/সময় বিন্যাস ব্যবহার করুন (যেমন, \"২ দিন আগে\" যথাযথ তারিখ/সময়ের পরিবর্তে)\nsettings_show_icon_labels=আইকন লেবেল দেখান\nsettings_show_icon_labels_description=সম্ভব হলে আইকনের নিচে লেবেল দেখান (যেমন হোম টুলবার অ্যাকশন)\nsettings_use_system_tray=সিস্টেম ট্রে ব্যবহার করুন\nsettings_use_system_tray_description=অ্যাপটি চলাকালীন সিস্টেম ট্রে আইকনটি দেখান\nsettings_start_on_boot=\"Boot\" এ স্বয়ংক্রিয়ভাবে শুরু করুন\nsettings_start_on_boot_description=ব্যবহারকারী লগইনে স্বয়ংক্রিয়ভাবে অ্যাপ্লিকেশন শুরু করুন\nsettings_notification_sound=বিজ্ঞপ্তির আওয়াজ /শব্দ\nsettings_notification_sound_description=নতুন বিজ্ঞপ্তিতে আওয়াজ /শব্দ চালান\nsettings_browser_integration=ব্রাউজার ইন্টিগ্রেশন\nsettings_browser_integration_description=ব্রাউজার থেকে ডাউনলোড গ্রহণ করুন\nsettings_browser_integration_server_port=সার্ভার পোর্ট\nsettings_browser_integration_server_port_description=ব্রাউজার ইন্টিগ্রেশন জন্য পোর্ট\nsettings_browser_integration_server_port_describe=অ্যাপ {{port}} পোর্ট ব্যবহৃত হচ্ছে।\nsettings_dynamic_part_creation=ডাইনামিক অংশ উদ্ভাবন\nsettings_dynamic_part_creation_description=একটি অংশ শেষ হয়ে গেলে, ডাউনলোডের গতি উন্নত করতে অন্য অংশগুলিকে বিভক্ত করে আরেকটি অংশ তৈরি করুন\nsettings_show_completion_dialog=সমাপ্ত ডাউনলোড ডায়ালগ দেখান\nsettings_show_completion_dialog_description=একটি ডাউনলোড শেষ হলে স্বয়ংক্রিয়ভাবে \"ডাউনলোড সমাপ্ত\" ডায়ালগ দেখান৷\nsettings_show_download_progress_dialog=ডাউনলোড অগ্রগতি ডায়ালগ দেখান\nsettings_show_download_progress_dialog_description=একটি ডাউনলোড শুরু হলে স্বয়ংক্রিয়ভাবে \"ডাউনলোড অগ্রগতি\" ডায়ালগ দেখান৷\nsettings_per_host_settings=প্রতি হোস্ট সেটিংস\nsettings_per_host_settings_descriptions=এই সেটিংসগুলি নির্দিষ্ট হোস্টের সাথে মেলে এমন যেকোনো নতুন ডাউনলোডের ক্ষেত্রে স্বয়ংক্রিয়ভাবে প্রয়োগ করা হবে।\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=গতিসীমা\ndownload_item_settings_speed_limit_description=এই আইটেমটির জন্য ডাউনলোডের গতি সীমিত করুন\ndownload_item_settings_show_download_completion_dialog=সমাপ্ত ডাউনলোড ডায়ালগ দেখান\ndownload_item_settings_show_download_completion_dialog_description=একটি ডাউনলোড শেষ হলে স্বয়ংক্রিয়ভাবে \"ডাউনলোড সমাপ্ত\" ডায়ালগ দেখান৷\ndownload_item_settings_shutdown_on_completion=সম্পূর্ণ হলে সিস্টেম বন্ধ করুন\ndownload_item_settings_shutdown_on_completion_description=এই ডাউনলোড শেষ হলে সিস্টেমটি স্বয়ংক্রিয়ভাবে বন্ধ হয়ে যাবে।\ndownload_item_settings_thread_count=থ্রেড গণনা\ndownload_item_settings_thread_count_description=এই ডাউনলোড আইটেমটি ডাউনলোড করতে কত থ্রেড ব্যবহার করা হয়েছে (ডিফল্টের জন্য 0)\ndownload_item_settings_thread_count_describe=এই ডাউনলোডের জন্য {{count}} থ্রেড\ndownload_item_settings_username_description=লিঙ্কটি একটি সুরক্ষিত সম্পদ হলে একটি \"ব্যবহারকারীর নাম\" প্রদান করুন\ndownload_item_settings_password_description=লিঙ্কটি সুরক্ষিত সম্পদ হলে একটি \"পাসওয়ার্ড\" প্রদান করুন৷\ndownload_item_settings_download_page=ডাউনলোড পেজ\ndownload_item_settings_download_page_description=যে ওয়েবপৃষ্ঠাটি এই ডাউনলোড শুরু করা হয়েছিল\ndownload_item_settings_file_checksum=ফাইল চেকসাম\ndownload_item_settings_file_checksum_description=একটি হ্যাশ স্ট্রিং যা ফাইলটি সঠিকভাবে ডাউনলোড হয়েছে কিনা তা পরীক্ষা করতে ব্যবহার করা যেতে পারে।\ndownload_item_settings_user_agent=ব্যবহারকারী-দূত\ndownload_item_settings_user_agent_description=এই আইটেমের জন্য কাস্টম ব্যবহারকারী-এজেন্ট (ডিফল্ট ব্যবহার করতে খালি রাখুন)\nfile_checksum=ফাইল চেকসাম\nfile_checksum_page=ফাইল চেকসাম পরীক্ষক\nfile_checksum_page_file_checksum_default_algorithm=ডিফল্ট অ্যালগরিদম\nfile_checksum_page_file_checksum_default_algorithm_help=ফাইল চেকসাম প্রদান না করা হলে তা গণনা করার জন্য ব্যবহৃত ডিফল্ট অ্যালগরিদম।\nstart=শুরু করুন\ncalculated_checksum=গণনাকৃত চেকসাম\nsaved_checksum=সংরক্ষিত চেকসাম\nchecksum_algorithm=অ্যালগরিদম\nfile_not_found=ফাইলটি পাওয়া যায়নি।\ndownload_not_finished=ডাউনলোড শেষ হয়নি।\ndone=সম্পন্ন হয়েছে\nwaiting=প্রতীক্ষারত\nmatches=মিলেছে\nnot_matches=মেলেনি\ncopy_to_clipboard=ক্লিপবোর্ডে কপি করুন\nusername=ইউজারনেম\npassword=পাসওয়ার্ড\naverage_speed=গড় গতি\nexact_speed=নির্ভুল /সঠিক গতি\nunlimited=সীমাহীন\nuse_global_settings=গ্লোবাল /বিশ্বব্যাপী সেটিংস ব্যবহার করুন\ncant_run_browser_integration=ব্রাউজার ইন্টিগ্রেশন চালানো যাচ্ছে না\ncant_open_file=ফাইল খোলা যাচ্ছে না\ncant_open_folder=ফোল্ডার খোলা যাচ্ছে না\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} বছর\nrelative_time_long_months={{months}} মাস\nrelative_time_long_days={{days}} দিন\nrelative_time_long_hours={{hours}} ঘন্টা\nrelative_time_long_minutes={{minutes}} মিনিট''স\nrelative_time_long_seconds={{seconds}} সেকেন্ড''স\nrelative_time_short_years={{years}} বছর\nrelative_time_short_months={{months}} মাস\nrelative_time_short_days={{days}} দিন\nrelative_time_short_hours={{hours}} ঘন্টা\nrelative_time_short_minutes={{minutes}} মিনিট\nrelative_time_short_seconds={{seconds}} সেকেন্ড\nrelative_time_left={{time}} বাকি\nrelative_time_ago={{time}} আগে\nauto=স্বয়ংক্রিয়\nunspecified=অনির্ধারিত\ncustom=পছন্দসই /কাস্টম\nicon=আইকন\nauthor=সম্পাদক\nlink=লিংক\nsize=সাইজ\nstatus=স্ট্যাটাস /অবস্থা\nparts_info_downloaded_size=ডাউনলোড সম্পন্ন হয়েছে\nparts_info_total_size=মোট\nspeed=গতি\ntime_left=অবশিষ্ট সময়\ndate_added=তারিখে যুক্ত হয়েছে\ninfo=তথ্য\ndownload_page_downloaded_size=ডাউনলোড সম্পন্ন হয়েছে\ndownload_page_download_completed=ডাউনলোড সম্পূর্ণ হয়েছে\nresume_support=পুনরায় শুরু সমর্থন করে\nyes=হ্যাঁ\nno=না\nparts_info=অংশ তথ্য\ndisconnected=সংযোগ বিছিন্ন\nreceiving_data=তথ্য গ্রহণ করা হচ্ছে\nconnecting=পেতে প্রেরণ করুন\nwarning=সতর্কতা\nunsupported_resume_warning=এই ডাউনলোড পুনরায় শুরু করা সমর্থন করে না\\! ডাউনলোড তালিকা থেকে আপনাকে পরে এটি পুনরায় চালু করতে হতে পারে\nstop_anyway=যেভাবেই হোক থামুন\ncustomize_columns=কলাম কাস্টমাইজ করুন\nreset=রিসেট /পুনরায় সেট করুন\nmonday=সোমবার\ntuesday=মঙ্গলবার\nwednesday=বুধবার\nthursday=বৃহস্পতিবার\nfriday=শুক্রবার\nsaturday=শনিবার\nsunday=রবিবার\nproxy_open_system_proxy_settings=সিস্টেম প্রক্সি সেটিংস খুলুন\nproxy_type=প্রক্সি টাইপ\nproxy_do_not_use_proxy_for=এর জন্য প্রক্সি ব্যবহার করবেন না৷\nproxy_do_not_use_proxy_for_description=ইউআরএলগুলির একটি তালিকা যা প্রক্সি করা যাবে না\\nআপনি * এর সাথে ওয়াইল্ডকার্ড ব্যবহার করতে পারেন\\nউদাহরণস্বরূপ, 192.168.1.* example.com (স্পেস আলাদা করা হয়েছে)\nproxy_change_title=প্রক্সি পরিবর্তন\nchange_proxy=প্রক্সি পরিবর্তন\nproxy_no=কোন প্রক্সি নেই\nproxy_system=সিস্টেম প্রক্সি\nproxy_manual=ম্যানুয়াল প্রক্সি\nproxy_pac=প্রক্সি অটো কনফিগারেশন\nproxy_pac_url=প্রক্সি অটো কনফিগারেশন URL\naddress=ঠিকানা\nport=পোর্ট\naddress_and_port=ঠিকানা ও পোর্ট\nuse_authentication=অথেনটিকেশন /প্রমাণীকরণ ব্যবহার করুন\nwarning_you_may_have_to_restart_the_download_later=আপনাকে পরে ডাউনলোড পুনরায় চালু করতে হতে পারে\\!\nedit_download_title=ডাউনলোড সম্পাদনা করুন\nedit_download_update_from_download_page=ডাউনলোড পেজ থেকে আপডেট\nedit_download_update_from_download_page_description=এই উইন্ডোটি খোলা হলে, আপনি ডাউনলোড পেজে যেতে পারেন এবং ডাউনলোড বোতামে ক্লিক করতে পারেন। অ্যাপটি নতুন ডাউনলোডের শংসাপত্রগুলি ক্যাপচার করবে এবং আপডেট করবে যাতে আপনি সেগুলি সংরক্ষণ করতে পারেন।\nedit_download_saved_download_item_size_not_match=সংরক্ষিত ডাউনলোড আইটেমটির আকার {{currentSize}}, যেটি {{newSize}} এর নতুন আকারের সাথে মেলে না৷\ntranslators_page_thanks=যারা এই প্রকল্পটি অনুবাদ করতে সাহায্য করেছেন তাদের প্রতি কৃতজ্ঞতা ❤️\ntranslators=অনুবাদক\nlanguage=ভাষা\ntranslators_contribute_title=অনুবাদ উন্নত করুন\ntranslators_contribute_description=এই প্রকল্পের উন্নতি করতে সাহায্য করতে চান? যদি আপনার ভাষা তালিকাভুক্ত না হয় বা কিছু পরিবর্তনের প্রয়োজন হয়, তাহলে আপনি আপনার অনুবাদে অবদান রাখতে পারেন এবং এটি আরও ভাল করতে পারেন\\!\ncontribute=অবদান\nmeet_the_translators=অনুবাদকারীদের দেখুন\nlocalized_by_translators=অনুবাদকদের দ্বারা স্থানীয়করণ\nconfirm_exit=প্রস্থান নিশ্চিত করুন\nconfirm_exit_description=আপনি কি নিশ্চিত যে আপনি AB Download Manager থেকে প্রস্থান করতে চান?\\nচলমান ডাউনলোড/সারি বন্ধ হয়ে যাবে\\!\nupdate=হালনাগাদ\nupdate_updater=হালনাগাদকারী\nupdate_available=হালনাগাদ উপলব্ধ\nupdate_error=আপডেট ত্রুটি\nupdate_available_suggest_to_to_update=আপনি নতুন বৈশিষ্ট্য, বর্ধিতকরণ, এবং কর্মক্ষমতা উন্নতি উপভোগ করতে সর্বশেষ সংস্করণে আপডেট করতে পারেন৷\nupdate_release_notes=মুক্তির চিরকুট\nupdate_check_for_update=হালনাগাদ পরীক্ষা করুন\nupdate_checking_for_update=হালনাগাদ পরীক্ষা করা হচ্ছে\nupdate_no_update=আপনি সর্বশেষ সংস্করণ ব্যবহার করছেন\nupdate_check_error=ত্রুটি যখন হালনাগাদ পরীক্ষা করা হচ্ছে\nupdate_app_updated_to_version_n=অ্যাপ {{version}} সংস্করণে আপডেট হয়েছে\ncreate_desktop_entry=ডেস্কটপ এন্ট্রি তৈরি করুন\nshutdown_alert=শাট ডাউন সতর্কতা\nsystem_shutdown_soon=সিস্টেম শীঘ্রই বন্ধ হয়ে যাবে\\!\nsystem_shutdown_failed=সিস্টেম শাট ডাউন ব্যর্থ হয়েছে\\!\nsystem_shutdown_soon_description=সিস্টেমটি শীঘ্রই বন্ধ হয়ে যাবে। আপনি যদি এখনও কম্পিউটার ব্যবহার করেন, তাহলে অনুগ্রহ করে আপনার কাজটি সংরক্ষণ করুন অথবা শাটডাউন বাতিল করুন।\nsystem_shutdown_reason_queue_completed=সারিতে থাকা সমস্ত ডাউনলোড সম্পূর্ণ হয়েছে।\nsystem_shutdown_reason_queue_end_time_reached=ডাউনলোড সারির নির্ধারিত সমাপ্তির সময় শেষ হয়ে গেছে।\nsystem_shutdown_download_finished=ডাউনলোড সম্পন্ন হয়েছে।\nshutdown_now=এখনই বন্ধ করুন\nsettings_per_host_settings_new_host=<নতুন হোস্ট>\nsettings_per_host_settings_not_selected=প্রথমে একটি নতুন আইটেম তৈরি করুন বা নির্বাচন করুন\\!\nsettings_per_host_settings_host=হোস্ট\nsettings_per_host_settings_host_description=এই সেটিংস এই হোস্টনামের সাথে মিলে যাওয়া ডাউনলোডগুলিতে প্রয়োগ করা হবে। ওয়াইল্ডকার্ড (*) সমর্থিত (যেমন, example.com, *.example.com — শুধুমাত্র একটি ব্যবহার করুন)।\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=অনুসারে সাজান\nwelcome=স্বাগতম\nnew_folder=নতুন ফোল্ডার\nskip=এড়িয়ে যান\nlets_go=চলুন যাই\nnext=পরবর্তী\nselect_all=সবগুলো নির্বাচন করুন\nselect_inside=ভিতরে নির্বাচন করুন\nselect_invert=বাহিরে নির্বাচন করুন\nopen_settings=সেটিংস খুলুন\nback=ফিরে যান\nservice_is_running=পরিষেবা চলছে\ninitial_setup_description=আসুন জিনিসগুলি সেট আপ করি\ninitial_setup_notice=আপনি পরে যেকোনো সময় এই সেটিংস পরিবর্তন করতে পারেন\npermission_granted=অনুমতি দেওয়া হয়েছে\npermission_not_granted=অনুমতি দেওয়া হয়নি\npermissions=অনুমতি\ngive_permission=অনুমতি দিন\ngive_storage_permission=স্টোরেজ অ্যাক্সেসের অনুমতি দিন\nstorage_roots=Storage Roots\npermissions_initial_title=অনুমতি সেটআপ\npermissions_initial_description=সঠিকভাবে কাজ করার জন্য, অ্যাপটির কয়েকটি অনুমতির প্রয়োজন। পরবর্তী স্ক্রিনে, আপনি প্রতিটি অনুমতি কীসের জন্য ব্যবহার করা হয়েছে তা দেখতে পাবেন এবং আপনি সিদ্ধান্ত নিতে পারেন কোনটি অনুমতি দেবেন বা এড়িয়ে যাবেন।\npermissions_done_title=আপনি সম্পূর্ণ প্রস্তুত\\!\npermissions_done_description=সবকিছু প্রস্তুত।সমস্ত প্রয়োজনীয় অনুমতি দেওয়া হয়েছে এবং অ্যাপটি ব্যবহার করা ভাল।\npermissions_manage_storage_title=স্টোরেজ অ্যাক্সেস পরিচালনা করুন\npermissions_manage_storage_reason=এই অনুমতি অ্যাপটিকে ডাউনলোড ফোল্ডার পরিবর্তন করতে, ডুপ্লিকেট ডাউনলোডগুলি আরও সঠিকভাবে সনাক্ত করতে এবং কিছু অতিরিক্ত বৈশিষ্ট্য সক্ষম করতে দেয়৷ এটি ঐচ্ছিক, কিন্তু সেরা অভিজ্ঞতার জন্য প্রস্তাবিত।\npermission_read_write_external_storage_title=স্টোরেজ পড়া এবং লিখুন\npermission_read_write_external_storage_reason=এই অনুমতি অ্যাপটিকে ডাউনলোড করা ফাইলগুলি সংরক্ষণ এবং পরিচালনা করতে, ডাউনলোডের অবস্থান পরিবর্তন করতে এবং ডুপ্লিকেট ডাউনলোড সনাক্তকরণ উন্নত করতে দেয়৷\npermissions_post_notification_title=বিজ্ঞপ্তি পোস্ট করুন\npermissions_post_notification_reason=ডাউনলোড পরিচালনা করতে অ্যাপটিকে ব্যাকগ্রাউন্ডে চালাতে হবে। আপনাকে অবগত রাখতে এবং ব্যাকগ্রাউন্ড অপারেশনের অনুমতি দিতে বিজ্ঞপ্তিগুলি ব্যবহার করা হয়৷\npermissions_ignore_battery_optimization_title=ব্যাটারি অপ্টিমাইজেশান উপেক্ষা করুন\npermissions_ignore_battery_optimization_reason=কিছু ডিভাইস ব্যাটারি বাঁচাতে আক্রমনাত্মকভাবে ব্যাকগ্রাউন্ড অ্যাক্টিভিটি সীমিত করে, যা অ্যাপ খোলা না থাকলে ডাউনলোড থামাতে বা বন্ধ করতে পারে। ডাউনলোডগুলি অবিচ্ছিন্নভাবে চলতে থাকে তা নিশ্চিত করতে আপনি বিকল্পভাবে অ্যাপটিকে ব্যাটারি অপ্টিমাইজেশন থেকে বাদ দিতে পারেন\nopen_in_browser=ব্রাউজারে খুলুন\nbrowser=ব্রাউজার\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=নতুন ট্যাবে খুলুন\nbrowser_open_in_new_background_tab=ব্যাকগ্রাউন্ডে নতুন ট্যাবে খুলুন\nbrowser_no_tab_open=কোন ট্যাব খোলা নেই\nbrowser_tabs=ট্যাব\nbrowser_paste_and_go=পেস্ট করুন ও এতে যান\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/bqi_IR.properties",
    "content": "app_title=دؽوۉداری دانلود کوۊ\nconfirm_auto_categorize_downloads_title=کتن بندی خوتکار دانلودا\nconfirm_auto_categorize_downloads_description=هر موورد بؽ کتن بندی، و جۊر خوتکار و کتن بندی مربۊت ب خوس ٱوورده ابۊ.\nconfirm_reset_to_default_categories_title=وورگندن و کتن بندیا پؽش فرز\nconfirm_reset_to_default_categories_description=ای کار پوی کتن بندیا ن پاک اکونه وو کتن بندیا پؽش فرز ن اوورگنه\\!\nconfirm_delete_download_items_title=تاییڌ پاک کردن\nconfirm_delete_download_items_description=الن اخۊین {{count}} موورد ن پاک کۊنین\nconfirm_delete_download_unfinished_items_description=الن اخۊین {{count}} دانلود تموم نوابیڌه ن پاک کۊنین؟\nconfirm_delete_download_finished_and_unfinished_items_description=الن اخۊین {{finishedCount}} دانلود تموم وابیڌه وو {{unfinishedCount}} دانلود تموم نوابیڌه ن پاک کۊنین؟\nalso_delete_file_from_disk=هومچیناکو فایلن ز ری دیسک هم پاک کۊنین\nconfirm_delete_category_item_title=کتن بندی {{name}} هونی پاک ابۊ\nconfirm_delete_category_item_description=الن اخۊی کتن بندی \"{{value}}\" ن پاک کۊنی؟\nyour_download_will_not_be_deleted=دانلودا ایسا پاک نؽبۊن\ndrag_the_file_to_another_app=فایل ن و برنومه دیری بکشین\ndrop_link_or_file_here=لینگ یا فایل ن ایچو بنین.\nnothing_will_be_imported=لینگی و من نیا\\!\nn_links_will_be_imported={{count}} لینگ و من ایا\nn_items_selected={{count}} موورد پسند وابیڌه\nwindow_close=بستن\nwindow_minimize=کۊچیر کردن\nwindow_maximize=گپ کردن\nwindow_restore=وورگندن\ndelete=پاک کردن\nremove=پاک کردن\ncancel=رڌ کردن\nclose=بستن\nmenu=Menu\nmore_options=More Options\nok=خا\nadd=ٱووردن\npaste=Paste\nchange=آلشت\nedit=آلشت\nchange_anyway=و هر هال آلشتس بڌه\ndownload=دانلود\nrefresh=وانۊ کردن\nsettings=سامووا\non_completion=دیندا\nunknown=ن دیاری\nunknown_error=ختا ن دیاری\ndownload_item_not_found=موورد دانلود ن نجوست\nname=نوم\ndownload_link=لینگ دانلود\nnot_finished=تموم نوابیڌه\nall=پوی\nfinished=تموم وابی\nUnfinished=تموم نکرده\ncanceled=رڌ وابیڌه\nerror=ختا\npaused=واڌاشته\ndownloading=هونی دانلود ابۊ\nadded=ٱوورده وابیڌه\nidle=بؽ کار\npreparing_file=هونی فایل ن ٱماڌه اکونه\ncreating_file=هونی فایل ن وورکل اکونه\nresuming=هونی ز سر اگره\nretrying=هونی ز نۊ قپ ریت اکونه\nlist_is_empty=نومگه پتی هڌ\\!\nsearch_in_the_list=پیتینیڌن من نومگه\nsearch=پیتینیڌن\nclear=روفتن\ngeneral=پوی وولاتی\nenabled=فعال\ndisabled=قیر فعال\ndefault=خوتکار\nfile=فایل\ntasks=کارا\ntools=ٱوزارا\nhelp=هیاری\nsystem=سیستوم\nall_missing_files=پوی فایلا ز دست رئڌه\nall_finished=پوی تموم وابیڌه یل\nall_unfinished=پوی تموم نوابیڌه یل\nentire_list=پوی نومگه\ndownload_browser_integration=ی جۊر کردن دانلود وا گشت گر\nexit=و در زیڌن\nshow_downloads=نشووݩ داڌن دانلودا\nnew_download=دانلود نۊ\nstop_all=واڌاشتن پوی\nimport_from_clipboard=و من ٱووردن ز کلیپ بورد\nbatch_download=دانلود کتنی\nopen=گۊشیڌن\nshare=Share\nopen_file=گۊشیڌن فایل\nopen_folder=گۊشیڌن دوبلگه\nresume=رئڌن وا پؽش\npause=واڌاشتن\nrestart_download=ناهاڌن پا دانلود دووارته\ncopy=لف گیری کردن\ncopy_link=لف گیری لینگ\ncopy_as_curl=جۊر cURL لف گیری بۊ\nshow_properties=نشووݩ داڌن ویژیی یل\nmove_to_queue=جا گورو و سف\nmove_to_this_queue=جا گورو و ای سف\nmove_to_category=جا گورو و کتن بندی\nmove_to_this_category=جا گورو و ای کتن بندی\ncategories=کتن بندی یل\nadd_category=ٱووردن کتن بندی\nedit_category=آلشت کتن بندی\ndelete_category=پاک کردن کتن بندی\ncategory_name=نوم کتن بندی\ncategory_download_location=جاگه زفت کردن کتن بندی\ncategory_download_location_description=هر سا ای کتن بندی من بلگه \"ٱووردن دانلود\" پسند وابی ای تور ن سی زفت کردن فایل و کار اگره\ncategory_file_types=نوء فایلا کتن بندی\ncategory_file_types_description=و جۊر خوتکار ای نوع فایلا و ای کتن بندی ازاف ابۊن (هرسا ک ی دانلود نۊ ازاف ابۊ)\\n«فاسله» ن سی سوا کردن نوع فایلا و کار بگیرین (ext1 ext2 ext3...)\ncategory_url_patterns=اۊلگۊ یل URL\ncategory_url_patterns_description=Automatically put download from these URLs to this category. (when you add new download)\\nSeparate URLs with space, you can also use * for wildcard\nauto_categorize_downloads=کتن بندی خوتکار دانلودا\nrestore_defaults=وورگندن پؽش فرزا\nabout=زبار\nversion_n=نوسخه {{value}}\ndeveloped_with_love_for_you=وا ❤️ سی ایسا وورکل وابیڌه\ndonate=لادراری مالی\nvisit_the_project_website=سایت پوروژه ن بنیرین\nthis_is_a_free_and_open_source_software=ای برنومه مۊفتی وو بونچک واز هڌ\nview_the_source_code=کود بونچک ن بنیرین\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=جۉݩ گرؽڌه ز برنومه یل بونچک واز\nview_the_open_source_licenses=نشووݩ داڌن موجوزا بونچک واز\nsupport_and_community=لادراری وو بونکۊ\ntelegram=تلگرام\nchannel=تورگه\ngroup=بونکۊ\nadd_download=ٱووردن دانلود\nadd_multi_download_page_header=مووردایی ک اخۊین دانلود بۊن پسند کۊنین\nsave_to=زفت کردن من\nwhere_should_each_item_saved=هر موورد کوئجه زفت بۊ؟\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=چند موورد هڌه\\! روش زفت کردن ن پسند کۊنین\neach_item_on_its_own_category=هر موورد من کتن بندی خوس\neach_item_on_its_own_category_description=هر موورد من کتن بندی خوس و ری نوء فایل جا اگره\nall_items_in_one_category=پوی مووردا من ی کتن بندی\nall_items_in_one_category_description=پوی فایلا من کتن پسند بیڌه زفت ابۊن\nall_items_in_one_Location=پوی مووردا من ی جاگه\nall_items_in_one_Location_description=پوی مووردا من دوبلگه پسند بیڌه زفت ابۊن\nunselected_all_items_in_specific_location_description=پوی فایلا من کتن بندی پسند بیڌه زفت ابۊن\nno_category_selected=کتن بندی پسند نوابیڌه\nno_categories_found=کتن بندی نجۊرست\ndownload_location=جاگه دانلود\nlocation=جاگه\nselect_queue=پسند سف\nwithout_queue=بؽ سف\nuse_category=و کار گرؽڌن کتن بندی\ncant_write_to_this_folder=نتره من ای دوبلگه هؽل کونه\nfile_name_already_exists=نوم فایلن هڌه\ndownload_already_exists=دانلود ز زیتر بیڌس\ninvalid_file_name=نوم فایل زبال نؽ\nshow_solutions=نشووݩ داڌن ره هلا...\nchange_solution=آلشت ره هل\nselect_a_solution=پسند ی ره هل\nselect_download_strategy_description=لینگی ک داڌین من نومگه دانلود هڌس، دیاری کۊنین ک اخۊین چ کاری ٱنجوم دین\ndownload_strategy_add_a_numbered_file=ٱووردن فایل وا شوماره\ndownload_strategy_add_a_numbered_file_description=ٱووردن شوماره دیندا نوم فایل دانلود\ndownload_strategy_override_existing_file=ز نۊ هؽل کردن فایلی ک هڌس\ndownload_strategy_override_existing_file_description=پاک کردن دانلودی ک هڌس وو هؽل کردن ری اۊ فایل\ndownload_strategy_update_download_link=ورۊ رسۊوی دانلودی ک هڌس\ndownload_strategy_update_download_link_description=ورۊ کردن لینگ وو موجوزا دانلودی ک هڌس\ndownload_strategy_show_downloaded_file=نشووݩ داڌن فایل دانلود وابیڌه\ndownload_strategy_show_downloaded_file_description=نشووݩ داڌن موورد دانلودی ک هڌس سی رئڌن وا پؽش یا گۊشیڌن\nbatch_download_link_help=لینگی و من بیارین ک کاراکترا جاگۊزین (wildcard) ن داشته بۊ (* ن و کار بگیرین)\ninvalid_url=نشۊوی زبال نؽ\nlist_is_too_large_maximum_n_items_allowed=نومگه قلوه گپ هڌ\\! هدکسر {{count}} موورد موجاز هڌ\nenter_range=زیڌن تلایه\nrange_from=ز\nrange_to=تا\nbatch_download_wildcard_length=تۊل کاراکتر wildcard\nfirst_link=لینگ نیایی\nlast_link=لینگ دیندایی\nopen_source_software_used_in_this_app=برنومه یل بونچک واز و کار گرؽڌه من ای برنومه\nlinks=لینگا\nwebsite=وب سایت\ndevelopers=وورکل کونووݩ\nsource_code=کود بونچک\nlicense=جواز\nno_license_found=جوازی نجوست\norganization=سازمووݩ\nadd_new_queue=ٱووردن سف نۊ\nqueue_name=نوم سف\nqueues=سفا\nstop_queue=واڌاشتن سف\nstart_queue=ر وندن سف\nclear_queue_items=پتی کردن سف\nconfig=سامووݩ\nitems=مووردا\nmove_down=بلم بیڌن\nmove_up=ب روء بیذن\nremove_queue=پاک کردن سف\nqueue_name_help=نومی سی ای سف دیاری کۊنین\nqueue_name_describe=نوم سف {{value}} هڌ\nqueue_max_concurrent_download=هدکسر دانلود وایکی\nqueue_max_concurrent_download_description=هدکسر دانلود وایکی سی ای سف\nqueue_automatic_stop=واڌاشتن خوتکار\nqueue_automatic_stop_description=واڌاشتن خوتکار سف هر سا ک مووردی منس نؽ\nqueue_scheduler=مجال بندی\nqueue_enable_scheduler=فعال کردن مجال بندی\nqueue_active_days=رۊزا فعال\nqueue_active_days_description=من چ رۊزایی وا مجال بندی فعال بۊوه؟\nqueue_scheduler_enable_auto_start_time=فعال کردن مجال ر وندن خوتکار\nqueue_scheduler_auto_start_time=مجال ر وندن خوتکار\nqueue_scheduler_enable_auto_stop_time=فعال کردن مجال واڌاشتن خوتکار\nqueue_scheduler_auto_stop_time=مجال واڌاشتن خوتکار\nqueue_shutdown_on_completion=دیندا سیستوم کۊر بۊ\nqueue_shutdown_on_completion_description=کۊر کردن خوتکار سیستوم مجالی ک ای سف تموم بۊ یا زمووݩ تموم بیڌن برنومه ریزی وابیڌس برسه.\nappearance=شؽوات\ndownload_engine=موتور دانلود\nbrowser_integration=ی جۊر کردن وا گشت گر\nsettings_download_max_retries_count=هدکسر قپ ریتا دووارته سی دانلود\nsettings_download_max_retries_count_description=هدکسر کرتایی ک برنومه قپ ریت اکونه تا  دانلود نامووفق ن ز نۊ ٱنجوم بڌه پؽش ز یو ک تسلیم بۊ\nsettings_download_max_retries_count_describe_no_retries=سی دانلودا نامووفق دووارته قپ ریت نؽکونه\nsettings_download_max_retries_count_describe_n_retries=سی دانلودا نامووفق {{count}} کرت قپ ریت اکونه\nsettings_download_thread_count=تئداد منپیزا\nsettings_download_thread_count_description=هدکسر تئداد منپیزا سی هر موورد دانلود\nsettings_download_thread_count_describe=هر دانلود تره تا {{count}} منپیز داشته بۊ\nsettings_download_thread_count_with_large_value_describe=Warning\\: Setting a high thread count may increase system resource usage, reduce performance, or cause connection issues with servers. Use higher values only if you understand the potential impact on your system and network.\nsettings_use_server_last_modified_time=و کار گرؽڌن مجال دیندایی آلشت سرور\nsettings_use_server_last_modified_time_description=مجال دانلود، زمووݩ آلشت دیندایی سرور سی فایل مهلی و کار اگؽره\nsettings_append_extension_to_incomplete_downloads=ٱووردن پسوند و دانلودا تموم نوابیڌه\nsettings_append_extension_to_incomplete_downloads_description=Append \".part\" extension to incomplete downloads. This helps to identify unfinished downloads and prevents accidental opening of incomplete files.\nsettings_use_sparse_file_allocation=وورکل فایل و جۊر پۊیا Sparse\nsettings_use_sparse_file_allocation_description=Create files more efficiently, especially on SSDs, by reducing unnecessary data writing. This can speed up download starts and reduce disk usage. If downloads start slowly or you experience unusual download speeds, consider disabling this option, as it may not be fully supported on some devices.\nsettings_ignore_ssl_certificates=نیڌه گرؽڌن گوواهی یل SSL\nsettings_ignore_ssl_certificates_description=واجۊری گوواهی SSL ن قیرفعال اکونه. تینا ٱر لنگس هڌین ای گۊزینه ن و کار بگیرین، چیناکه گاشڌ سی منپیز ایسا خترا ٱمنیتی پؽش بیا.\nsettings_global_speed_limiter=مئدۊد کوݩ ترات کۊلی\nsettings_global_speed_limiter_description=ترات کۊلی دانلود و ای مقدار مئدۊد ابۊ (0 و مئنی بؽ مئدۊدیت)\nsettings_show_average_speed=نشووݩ داڌن ترات هندا منجا\nsettings_show_average_speed_description=ترات دانلود جۊر هندا منجا یا دییق نشووݩ داڌه ابۊ\nsettings_use_category_by_default=و کار گرؽڌن کتن بندی جۊر پؽش فرز\nsettings_use_category_by_default_description=مجال ٱووردن دانلود، هالت پؽش فرز کتن بندی ن و کار بگره.\nsettings_default_download_folder=دوبلگه دانلود پؽش فرز\nsettings_default_download_folder_description=مجالی ک دانلود نۊیی ن ٱوۊردین، ای جاگه سی جاگه پؽش فرز و کار اره\nsettings_default_download_folder_describe=\"{{folder}}\" و کار اروه\nsettings_use_proxy=و کار گرؽڌن پروکسی\nsettings_use_proxy_description=سی دانلود فایلا پروکسی ن و کار بگیرین\nsettings_use_proxy_describe_no_proxy=پروکسی و کار گرؽڌه نؽبۊ\nsettings_use_proxy_describe_system_proxy=پروکسی سیستوم و کار اروه\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" و کار اروه\nsettings_use_proxy_describe_pac_proxy=فایل pac وا ای نشۊوی و کار اروه\\: {{value}}\nsettings_track_deleted_files_on_disk=رئگیری فایلا ز دست رئڌه ز ری ویرگه\nsettings_track_deleted_files_on_disk_description=ٱر فایلا ز تور دانلود پاک یا جا گورو وابین، و جۊر خوتکار ز نومگه دانلود پاک ابۊن.\nsettings_delete_partial_file_on_download_cancellation=پاک کردن فایل تموم نوابیڌه مجال رڌ کردن دانلود\nsettings_delete_partial_file_on_download_cancellation_description=When a download is canceled, the partially downloaded file will be deleted from the disk. This helps keep your download folder clean and reduces unnecessary disk space usage. However, the download will restart from the beginning the next time you start it.\nsettings_default_user_agent=User-Agent پؽش فرز\nsettings_default_user_agent_description=Specify the Default-User Agent string to define how requests identify to servers. This can help in accessing content optimized for particular devices or in circumventing download limitations imposed by certain websites.\nsettings_download_size_unit=واهڌ هندا دانلود\nsettings_download_size_unit_description=واهڌ و کار گرؽڌه سی نشووݩ داڌن هندا دانلود\nsettings_download_speed_unit=واهڌ ترات دانلود\nsettings_download_speed_unit_description=واهڌ و کار گرؽڌه سی نشووݩ داڌن ترات دانلود\nsettings_theme=تم\nsettings_theme_description=پسند تم برنومه\nsettings_default_dark_theme=تم تاریک پؽش فرز\nsettings_default_dark_theme_description=مجالی ک برنومه تم سیستوم ن و دین اکونه وو تم سیستوم تاریک هڌ ای تم و کار اروه\nsettings_default_light_theme=تم رۊشنا پؽش فرز\nsettings_default_light_theme_description=مجالی ک برنومه تم سیستوم ن و دین اکونه وو تم سیستوم رۊشنا هڌ ای تم و کار اروه\nsettings_font=فونت\nsettings_font_description=آلشت فونتی ک من برنومه و کار اروه. ی قرد ز فونتا گاشڌ من ای برنومه و خۊوی نشووݩ داڌه نبۊن.\nsettings_ui_scale=هندا رابت منتوری\nsettings_ui_scale_description=آلشت هندا المان وو هؽلا من بلگه یل\nsettings_language=زووݩ\nsettings_compact_top_bar=نوار رویی جم وو جۊر\nsettings_compact_top_bar_description=شؽونیڌن نوار رویی وا نوار وارسۊوی مجالی ک نیمدری ٱسلی و هندایی ک بس بۊ جا داشته بۊ\nsettings_use_native_menu_bar=و کار گرؽڌن نوار نومگه سیستوم\nsettings_use_native_menu_bar_description=نوار نومگه پؽش فرز سیستوم و کار گرؽڌه بۊ\nsettings_use_relative_date_time=و کار گرؽڌن زمووݩ نسبی\nsettings_use_relative_date_time_description=قالوو زمووݩ/ویرگار نسبی ن سی نشووݩ داڌن ویرگارا من برنومه و کار بگیرین (جۊر «2 رۊز پؽش» و جا ویرگار وو زمووݩ دییق)\nsettings_show_icon_labels=نشووݩ داڌن هؽل آیکونا\nsettings_show_icon_labels_description=لیبلا ٱر ک بۊوه زؽر آیکونا نشووݩ داڌه ابۊن (جۊر دویمه یل نوار ٱوزار بلگه ٱسلی)\nsettings_use_system_tray=و کار گرؽڌن System Tray\nsettings_use_system_tray_description=نشووݩ داڌن System Tray مجالی ک برنومه ر وسته\nsettings_start_on_boot=ر وستن مجال و من ٱووڌن و سیستوم\nsettings_start_on_boot_description=ر وستن خوتکار برنومه مجال و من ٱووڌن منتور\nsettings_notification_sound=دونگ وارسۊوی\nsettings_notification_sound_description=پشک دونگ مجال وارسۊوی نۊ\nsettings_browser_integration=ی جۊر کردن وا گشت گر\nsettings_browser_integration_description=گرؽڌن دانلودا ز گشت گر\nsettings_browser_integration_server_port=پورت سرور\nsettings_browser_integration_server_port_description=پورت سی ی جۊر کردن وا گشت گر\nsettings_browser_integration_server_port_describe=برنومه ز پورت {{port}} گۊش اگره\nsettings_dynamic_part_creation=وورکل پارت و جۊر پۊیا\nsettings_dynamic_part_creation_description=مجالی ک ی پارت کامل وابی، پارت دیری وورکل ابۊ تا ترات دانلود قلوه بۊ\nsettings_show_completion_dialog=نشووݩ داڌن نیمدری کامل وابیڌن دانلود\nsettings_show_completion_dialog_description=هر سا ک ی دانلود تموم وابی و جۊر خوتکار نیمدری تموم وابیڌن دانلود نشووݩ داڌه ابۊ.\nsettings_show_download_progress_dialog=نشووݩ داڌن نیمدری پؽش رئڌن دانلود\nsettings_show_download_progress_dialog_description=هر سا ک ی دانلود ر وست و جۊر خوتکار نیمدری پؽش رئڌن دانلود نشووݩ داڌه ابۊ.\nsettings_per_host_settings=سامووا سی هر هاست\nsettings_per_host_settings_descriptions=ای سامووا و جۊر خوتکار ری دانلودا نۊیی ک ی هاست دیاری ن و کار اگرن ائمال ابۊ.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=مئدۊدیت دانلود\ndownload_item_settings_speed_limit_description=مئدۊدیت ترات دانلود سی ای موورد\ndownload_item_settings_show_download_completion_dialog=نشووݩ داڌن نیمدری کامل وابیڌن دانلود\ndownload_item_settings_show_download_completion_dialog_description=هر سا ک ای دانلود تموم وابی و جۊر خوتکار بلگه کامل وابیڌن دانود نشووݩ داڌه ابۊ.\ndownload_item_settings_shutdown_on_completion=دیندا سیستوم کۊر بۊ\ndownload_item_settings_shutdown_on_completion_description=کۊر کردن خوتکار سیستوم مجالی ک ای دانلود تموم وابی.\ndownload_item_settings_thread_count=تئداد منپیزا\ndownload_item_settings_thread_count_description=چند تا منپیز سی ای دانلود و کار گرؽڌه بۊ (0 سی پؽش فرز)\ndownload_item_settings_thread_count_describe={{count}} منپیز سی ای دانلود\ndownload_item_settings_username_description=ٱر لینگ ائراز هۊویت ز ایسا اخو، نوم منتوری ن بڌین\ndownload_item_settings_password_description=ٱر لینگ ائراز هۊویت ز ایسا اخو، رزم ن بڌین\ndownload_item_settings_download_page=بلگه دانلود\ndownload_item_settings_download_page_description=بلگه سایتی ک ای دانلود ز من اوچو وورکل وابیڌه\ndownload_item_settings_file_checksum=امزا فایل\ndownload_item_settings_file_checksum_description=وا و کار گرؽڌن ای کود ترین واجۊری کۊنین ک فایل و خۊوی دانلود وابیڌه یا ن\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=ی User-Agent سیخومی سی و کار گرؽڌن من ای دانلود (سی و کار گرؽڌن مقدار پؽش فرز، بؽلینس پتی بۊ)\nfile_checksum=امزا فایل\nfile_checksum_page=واجۊری کوݩ امزا فایل\nfile_checksum_page_file_checksum_default_algorithm=ٱلگوریتم پؽش فرز\nfile_checksum_page_file_checksum_default_algorithm_help=ٱر ک امزا فایلا داڌه نبۊن ز ای ٱلگۊریتم پؽش فرز سی هساو کردن امزا فایل و کار اروه.\nstart=ر وندن\ncalculated_checksum=امزا هساو وابیڌه\nsaved_checksum=امزا زفت وابیڌه\nchecksum_algorithm=ٱلگوریتم\nfile_not_found=فایل ن نجوست\ndownload_not_finished=دانلود کامل نوابیڌه\ndone=ٱنجوم وابی\nwaiting=مندیر\nmatches=جۊر یکن\nnot_matches=جۊر یک نؽن\ncopy_to_clipboard=لف گیری من کلیپ بورد\nusername=نوم منتوری\npassword=رزم\naverage_speed=ترات منجا\nexact_speed=ترات دییق\nunlimited=نا مئدۊد\nuse_global_settings=و کار گرؽڌن سامووا پوی وولاتی\ncant_run_browser_integration=نتره ی جۊر کردن گشت گر ن ر ونه\ncant_open_file=نتره فایل ن بوگوشه\ncant_open_folder=نتره دوبلگه ن بوگوشه\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} سال\nrelative_time_long_months={{months}} ما\nrelative_time_long_days={{days}} رۊز\nrelative_time_long_hours={{hours}} ساعت\nrelative_time_long_minutes={{minutes}} دیقه\nrelative_time_long_seconds={{seconds}} سانیه\nrelative_time_short_years={{years}} سال\nrelative_time_short_months={{months}} ما\nrelative_time_short_days={{days}} رۊز\nrelative_time_short_hours={{hours}} ساعت\nrelative_time_short_minutes={{minutes}} دیقه\nrelative_time_short_seconds={{seconds}} سانیه\nrelative_time_left={{time}} منده\nrelative_time_ago={{time}} پؽش\nauto=خوتکار\nunspecified=ن دیلری\ncustom=دلخا\nicon=آیکون\nauthor=وورکل کوݩ\nlink=لینگ\nsize=هندا\nstatus=وزعیت\nparts_info_downloaded_size=دانلود وابیڌه\nparts_info_total_size=پوی\nspeed=ترات\ntime_left=مجال باقی منده\ndate_added=ویرگار ٱووڌن\ninfo=جۊزعیات\ndownload_page_downloaded_size=دانلود وابیڌه\ndownload_page_download_completed=دانلود تموم وابی\nresume_support=امکووݩ ز سر گرؽڌن\nyes=هری\nno=ن\nparts_info=جۊزعیات پارتا\ndisconnected=قت وابی\nreceiving_data=گرؽڌن داده\nconnecting=هونی منپیز ابۊ\nwarning=بپا\nunsupported_resume_warning=ای دانلود ن نترین ز سر گیرین وو گاشڌ دیندا تر مجبۊر بۊین هونه ز من نومگه دانلود \"ریستارت\" کۊنین\nstop_anyway=و هر هال واسته\ncustomize_columns=آلشت سۊتۊنا\nreset=وورنشۊوی\nmonday=دوشمبه\ntuesday=سه شمبه\nwednesday=چار شمبه\nthursday=پنجشمبه\nfriday=جومه\nsaturday=شمبه\nsunday=ی شمبه\nproxy_open_system_proxy_settings=گۊشیڌن سامووا پروکسی سیستوم\nproxy_type=نوء پروکسی\nproxy_do_not_use_proxy_for=پروکسی ن سی یونووݩ و کار مگر\nproxy_do_not_use_proxy_for_description=A list of urls that may not be proxied\\nYou can use wildcard with *\\nfor example 192.168.1.* example.com (space separated)\nproxy_change_title=آلشت پروکسی\nchange_proxy=آلشت پروکسی\nproxy_no=بؽ پروکسی\nproxy_system=پروکسی سیستوم\nproxy_manual=پروکسی دستی\nproxy_pac=کانفیگ خوتکار پروکسی (pac)\nproxy_pac_url=آدرس فایل کانفیگ خوتکار پروکسی\naddress=آدرس\nport=پورت\naddress_and_port=آدرس وو پورت\nuse_authentication=و کار گرؽڌن ائراز هۊویت\nwarning_you_may_have_to_restart_the_download_later=ایسا گاشڌ دیندا تر مجبۊر بۊین ای دانلود ن دووارته ر ونین\\!\nedit_download_title=آلشت دانلود\nedit_download_update_from_download_page=ورۊ رسۊوی ز بلگه دانلود\nedit_download_update_from_download_page_description=When this window is open, you can go to the Download Page and click the download button. The app will capture and update the new download credentials so you can save them.\nedit_download_saved_download_item_size_not_match=موورد دانلود وا هندا {{currentSize}} زفت وابیڌه، ک وا هندا نۊ {{newSize}} ی جۊر نؽ.\ntranslators_page_thanks=ممنووݩ دار هونووی هڌیم ک من ولرنیڌن ای پوروژه هیاری کردن ❤️\ntranslators=ولرنی کارووݩ\nlanguage=زووݩ\ntranslators_contribute_title=بؽڌر کردن ولرنیڌنا\ntranslators_contribute_description=Want to help improve this project? If your language isn't listed or needs some tweaks, you can contribute your translations and make it better\\!\ncontribute=هیاری داڌن\nmeet_the_translators=آشنایی وا ولرنی کارووݩ\nlocalized_by_translators=بۊمی وابیڌه و دست ولرنی کارووݩ\nconfirm_exit=تاییڌ و در زیڌن\nconfirm_exit_description=الن اخۊی ز AB Download Manager زنی و در؟\\nدانلودا وو سفا فعال، واڌاشته ابۊن\\!\nupdate=ورۊ رسۊوی\nupdate_updater=ورۊ کوننده\nupdate_available=ورۊ رسۊوی من دسرس هڌ\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=ایسا وا ورۊ رسۊوی ترین قابلیتا دیندایی، پؽش رئڌنا وو بؽڌر وابیڌنا عملکردی ن و دست یارین.\nupdate_release_notes=ویرداشتا تیجنیڌن\nupdate_check_for_update=واجۊری سی ورۊ رسۊوی\nupdate_checking_for_update=هونی واجۊری اکونه سی ورۊ رسۊوی\nupdate_no_update=ایسا نوسخه دیندایی ن و کار گیریڌینه\nupdate_check_error=مجال ورۊ رسۊوی ختایی پؽش ٱووڌ\nupdate_app_updated_to_version_n=برنومه و نوسخه {{version}} ورۊ رسۊوی وابی\ncreate_desktop_entry=وورکل و من ٱووڌنی دسکتاپ\nshutdown_alert=هوشدار کۊر بیڌن\nsystem_shutdown_soon=سیستوم و هیم زی کۊر ابۊ\\!\nsystem_shutdown_failed=کۊر کردن سیستوم مووفق نبی\\!\nsystem_shutdown_soon_description=The system will shut down soon. If you're still using the computer, please save your work or cancel the shutdown.\nsystem_shutdown_reason_queue_completed=پوی دانلودا سف تموم کردن.\nsystem_shutdown_reason_queue_end_time_reached=زمووݩ تموم وابیڌن برنومه ریزی سی سف دانلود رسیڌه.\nsystem_shutdown_download_finished=دانلود تموم وابی.\nshutdown_now=هیم سکو کۊرس کوݩ\nsettings_per_host_settings_new_host=<هاست نۊ>\nsettings_per_host_settings_not_selected=ٱول ی موورد نۊ وورکل یا پسند کۊنین\\!\nsettings_per_host_settings_host=هاست\nsettings_per_host_settings_host_description=These settings will be applied to downloads matching this hostname. Wildcards (*) are supported (e.g., example.com, *.example.com — use only one).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ckb_IR.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=ڕێکخستنی بەشەکانی داگرتن بە خۆکاری\nconfirm_auto_categorize_downloads_description=ھەر دابەزاندنێکی پۆلێننەکراو خۆکارانە زیاد دەکرێت بۆ پۆلی پەیوەست پێی.\nconfirm_reset_to_default_categories_title=ڕێکیبخەرەوە بۆ پۆلە بنچینەییەکان\nconfirm_reset_to_default_categories_description=ئەمە ھەموو پۆلەکان دەسڕێتەوە و پۆلە بنچینەییەکان دەھێنێتەوە\\!\nconfirm_delete_download_items_title=سڕینەوە دووپات بکەرەوە\nconfirm_delete_download_items_description=دڵنیایت لە سڕینەوەی {{count}} شت؟\nconfirm_delete_download_unfinished_items_description=دڵنیایت لە سڕینەوەی {{count}} داونڵۆدی تەواونەبوو؟\nconfirm_delete_download_finished_and_unfinished_items_description=دڵنیایت لە سڕینەوەی {{finishedCount}} داونڵۆدی تەواوبوو و {{unfinishedCount}} دانەی تەواونەبوو؟\nalso_delete_file_from_disk=لەسەر کۆمپیوتەریش بیسڕەوە\nconfirm_delete_category_item_title=سڕینەوەی پۆلی {{name}}\nconfirm_delete_category_item_description=دڵنیایت لە سڕینەوەی {{value}} شت؟\nyour_download_will_not_be_deleted=داونڵۆدەکانت ناسڕدرێنەوە\ndrag_the_file_to_another_app=پەڕگەکە ڕاکێشە بۆ بەرنامەیەکی تر\ndrop_link_or_file_here=بەستەر یان پەڕگە بخەرە ئێرە.\nnothing_will_be_imported=ھیچ ھاوردە ناکرێت\nn_links_will_be_imported={{count}} بەسەر ھاوردە دەکرێت\nn_items_selected={{count}} شت دیاری کراوە\nwindow_close=داخستن\nwindow_minimize=بچووککردنەوە\nwindow_maximize=گەورەی بکە\nwindow_restore=گێڕانەوە\ndelete=سڕینەوە\nremove=لایببە\ncancel=ھەڵوەشاندنەوە\nclose=دایبخە\nmenu=Menu\nmore_options=More Options\nok=باشە\nadd=زیادکردن\npaste=Paste\nchange=بیگۆڕە\nedit=دەستکاری\nchange_anyway=ھەر چۆنێک بێت بیگۆڕە\ndownload=دایبەزێنە\nrefresh=نوێکردنەوە\nsettings=ڕێکخستنەکان\non_completion=لە تەواوبووندا\nunknown=نەزاندراو\nunknown_error=ھەڵەی نەزاندراو\ndownload_item_not_found=ستی داونڵۆد نەدۆزرایەوە\nname=ناو\ndownload_link=بەستەری داونڵۆد\nnot_finished=تەواو نەبووە\nall=ھەموو\nfinished=تەواوبوو\nUnfinished=تەواونەبوو\ncanceled=ھەڵوەشێندراو\nerror=ھەڵە\npaused=ڕاگیراو\ndownloading=دادەبەزێت\nadded=زیاد کرا\nidle=IDLE\npreparing_file=پەڕگەکە ئامادە دەکرێت\ncreating_file=پەڕگەکە دروست دەکرێت\nresuming=بەردەوامبوون\nretrying=ھەوڵدانەوە\nlist_is_empty=پێڕستەکە بەتاڵە\\!\nsearch_in_the_list=لە پێڕستەکەدا بگەڕێ\nsearch=گەڕان\nclear=سڕینەوە\ngeneral=گشتی\nenabled=کارا\ndisabled=ناکارا\ndefault=بنچینیەیی\nfile=پەڕگە\ntasks=ئەرکەکان\ntools=ئامرازەکان\nhelp=یارمەتی\nsystem=سیستەم\nall_missing_files=ھەموو پەڕگە بزرەکان\nall_finished=ھەموو تەواوبووەکان\nall_unfinished=ھەموو تەواونەبووەکان\nentire_list=پێڕستی تەواو\ndownload_browser_integration=یەکخستنی وێبگەڕ دابگرە\nexit=دەرچوون\nshow_downloads=داونڵۆدەکان پیشان بدە\nnew_download=داونڵۆدی نوێ\nstop_all=ھەمووی ڕابگرە\nimport_from_clipboard=لە کلیپبۆردەوە ھاوردەی بکە\nbatch_download=داونڵۆدی بە کۆمەڵ\nopen=بیکەرەوە\nshare=Share\nopen_file=پەڕگەکە بکەرەوە\nopen_folder=فۆڵدەرەکە بکەرەوە\nresume=بەردەوامبوون\npause=ڕاگرتن\nrestart_download=داونڵۆدەکە دەستپێبکەرەوە\ncopy=لەبەرگرتنەوە\ncopy_link=لەبەرگرتنەوەی بەستەر\ncopy_as_curl=لەبەرگرتنەوە وەک cURL\nshow_properties=تایبەتمەندییەکان پیشان بدە\nmove_to_queue=بیگوازەرەوە بۆ ڕیز\nmove_to_this_queue=بیگوازەرەوە بۆ ئەم ڕیزە\nmove_to_category=بیگوازەوە بۆ پۆل\nmove_to_this_category=بیگوازەرەوە بۆ ئەم پۆلە\ncategories=پۆلەکان\nadd_category=پۆل زیاد بکە\nedit_category=پۆل دەستکاری بکە\ndelete_category=پۆل بسڕەوە\ncategory_name=ناوی پۆل\ncategory_download_location=شوێنی داونڵۆدی پۆلەکە\ncategory_download_location_description=کاتێک ئەم پۆلە ھەڵدەبژێردرێت لە \"زیادکردنی داونڵۆد\" ئەمە وەک \"شوێنی داونڵۆد\" بەکاربێنە\ncategory_file_types=جۆری پەڕگەی پۆلەکە\ncategory_file_types_description=خۆکارانە ئەم جۆرە پەڕگانە بخەرە ئەم پۆلەوە. (کاتێک داونڵۆدێکی نوێ زیاد دەکەیت)\\nپاشگری پەڕگەکان بە بۆشایی جیا بکەرەوە (پاشگری١ پاشگری٢ ...)\ncategory_url_patterns=داڕێژەکانی بەستەر\ncategory_url_patterns_description=خۆکارانە داونڵۆدەکان لەم بەستەرەوە بخەرە ئەم پۆلەوە. (کاتێک داونڵۆدێکی نوێ زیاد دەکەیت)\\nبەستەرەکان بە بۆشایی جیابکەرەوە، دەشتوانیت * بەکاربھێنیت وەک کارەکتەری جێگرەوە\nauto_categorize_downloads=خۆکارانە دابەزاندنەکان پۆلێن بکە\nrestore_defaults=بیگەڕێنەرەوە سەر بنچینەییەکان\nabout=دەربارە\nversion_n=وەشانی {{value}}\ndeveloped_with_love_for_you=بە ❤️ەوە پەرەی پێدراوە\ndonate=بەخشین\nvisit_the_project_website=بچۆرە سەر وێبگەی پڕۆژەکە\nthis_is_a_free_and_open_source_software=ئەمە بەرنامەیەکی خۆڕایی و سەرچاوە کراوەیە\nview_the_source_code=کۆدی سەرچاوە ببینە\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=لەلایەن بەرنامەی سەرچاوە کراوەوە پاڵپشتیی کراوە\nview_the_open_source_licenses=مۆڵەتەکانی سەرچاوە کراوە ببینە\nsupport_and_community=یارمەتی و کۆمەڵگا\ntelegram=تەلەگرام\nchannel=کەناڵ\ngroup=کۆمەڵە\nadd_download=داونڵۆد زیاد بکە\nadd_multi_download_page_header=ئەو شتانە هەڵبژێە کە دەتەوێت ھەڵیانبگریت بۆ داونڵۆد\nsave_to=پاشەکەوتی بکە بۆ\nwhere_should_each_item_saved=لە کوێ ھەریەک لە شتەکان پاشەکەوت بکرێت؟\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=زیاد لە یەک شت ھەیە\\! تکایە ڕێگایەک ھەڵبژێرە بۆ پاشەکەوتکردنیان\neach_item_on_its_own_category=ھەر شتێک لەسەر پۆلی خۆی\neach_item_on_its_own_category_description=ھەر شتێک دەخرێتە پۆلێکەوە کە ئەو جۆرە پەڕگەیەی تێدایە\nall_items_in_one_category=ھەموو شتەکان لە یەک پۆل\nall_items_in_one_category_description=ھەموو پەڕگەکان پاشەکەوت دەکرێنە پۆلە دیاریکراوەکەوە\nall_items_in_one_Location=ھەموو شتەکان لە یەک شوێن\nall_items_in_one_Location_description=ھەموو شتەکان پاشەکەوت دەکرێنە پۆلە دیاریکراوەکەوە\nunselected_all_items_in_specific_location_description=ھەموو پەڕگەکان پاشەکەوت دەکرێنە شوێنی پۆلە دیاریکراوەکەوە\nno_category_selected=ھیچ پۆلێک ھەڵنەبژێردراوە\nno_categories_found=ھیچ پۆلێک نەدرۆزرایەوە\ndownload_location=شوێنی داونڵۆد\nlocation=شوێن\nselect_queue=ڕیز ھەڵبژێرە\nwithout_queue=بەبێ ڕیز\nuse_category=پۆل بەکاربێنە\ncant_write_to_this_folder=ناوتواندرێت لەسەر ئەم فۆڵدەرە بنووسرێت\nfile_name_already_exists=ناوی پەڕگە بوونی ھەیە\ndownload_already_exists=داونڵۆدەکە بوونی ھەیە\ninvalid_file_name=ناوی پەڕگەی نادروست\nshow_solutions=چارەسەرەکان پیشان بدە...\nchange_solution=چارەسەر بگۆڕە\nselect_a_solution=چارەسەرێک ھەڵبژێرە\nselect_download_strategy_description=ئەو بەستەرەی دابینت کردووە بوونی ھەیە لە پێڕستەکانی داونڵۆددا، تکایە دیاری بکە کە دەتەوێت چی بکەیت\ndownload_strategy_add_a_numbered_file=پەڕگەیەکی ژمارەکراو زیاد بکە\ndownload_strategy_add_a_numbered_file_description=نیشایەنەیەک زیاد بکە لە کۆتاییی ناوی پەڕگەی دابەزیوو\ndownload_strategy_override_existing_file=جێی پەڕگەی ھەبوو بگرەوە\ndownload_strategy_override_existing_file_description=ئەو داونڵۆدەی ھەیە بیسڕەوە و جێگەی بگرەوە\ndownload_strategy_update_download_link=داونڵۆدی ھەبوو نوێبکەرەوە\ndownload_strategy_update_download_link_description=بەستەری داونڵۆدی ھەبوو و باوەڕنامەکەی نوێ بکەرەوە\ndownload_strategy_show_downloaded_file=پەڕگەی داونڵۆدکراو پیشان بدە\ndownload_strategy_show_downloaded_file_description=داونڵۆدی لەوەپێش ھەبوو پیشان بدە، تاکوو بتوانیت کلیک لەسەر بەردەوامبوون بکەیت و بیکەیتەوە\nbatch_download_link_help=بەستەرێک بنووسە کە کارەکتەری جێگرەوەی تێدایە (* بەکاربھێنە)\ninvalid_url=بەستەری نادروست\nlist_is_too_large_maximum_n_items_allowed=پێڕستەکە زۆر گەورەیە\\! زۆرترین {{count}} ڕێگەپێدراوە\nenter_range=مەودایەک بنووسە\nrange_from=لە\nrange_to=بۆ\nbatch_download_wildcard_length=درێژیی کارەکتەری جێگرەوە\nfirst_link=یەکەم بەستەر\nlast_link=دوایین بەستەر\nopen_source_software_used_in_this_app=بەرنامە سەرچاوە کراوەکانی لەم بەرنامەیەدا بەکارھاتوون\nlinks=بەستەرەکان\nwebsite=وێبگە\ndevelopers=پەرەپێدەرەکان\nsource_code=کۆدی سەرچاوە\nlicense=مۆڵەت\nno_license_found=ھیچ مۆڵەتێک نەدۆزرایەوە\norganization=ڕێکخراو\nadd_new_queue=ڕیزێکی نوێ زیاد بکە\nqueue_name=ناوی ڕیز\nqueues=ڕیزەکان\nstop_queue=ڕیز ڕابگرە\nstart_queue=ڕیز دەستپێبکە\nclear_queue_items=ڕیز بەتاڵە\nconfig=شێوەپێدان\nitems=شتەکان\nmove_down=بیبەرە خوارەوە\nmove_up=بیبەرە سەرەوە\nremove_queue=ڕیز بسڕەوە\nqueue_name_help=ناوێک دیاری بکە بۆ ئەم ڕیزە\nqueue_name_describe=ناوی ڕیز {{value}}ە\nqueue_max_concurrent_download=زۆرترین داونڵۆدی ھاوکات\nqueue_max_concurrent_download_description=زۆرترین داونڵۆد بۆ ئەم ڕیزە\nqueue_automatic_stop=ڕاگرتنی خۆکار\nqueue_automatic_stop_description=خۆکارانە ڕیز بوەستێنە کە ھیچی تێدا نەبوو\nqueue_scheduler=خشتەکردن\nqueue_enable_scheduler=خشتەدانان کارا بکە\nqueue_active_days=ڕۆژە چالاکەکان\nqueue_active_days_description=خشتەکە لە چ ڕۆژێکدا کار بکات؟\nqueue_scheduler_enable_auto_start_time=کاتی دەستپێکردنی خۆکارانە کارا بکە\nqueue_scheduler_auto_start_time=کاتی دەستپێکردنی خۆکار\nqueue_scheduler_enable_auto_stop_time=کاتی ڕاگرتنی خۆکارانە کارا بکە\nqueue_scheduler_auto_stop_time=کاتی ڕاگرتنی خۆکار\nqueue_shutdown_on_completion=سیستەمەکە بکوژێنەوە لەگەڵ تەواوبوون\nqueue_shutdown_on_completion_description=خۆکارانە سیستەمەکە بکوژێنەرەوە کاتێک ڕیزەکە تەواو بوو، یان کە گەیشتیتە کاتی دیاریکراو.\nappearance=دەرکەوتن\ndownload_engine=بزوێنەری داونڵۆد\nbrowser_integration=یەکخستنی وێبگەڕ\nsettings_download_max_retries_count=زۆرترین ڕێژەی ھەوڵدانەوەکانی داونڵۆد\nsettings_download_max_retries_count_description=زۆرترین ڕێژەی ئەو کاتانەی کە بەرنامەکە ھەوڵی دووبارەکردنەوەی داونڵۆدێکی شکتخواردوو دەدات پێش وازھێنان\nsettings_download_max_retries_count_describe_no_retries=داونڵۆدە شکستخواردووەکان ھەوڵیان لەگەڵدا نادرێتەوە\nsettings_download_max_retries_count_describe_n_retries=داونڵۆدە شکستخواردووەکان {{count}} جار ھەوڵیان لەگەڵدا دەدرێتەوە\nsettings_download_thread_count=ژمارەی تاڵەکان\nsettings_download_thread_count_description=زۆرترین ڕێژەی تاڵەکانی داونڵۆد بۆ ھەر داونڵۆدێک\nsettings_download_thread_count_describe=داونڵۆدێک دەکرێت تا {{count}} تاڵی ھەبێت\nsettings_download_thread_count_with_large_value_describe=ئاگاداری\\: دانانی ژمارەی تاڵی زۆر لەوانەیە بەکارھێنانی سەرچاوەکانی سیستەمەکەت زیاد بکات، سیستەمەکەت خاو بکاتەوە، یان تووشی کێشەت بکات لەگەڵ پەیوەستبوون بە سێرڤەرەوە.\\nبەھای بەرزتر بەکاربھێنە ئەگەر لەو کاریگەرییانە تێدەگەیت کە لەسەر سیستەم و تۆڕەکەت دەبێت.\nsettings_use_server_last_modified_time=کاتی دوایین گۆڕانکاریی سێرڤەر بەکاربھێنە\nsettings_use_server_last_modified_time_description=کاتێک پەڕگەیەک دادەبەزێت، کاتی دوایین گۆڕانکاریی سێرڤەر بەکاربھێنە بۆ پەڕگەکە\nsettings_append_extension_to_incomplete_downloads=پاشکۆیەک زیاد بکە بۆ داونڵۆدە تەواونەبووەکان\nsettings_append_extension_to_incomplete_downloads_description=پاشکۆی \".part\" زیاد بکە بۆ داونڵۆدە تەواونەبووەکان. ئەمە یارمەتیدەر دەبێت لە جیاکردنەوەی داونڵۆدە تەواونەبووەکان و ڕێگری دەکات لە کردنەوەی پەڕگە تەواونەبووەکان.\nsettings_use_sparse_file_allocation=دابەشکردنی پەڕگە شاشەکان\nsettings_use_sparse_file_allocation_description=پەڕگەکان بە شێوەیەکی باشتر دروست بکە، بە تایبەتی لەسەر ھاردی ئێس ئێس دی، بە کەمکردنەوەی نووسینی داتای ناپێویست. ئەمە دەتوانێت خێراییی داونڵۆدەکان باشتر بکات و بەکارھێنانی بیرگە کەم بکاتەوە. ئەگەر داونڵۆدەکە بە خاوی دەستپێدەکات یان خێراییی داونڵۆدەکانت نائاسایین، باشترە کە ئەمە ناچالاک بکەیت، چونکە لەوانەیە ھەموو ئامێرێک پاڵپشتیی نەکەن.\nsettings_ignore_ssl_certificates=مۆڵەتی SSL پشتگوێ بخە\nsettings_ignore_ssl_certificates_description=سەلماندنی مۆڵەتی SSL ناچالاک بکە. تەنیا ئەگەر پێویست بوو بەکاری بھێنە، چونکە دەکرێت پەیوەندییەکەت ڕووبەڕووی کێشەی سەلامەتی بکاتەوە.\nsettings_global_speed_limiter=سنووردارکەری خێرایی سەرتاسەری\nsettings_global_speed_limiter_description=سنووری خێراییی داونڵۆدی سەرتاسەری (٠ واتە بێسنوور)\nsettings_show_average_speed=خێرایی مامناوەند پیشان بدە\nsettings_show_average_speed_description=خێراییی داونڵۆد بە مامناوەند یان بە تەواوەتی\nsettings_use_category_by_default=پۆلەکە بەکاربێنە بە شێوەی بنچینەیی\nsettings_use_category_by_default_description=ئەم پۆلە وەک پۆلی بنچینەیی بەکاربھێنە لەکاتی زیادکردنی داونڵۆد.\nsettings_default_download_folder=فۆڵدەری داونڵۆدەی بنچینەیی\nsettings_default_download_folder_description=کاتێک داونڵۆدێک زیاد دەکەیت ئەم شوێنە وەکوو شوێنی بنچینەیی بەکاردێت\nsettings_default_download_folder_describe=\"{{folder}}\" بەکاردێت\nsettings_use_proxy=پڕۆکسی بەکاربھێنە\nsettings_use_proxy_description=پڕۆکسی بەکاربھێنە بۆ داونڵۆدکردنی پەڕگەکان\nsettings_use_proxy_describe_no_proxy=ھیچ پڕۆکسییەک بەکار نایەت\nsettings_use_proxy_describe_system_proxy=پڕۆکسیی سیستەم بەکاردێت\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" بەکاردێت\nsettings_use_proxy_describe_pac_proxy=پەڕگەی PACی \"{{value}}\" بەکاربھێنە\nsettings_track_deleted_files_on_disk=شوێن پەڕگە سڕاوەکانی سەر بیرگە بکە\nsettings_track_deleted_files_on_disk_description=خۆکارانە پەڕگەکان لە پێڕستەکە بسڕەوە کاتێک لە شوێنی داونڵۆدەکەیان دەسڕێنەوە یان دەگوازرێنەوە.\nsettings_delete_partial_file_on_download_cancellation=پەڕگە ناتەواوەکان بسڕەوە لەگەڵ ھەڵوەشاندنەوەی دابەزاندنەکە\nsettings_delete_partial_file_on_download_cancellation_description=کاتێک دابەزاندنێک ھەڵدەوەشێندرێتەوە، پەڕگە دابەزاوە ناتەواوەکان لەسەر ئامێرەکە دەسطدرێنەوە. ئەمە یارمەتیدەر دەبێت لە پاکڕاگرتنی فۆڵدەری دابەزاندنەکان و کەمکردنەوەی بەکارھێنانی ناپێویستی بیرگە. بەڵام دابەزاندنەکە لە سەرەتاوە دەستپێدەکاتەوە کە دووبارە دەستت پێکردەوە.\nsettings_default_user_agent=نوێنەری بنچینەییی بەکارھێنەر\nsettings_default_user_agent_description=زنجیرەنووسەی بنچینەییی بریکاری بەکارھێنەر بنووسە بۆ دیاریکردنی شێوازی ناساندنی داواکارییەکان بۆ سێرڤەرەکان. ئەمە یارمەتیدەر دەبێت لە دەستگەیشتن بەو ناوەڕۆکانەی بۆ ئامێری دیاریکراو باشکراون یان سنووردارکردنی داونڵۆدی فریودەرانە کە لەلایەن وێبگەی دیاریکراوەوە دەسەپێندرێن.\nsettings_download_size_unit=یەکەی قەبارەی داونڵۆد\nsettings_download_size_unit_description=ئەو یەکەیەی بەکاردێت بۆ پیشاندانی قەبارەی داونڵۆد\nsettings_download_speed_unit=یەکەی خێراییی داونڵۆد\nsettings_download_speed_unit_description=ئەو یەکەیەی بەکاردێت بۆ پیشاندانی خێراییی داونڵۆد\nsettings_theme=ڕووکار\nsettings_theme_description=شێوازێک بۆ بەرنامەکە دیاری بکە\nsettings_default_dark_theme=ڕووکاری تاریک وەک بنچینەیی\nsettings_default_dark_theme_description=کاتێک کار دەکات کە بەرنامەکە شوێن شێوازی سیستەم دەکەوێت و ڕووکاری تاریک چالاکە\nsettings_default_light_theme=ڕووکاری ڕووناک وەک بنچینەیی\nsettings_default_light_theme_description=کاتێک کار دەکات کە بەرنامەکە شوێن شێوازی سیستەم دەکەوێت و ڕووکاری ڕووناک چالاکە\nsettings_font=فۆنت\nsettings_font_description=ئەو فۆنتە بگۆڕە کە لە ڕووکاری بەرنامەکەدا بەکاردێت، ھەندێک فۆنت لەوانەیە بە دروستی پیشاننەدرێن لە بەرنامەکەدا.\nsettings_ui_scale=ئەندازەی ڕووکار\nsettings_ui_scale_description=قەبارەی بەشەکانی ڕووکاری بەرنامەکە بگۆڕە\nsettings_language=زمان\nsettings_compact_top_bar=تووڵی سەرەوەی پەستێنراو\nsettings_compact_top_bar_description=تووڵی سەرەوە و تووڵی ناونیشان بکە بە یەک کاتێک پەنجەرەی سەرەکی پانیی تەواوی ھەیە\nsettings_use_native_menu_bar=تووڵی پێڕستی خۆماڵی بەکاربێنە\nsettings_use_native_menu_bar_description=شێوازی تووڵی پێڕستی بنچینەییی سیستەم بەکاربێنە\nsettings_use_relative_date_time=کات/ڕێکەوتی ڕێژەیی بەکاربێنە\nsettings_use_relative_date_time_description=شێوازی ڕێکەوت/کاتی ڕێژەیی بەکاربھێنە لە بەرنامەکەدا (بۆ نموونە \"٢ ڕۆژ پێش ئێستا\" لەجیاتیی ڕێکەوت/کاتی تەواو)\nsettings_show_icon_labels=نووسینی ئایکۆنەکان پیشان بدە\nsettings_show_icon_labels_description=ناو لەژێر ئایکۆنەکان پیشان بدە کە تواندرا (وەک کردارەکانی تووڵامرازی ماڵەوە)\nsettings_use_system_tray=سندووقی سیستەم بەکاربھێنە\nsettings_use_system_tray_description=ئایکۆنی سندووقی سیستەم پیشان بدە کاتێک بەرنامەکە کارایە\nsettings_start_on_boot=لەگەڵ ھەڵبووندا دەستپێبکە\nsettings_start_on_boot_description=خۆکارانە بەرنامەکە بکەرەوە کاتێک بەکارھێنەر سیستەمەکەی کارپێدەکات\nsettings_notification_sound=دەنگی ئاگادارکەرەوە\nsettings_notification_sound_description=دەنگێک لێبدە لەکاتی ئاگاداریی نوێ\nsettings_browser_integration=یەکخستنی وێبگەڕ\nsettings_browser_integration_description=داونڵۆد لە وێبگەڕەوە قبوڵ بکە\nsettings_browser_integration_server_port=پۆڕتی سێرڤەر\nsettings_browser_integration_server_port_description=پۆرت بۆ یەکخستنی وێبگەڕ\nsettings_browser_integration_server_port_describe=بەرنامەکە گوێ بۆ پۆرتی {{port}} دەگرێت\nsettings_dynamic_part_creation=دروستکەری بەشی بزۆک\nsettings_dynamic_part_creation_description=کاتێک بەشێک تەواو دەبێت بەشێکی تر دروست بکە بە لەتکردنی بەشەکانی تر بۆ باشترکردنی خێراییی داونڵۆد\nsettings_show_completion_dialog=دیالۆگی تەواوبوونی داونڵۆد پیشان بدە\nsettings_show_completion_dialog_description=خۆکارانە دیالۆگی \"داونڵۆد تەواو بوو\" پیشان بدە کە داونڵۆدێک تەواو بوو.\nsettings_show_download_progress_dialog=دیالۆگی بەرەوپێشچوونی داونڵۆد پیشان بدە\nsettings_show_download_progress_dialog_description=خۆکارانە دیالۆگی \"بەرەوپێشچوونی داونڵۆد\" پیشان بدە کە داونڵۆدێک دەستی پێکرد.\nsettings_per_host_settings=ڕێکخستنی ڕاژەی تایبەتمەند\nsettings_per_host_settings_descriptions=ئەم ڕێکخستنە بە خۆکاری جێبەجێ دەکرێت بۆ هەر داگرتنێکی نوێ کە هاوشێوەی ڕاژەکە بن.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=سنووری خێرایی\ndownload_item_settings_speed_limit_description=خێراییی داونڵۆد سنووردار بکە بۆ ئەم شتە\ndownload_item_settings_show_download_completion_dialog=دیالۆگی تەواوبوونی داونڵۆد پیشان بدە\ndownload_item_settings_show_download_completion_dialog_description=خۆکارانە دیالۆگی \"داونڵۆد تەواو بوو\" پیشان بدە کە ئەم داونڵۆدە تەواو بوو.\ndownload_item_settings_shutdown_on_completion=سیستەمەکە بکوژێنەوە لەگەڵ تەواوبوون\ndownload_item_settings_shutdown_on_completion_description=خۆکارانە سیستەمەکە بکوژێنەوە کە ئەم دابەزاندنە تەواو بوو.\ndownload_item_settings_thread_count=ژمارەی تاڵەکان\ndownload_item_settings_thread_count_description=چەن تاڵ بەکار بێت بۆ ئەم داونڵۆدە (٠ بۆ بنچینەیی)\ndownload_item_settings_thread_count_describe={{count}} تاڵ بۆ ئەم داونڵۆدە\ndownload_item_settings_username_description=ناوی بەکارھێنەرێک دابین بکە ئەگەر بەستەرەکە پارێزراوە\ndownload_item_settings_password_description=تێپەڕوشەیەک دابین بکە ئەگەر بەستەرەکە پارێزراوە\ndownload_item_settings_download_page=پەڕەی داونڵۆد\ndownload_item_settings_download_page_description=ئەو وێبگەیەی ئەم داونڵۆدەی لێوە دەستپێکرا\ndownload_item_settings_file_checksum=چێکسەمی پەڕگە\ndownload_item_settings_file_checksum_description=زنجیرەنووسەی وردکردن کە دەتواندرێت بەکاربێت بۆ زانینی ئەوەی کە پەڕگەکە بە دروستی دابەزیوە\ndownload_item_settings_user_agent=تایبەت\ndownload_item_settings_user_agent_description=ڕێکخستنی تایبەتی بەکارهێنەر بۆ ئەمە (بە بەتاڵی جێی بهێڵە بۆ بنەڕەتی)\nfile_checksum=چێکسەمی پەڕگە\nfile_checksum_page=پشکنەری چێکسەمی پەڕگە\nfile_checksum_page_file_checksum_default_algorithm=ئالگۆریتمی بنچینەیی\nfile_checksum_page_file_checksum_default_algorithm_help=ئالگۆریتمی بنچینەیی کە بەکاردێت بۆ ژمێرکاریی چێکسەمی پەڕگە کاتێک دابین نەکرابن.\nstart=دەستپێکردن\ncalculated_checksum=چێکسەمی ژمێرکاریکراو\nsaved_checksum=چێکسەمی پاشەکەوتکراو\nchecksum_algorithm=ئالگۆریتم\nfile_not_found=پەڕگە نەدۆزرایەوە\ndownload_not_finished=داونڵۆدەکە تەواو نەبووە\ndone=تەواوبوو\nwaiting=چاوەڕێبە\nmatches=ھاوتایە\nnot_matches=ھاوتا نییە\ncopy_to_clipboard=لەبەری بگرەوە بۆ کلیپبۆرد\nusername=ناوی بەکارھێنەر\npassword=تێپەڕوشە\naverage_speed=خێرایی مامناوەند\nexact_speed=خێراییی تەواو\nunlimited=بێسنوور\nuse_global_settings=ڕێکخستنە سەرتاسەرییەکان بەکاربھێنە\ncant_run_browser_integration=ناتواندرێت یەکخستنی وێبگەڕ بەکارببرێت\ncant_open_file=ناتواندرێت پەڕگەکە بکرێتەوە\ncant_open_folder=ناتواندرێت فۆڵدەرەکە بکرێتەوە\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} ساك\nrelative_time_long_months={{months}} مانگ\nrelative_time_long_days={{days}} ڕۆژ\nrelative_time_long_hours={{hours}} کاتژمێر\nrelative_time_long_minutes={{minutes}} خولەک\nrelative_time_long_seconds={{seconds}} چرکە\nrelative_time_short_years={{years}} س\nrelative_time_short_months={{months}} م\nrelative_time_short_days={{days}} ڕ\nrelative_time_short_hours={{hours}} ک\nrelative_time_short_minutes={{minutes}} خ\nrelative_time_short_seconds={{seconds}} چ\nrelative_time_left={{time}}ی ماوە\nrelative_time_ago={{time}} پێش ئێستا\nauto=خۆکارانە\nunspecified=نادیار\ncustom=ڕاسپێردراو\nicon=ئایکۆن\nauthor=دانەر\nlink=بەستەر\nsize=قەبارە\nstatus=دۆخ\nparts_info_downloaded_size=دابەزێندراو\nparts_info_total_size=سەرجەم\nspeed=خێرایی\ntime_left=کاتی ماوە\ndate_added=ڕێکەوتی زیادکردن\ninfo=زانیاری\ndownload_page_downloaded_size=دابەزێندراو\ndownload_page_download_completed=داونڵۆدەکە تەواو بوو\nresume_support=پاڵپشتیی بەردەوامبوون دەکات\nyes=بەڵێ\nno=نەخێر\nparts_info=زانیاریی پارچەکان\ndisconnected=دابڕا\nreceiving_data=داتا وەردەگرێت\nconnecting=پەیوەندی بەستن\nwarning=ئاگاداری\nunsupported_resume_warning=ئەم داونڵۆدە پاڵپشتیی بەردەوامبوون ناکات\\! دوایی لەوانەیە پێویست بکات لە سەرەتاوە دەستی پێبکەیتەوە لە پێڕستی داونڵۆد\nstop_anyway=ھەر چۆنێک بێت ڕایبگرە\ncustomize_columns=ستوونەکان ڕێکبخە\nreset=ڕێکخستنەوە\nmonday=دووشەممە\ntuesday=سێشەممە\nwednesday=چوارشەممە\nthursday=پێنجشەممە\nfriday=ھەینی\nsaturday=شەممە\nsunday=یەکشەممە\nproxy_open_system_proxy_settings=ڕێکخستنەکانی پڕۆکسیی سیستەم بکەرەوە\nproxy_type=جۆری پڕۆکسی\nproxy_do_not_use_proxy_for=پڕۆکسی بەکار مەھێنە بۆ\nproxy_do_not_use_proxy_for_description=پێڕستێک لەو بەستەرانەی لەوانەیە پڕۆکسی نەکرێن\\nدەتوانیت * بەکاربێنیت وەک کارەکتەری جێگرەوە\\nبۆ نموونە 192.168.1.* example.com (بە بۆشایی جیاکراونەتەوە)\nproxy_change_title=پڕۆکسی بگۆڕە\nchange_proxy=پڕۆکسی بگۆڕە\nproxy_no=بێ پڕۆکسی\nproxy_system=پڕۆکسیی سیستەم\nproxy_manual=پڕۆکسیی دەستی\nproxy_pac=شێوەپێدانی خۆکاری پڕۆکسی\nproxy_pac_url=بەستەری شێوەپێدانی خۆکارانەی پڕۆکسی\naddress=ناونیشان\nport=پۆرت\naddress_and_port=ناونیشان و پۆرت\nuse_authentication=سەلماندن بەکاربھێنە\nwarning_you_may_have_to_restart_the_download_later=لەوانەیە دواتر پێویست بکات داونڵۆدەکە لە سەرەتاوە دەستپێبکەیتەوە\\!\nedit_download_title=داونڵۆد دەستکاری بکە\nedit_download_update_from_download_page=وەشانی نوێ لە پەڕەی داونڵۆدەوە بەدەست بھێنە\nedit_download_update_from_download_page_description=کاتێک ئەم پەنجەرەیە کراوەیە، دەتوانیت بچیتە سەر پەڕەی داونڵۆد و کلیک لەسەر دوگمەی داونڵۆد بکەیت. بەرنامەکە باوەڕنامە نوێکەی داونڵۆدەکە دەگرێت و نوێی دەکاتەوە تاکوو بتوانیت پاشەکەوتی بکەیت.\nedit_download_saved_download_item_size_not_match=داونڵۆدە پاشەکەوتکراوەکە قەبارەی {{currentSize}} ھەیە، کە ھاوتای قەبارە نوێکە نییە کە {{newSize}}ە.\ntranslators_page_thanks=سپاس و پێزانین بۆ ئەوانەی یارمەتیدەر بوون لە وەرگێڕانی ئەم پڕۆژەیە ❤️\ntranslators=وەرگێڕەکان\nlanguage=زمان\ntranslators_contribute_title=وەرگێڕانەکان باشتر بکە\ntranslators_contribute_description=دەتەوێت یارمەتیدەر بیت لە باشترکردنی ئەم پڕۆژەیە؟ ئەگەر ئەو زمانەی دەتەوێت لێرەدا نییە یاخود پێویستی بە دەستکاریکردن ھەیە، دەتوانیت بەشدار بیت لە وەرگێڕانەکان و باشتریان بکەیت\\!\ncontribute=بەشدار بە\nmeet_the_translators=وەرگێڕەکان بناسە\nlocalized_by_translators=لەلایەن وەرگێڕەکانەوە وەرگێڕدراوە\nconfirm_exit=دەرچوون دڵنیابکەرەوە\nconfirm_exit_description=دڵنیایت کە دەتەوێت لە ئەی بی داونڵۆد مەنەجەر دەربچیت؟\\nداونڵۆد/ڕیزە چالاکەکان ڕادەگیرێن\\!\nupdate=نوێکردنەوە\nupdate_updater=نوێکەرەوە\nupdate_available=وەشانی نوێ بەردەستە\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=دەتوانیت بەرنامەکە نوێ بکەیتەوە بۆ دوایین وەشان بۆ چێژبینین لە تایبەتمەندی، باشترکردن، و سوودمەندیی نوێ.\nupdate_release_notes=تێبینییەکانی بڵاوبوونەوە\nupdate_check_for_update=بگەڕێ بۆ وەشانی نوێ\nupdate_checking_for_update=گەڕان بۆ وەشانی نوێ\nupdate_no_update=تۆ دوایین وەشان بەکار دەھێنیت\nupdate_check_error=ھەڵە ھەبوو لە گەڕان بۆ وەشانی نوێ\nupdate_app_updated_to_version_n=بەرنامەکە نوێکرایەوە بۆ وەشانی {{version}}\ncreate_desktop_entry=قەدبڕی دێسکتۆپ دروست بکە\nshutdown_alert=ئاگاداریی کوژاندنەوە\nsystem_shutdown_soon=سیستەمەکە بەمزووانە دەکوژێتەوە\\!\nsystem_shutdown_failed=کوژاندنەوەی سیستەمەکە شکتی ھێنا\\!\nsystem_shutdown_soon_description=سیستەمەکە بەمزووانە دەکوژێتەوە. ئەگەر ھێشتا کۆمپیوتەرەکە بەکار دەھێنیت، تکایە کارەکانت پاشەکەوت بکە یان کوژاندنەوەکە ھەڵبوەشێنەرەوە.\nsystem_shutdown_reason_queue_completed=ھەموو دابەزاندنەکانی ڕیزەکە تەواو بوون.\nsystem_shutdown_reason_queue_end_time_reached=گەیشتینە کاتی داندراوی کۆتایی بۆ ڕیزی دابەزاندن.\nsystem_shutdown_download_finished=داونڵۆدەکە تەواو بوو.\nshutdown_now=ئێستا بیکوژێنەوە\nsettings_per_host_settings_new_host=<ڕاژەی نوێ>\nsettings_per_host_settings_not_selected=سەرەتا دانەیەکی نوێ دروست بکە یان هەڵبژێرە\\!\nsettings_per_host_settings_host=ڕاژە\nsettings_per_host_settings_host_description=ئەم ڕێکخستنە جێبەجێ دەکرێت بۆ ئەو داگرتنانەی کە هەمان ناوی ڕاژەیان هەیە،. کاردەکانی پشتگیریکراون (*) (بۆ نمونە\\: example.com, *.example.com — تەنیا یەکێکیان بەکاربێنە).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/de_DE.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Downloads automatisch kategorisieren\nconfirm_auto_categorize_downloads_description=Alle nicht kategorisierten Einträge werden automatisch zu der zugehörigen Kategorie hinzugefügt.\nconfirm_reset_to_default_categories_title=Auf Standardkategorien zurücksetzen\nconfirm_reset_to_default_categories_description=dies wird alle Kategorien ENTFERNEN und stellt die Standardkategorien wieder her\\!\nconfirm_delete_download_items_title=Löschen bestätigen\nconfirm_delete_download_items_description=Sind Sie sicher, dass Sie {{count}} Einträge löschen möchten?\nconfirm_delete_download_unfinished_items_description=Sind Sie sicher, dass sie {{count}} laufende Downloads entfernen möchten?\nconfirm_delete_download_finished_and_unfinished_items_description=Möchtest du wirklich {{finishedCount}} abgeschlossene und {{unfinishedCount}} nicht abgeschlossene Downloads löschen?\nalso_delete_file_from_disk=Datei auch von der Festplatte löschen\nconfirm_delete_category_item_title=Kategorie {{name}} wird gelöscht\nconfirm_delete_category_item_description=Sind Sie sicher, dass Sie die Kategorie \"{{value}}\" löschen möchten?\nyour_download_will_not_be_deleted=Ihre Downloads werden nicht gelöscht\ndrag_the_file_to_another_app=Datei in eine andere App ziehen\ndrop_link_or_file_here=Link oder Datei hierher ziehen.\nnothing_will_be_imported=Es wird nichts importiert\nn_links_will_be_imported={{count}} Links werden importiert\nn_items_selected={{count}} Elemente ausgewählt\nwindow_close=Schließen\nwindow_minimize=Minimieren\nwindow_maximize=Maximieren\nwindow_restore=Wiederherstellen\ndelete=Löschen\nremove=Entfernen\ncancel=Abbrechen\nclose=Schließen\nmenu=Menü\nmore_options=Weitere Optionen\nok=OK\nadd=Hinzufügen\npaste=Paste\nchange=Ändern\nedit=Bearbeiten\nchange_anyway=Trotzdem ändern\ndownload=Download\nrefresh=Aktualisieren\nsettings=Einstellungen\non_completion=Nach Abschluss\nunknown=Unbekannt\nunknown_error=Unbekannter Fehler\ndownload_item_not_found=Download-Element nicht gefunden\nname=Name\ndownload_link=Downloadlink\nnot_finished=Nicht abgeschlossen\nall=Alle\nfinished=Abgeschlossen\nUnfinished=Unvollendet\ncanceled=Abgebrochen\nerror=Fehler\npaused=Pausiert\ndownloading=Wird heruntergeladen\nadded=Hinzugefügt\nidle=Ruhezustand\npreparing_file=Datei wird vorbereitet\ncreating_file=Datei wird erstellt\nresuming=Wird fortgesetzt\nretrying=Wiederhole\nlist_is_empty=Liste ist leer\\!\nsearch_in_the_list=In der Liste suchen\nsearch=Suchen\nclear=Leeren\ngeneral=Allgemein\nenabled=Aktiviert\ndisabled=Deaktiviert\ndefault=Standard\nfile=Datei\ntasks=Aufgaben\ntools=Werkzeuge\nhelp=Hilfe\nsystem=System\nall_missing_files=Alle fehlenden Dateien\nall_finished=Alle abgeschlossen\nall_unfinished=Alle unvollendet\nentire_list=Gesamte Liste\ndownload_browser_integration=Browser-Integration herunterladen\nexit=Beenden\nshow_downloads=Downloads anzeigen\nnew_download=Neuer Download\nstop_all=Alle stoppen\nimport_from_clipboard=Aus Zwischenablage importieren\nbatch_download=Batch-Download\nopen=Öffnen\nshare=Teilen\nopen_file=Datei öffnen\nopen_folder=Ordner öffnen\nresume=Fortsetzen\npause=Pause\nrestart_download=Download neu starten\ncopy=Kopieren\ncopy_link=Link kopieren\ncopy_as_curl=Kopieren als cURL\nshow_properties=Eigenschaften anzeigen\nmove_to_queue=In Warteschlange verschieben\nmove_to_this_queue=In diese Warteschlange verschieben\nmove_to_category=In Kategorie verschieben\nmove_to_this_category=In diese Kategorie verschieben\ncategories=Kategorien\nadd_category=Kategorie hinzufügen\nedit_category=Kategorie bearbeiten\ndelete_category=Kategorie löschen\ncategory_name=Kategoriename\ncategory_download_location=Kategorie Download-Verzeichnis\ncategory_download_location_description=Wenn diese Kategorie unter \"Download hinzufügen\" ausgewählt wurde, verwende dieses Verzeichnis als \"Download-Speicherort\"\ncategory_file_types=Kategorie-Dateitypen\ncategory_file_types_description=Fügen Sie automatisch Dateien dieses Typs in diese Kategorie ein (wenn Sie einen neuen Download starten). Die Endungen sind mit einem Leerzeichen getrennt (ext1 ext2...)\ncategory_url_patterns=URL-Muster\ncategory_url_patterns_description=Fügen Sie automatisch Dateien dieser Quelle in diese Kategorie ein (wenn Sie einen neuen Download starten). Die Endungen sind mit einem Leerzeichen getrennt und \"*\" können für Wildcards verwendet werden\nauto_categorize_downloads=Downloads automatisch kategorisieren\nrestore_defaults=Standard wiederherstellen\nabout=Über\nversion_n=Version {{value}}\ndeveloped_with_love_for_you=Entwickelt mit ❤️ für dich\ndonate=Spenden\nvisit_the_project_website=Projektwebseite besuchen\nthis_is_a_free_and_open_source_software=Dies ist eine freie & Open Source Software\nview_the_source_code=Quelltext ansehen\nthird_party_libraries=Drittanbieter-Bibliotheken\npowered_by_open_source_software=Unterstützt durch Open Source Software\nview_the_open_source_licenses=Open-Source-Lizenzen anzeigen\nsupport_and_community=Support & Community\ntelegram=Telegram\nchannel=Kanal\ngroup=Gruppe\nadd_download=Download hinzufügen\nadd_multi_download_page_header=Wählen sie alle Elemente aus, die sie Herunterladen möchten\nsave_to=Speichern unter\nwhere_should_each_item_saved=Wo soll jedes Element gespeichert werden?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Es sind multiple Objekte existent\\! Wählen sie einen weg diese zu speichern\neach_item_on_its_own_category=Jede Eintrag in seiner eigenen Kategorie\neach_item_on_its_own_category_description=Jeder Eintrag wird in einer Kategorie mit diesem Dateityp platziert\nall_items_in_one_category=Alle Einträge in einer Kategorie\nall_items_in_one_category_description=Alle Dateien werden in der gewählten Kategorie gespeichert\nall_items_in_one_Location=Alle Einträge an einem Ort\nall_items_in_one_Location_description=Alle Einträge werden im ausgewählten Verzeichnis gespeichert\nunselected_all_items_in_specific_location_description=Alle Dateien werden im Verzeichnis der ausgewählten Kategorie gespeichert\nno_category_selected=Keine Kategorie ausgewählt\nno_categories_found=Keine Kategorien gefunden\ndownload_location=Downloadverzeichnis\nlocation=Dateipfad\nselect_queue=Warteschlange auswählen\nwithout_queue=Ohne Warteschlange\nuse_category=Kategorie verwenden\ncant_write_to_this_folder=In diesen Ordner kann nicht geschrieben werden\nfile_name_already_exists=Dateiname existiert bereits\ndownload_already_exists=Download bereits vorhanden\ninvalid_file_name=Ungültiger Dateiname\nshow_solutions=Lösungen anzeigen...\nchange_solution=Lösung ändern\nselect_a_solution=Eine Lösung auswählen\nselect_download_strategy_description=Der von Ihnen angegebene Link ist bereits in den Downloadlisten. Bitte geben Sie an, was Sie tun möchten\ndownload_strategy_add_a_numbered_file=Datei nummerieren\ndownload_strategy_add_a_numbered_file_description=Index am Ende des Dateinamens hinzufügen\ndownload_strategy_override_existing_file=Vorhandene Datei überschreiben\ndownload_strategy_override_existing_file_description=Bestehenden Download entfernen und in diese Datei schreiben\ndownload_strategy_update_download_link=Vorhandenen Download aktualisieren\ndownload_strategy_update_download_link_description=Vorhandenen Download-Link und die zugehörigen Anmeldedaten aktualisieren\ndownload_strategy_show_downloaded_file=Heruntergeladene Datei anzeigen\ndownload_strategy_show_downloaded_file_description=Zeige bereits vorhandene Download-Elemente an, damit du auf \"Fortsetzen\" klicken oder sie öffnen kannst.\nbatch_download_link_help=Gib einen Link ein, der Platzhalter enthält (verwende *)\ninvalid_url=Ungültige URL\nlist_is_too_large_maximum_n_items_allowed=Liste ist zu groß\\! Maximal {{count}} Einträge erlaubt\nenter_range=Bereich eingeben\nrange_from=Von\nrange_to=Bis\nbatch_download_wildcard_length=Platzhalterlänge\nfirst_link=Erster Link\nlast_link=Letzter Link\nopen_source_software_used_in_this_app=In dieser App verwendete Open-Source-Software\nlinks=Links\nwebsite=Webseite\ndevelopers=Entwickler\nsource_code=Quelltext\nlicense=Lizenz\nno_license_found=Keine Lizenz gefunden\norganization=Organisation\nadd_new_queue=Neue Warteschlange hinzufügen\nqueue_name=Name der Warteschlange\nqueues=Warteschlangen\nstop_queue=Warteschlange stoppen\nstart_queue=Warteschlange starten\nclear_queue_items=Leere Warteschlange\nconfig=Konfiguration\nitems=Elemente\nmove_down=Nach unten\nmove_up=Nach oben\nremove_queue=Warteschlange entfernen\nqueue_name_help=Namen für diese Warteschlange festlegen\nqueue_name_describe=Name der Warteschlange ist {{value}}\nqueue_max_concurrent_download=Max. gleichzeitiger Downloads\nqueue_max_concurrent_download_description=Max. Download für diese Warteschlange\nqueue_automatic_stop=Automatischer Stopp\nqueue_automatic_stop_description=Die Warteschlange wird automatisch beendet, wenn kein Element mehr vorhanden ist\nqueue_scheduler=Planer\nqueue_enable_scheduler=Planer aktivieren\nqueue_active_days=Aktive Tage\nqueue_active_days_description=An welchen Tagen soll der Planer funktionieren?\nqueue_scheduler_enable_auto_start_time=Auto-Startzeit aktivieren\nqueue_scheduler_auto_start_time=Auto-Startzeit\nqueue_scheduler_enable_auto_stop_time=Auto-Stoppzeit aktivieren\nqueue_scheduler_auto_stop_time=Auto-Stoppzeit\nqueue_shutdown_on_completion=System nach Abschluss Herunterfahren\nqueue_shutdown_on_completion_description=Das System automatisch herunterfahren, wenn diese Warteschlange abgeschlossen ist oder wenn die geplante Endzeit erreicht ist.\nappearance=Aussehen\ndownload_engine=Download-Engine\nbrowser_integration=Browserintegration\nsettings_download_max_retries_count=Maximale Download-Wiederholungen\nsettings_download_max_retries_count_description=Die maximale Anzahl von Versuchen, die die App unternimmt, um einen fehlgeschlagenen Download erneut durchzuführen, bevor sie aufgibt\nsettings_download_max_retries_count_describe_no_retries=Fehlgeschlagene Downloads werden nicht erneut versucht\nsettings_download_max_retries_count_describe_n_retries=Fehlgeschlagene Downloads werden {{count}} mal wiederholt\nsettings_download_thread_count=Thread-Anzahl\nsettings_download_thread_count_description=Maximale Download-Threads pro Eintrag\nsettings_download_thread_count_describe=Ein Download kann bis zu {{count}} Threads haben\nsettings_download_thread_count_with_large_value_describe=Warnung\\: Das Festlegen einer hohen Thread-Anzahl kann die Systemressourcen stärker beanspruchen, die Leistung verringern oder Verbindungsprobleme mit Servern verursachen. Verwende höhere Werte nur, wenn du die möglichen Auswirkungen auf dein System und Netzwerk kennst.\nsettings_use_server_last_modified_time=Verwende das Datum der letzten Änderung des Servers\nsettings_use_server_last_modified_time_description=Verwende beim Herunterladen einer Datei das Datum der letzten Änderung des Servers für die lokale Datei\nsettings_append_extension_to_incomplete_downloads=Erweiterung an unvollständige Downloads anhängen\nsettings_append_extension_to_incomplete_downloads_description=Hängt die Dateierweiterung \".part\" an unvollständige Downloads an. Dies hilft dabei, unvollständige Downloads zu identifizieren und verhindert das versehentliche Öffnen unvollständiger Dateien.\nsettings_use_sparse_file_allocation=Sparse-Datei-Zuweisung\nsettings_use_sparse_file_allocation_description=Erstelle Dateien effizienter, insbesondere auf SSDs, indem unnötiges Schreiben von Daten reduziert wird. Dies kann den Start von Downloads beschleunigen und den Speicherplatzbedarf verringern. Wenn Downloads langsam starten oder ungewöhnliche Downloadgeschwindigkeiten auftreten, solltest du erwägen, diese Option zu deaktivieren, da sie möglicherweise auf einigen Geräten nicht vollständig unterstützt wird.\nsettings_ignore_ssl_certificates=SSL-Zertifikat ignorieren\nsettings_ignore_ssl_certificates_description=Deaktiviert die Überprüfung von SSL-Zertifikaten. Nur bei Bedarf verwenden, da dies Ihre Verbindung Sicherheitsrisiken aussetzen kann.\nsettings_global_speed_limiter=Globale Geschwindigkeitsbegrenzung\nsettings_global_speed_limiter_description=Globales Limit für Downloadgeschwindigkeit (0 bedeutet unbegrenzt)\nsettings_show_average_speed=Durchschnittsgeschwindigkeit anzeigen\nsettings_show_average_speed_description=Durchschnittsgeschwindigkeit oder reale Geschwindigkeit des Downloads\nsettings_use_category_by_default=Kategorie standardmäßig nutzen\nsettings_use_category_by_default_description=Kategorie standardmäßig beim Hinzufügen eines Downloads verwenden.\nsettings_default_download_folder=Standard-Downloadordner\nsettings_default_download_folder_description=Wenn Sie einen neuen Download hinzufügen, wird dieser Ordner standardmäßig verwendet\nsettings_default_download_folder_describe=\"{{folder}}\" wird verwendet\nsettings_use_proxy=Proxy verwenden\nsettings_use_proxy_description=Proxy für das Herunterladen von Dateien verwenden\nsettings_use_proxy_describe_no_proxy=Es wird kein Proxy verwendet\nsettings_use_proxy_describe_system_proxy=System-Proxy wird verwendet\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" wird verwendet\nsettings_use_proxy_describe_pac_proxy=Die PAC-Datei \"{{value}}\" wird verwendet\nsettings_track_deleted_files_on_disk=Gelöschte Dateien auf der Festplatte verfolgen\nsettings_track_deleted_files_on_disk_description=Entferne automatisch alle Elemente aus der Liste, welche gelöscht wurden oder sich nicht mehr im Download-Verzeichnis befinden.\nsettings_delete_partial_file_on_download_cancellation=Unvollständige Datei bei Abbruch eines Downloads löschen\nsettings_delete_partial_file_on_download_cancellation_description=Wenn ein Download abgebrochen wird, wird die unvollständig heruntergeladene Datei von der Festplatte gelöscht. Dadurch bleibt der Download-Ordner übersichtlich und unnötiger Speicherplatz wird nicht belegt. Allerdings wird der Download beim nächsten Start wieder von vorne beginnen.\nsettings_default_user_agent=Standard-Benutzeragent\nsettings_default_user_agent_description=Geben Sie den Standard User-Agent String an, um zu bestimmen, wie sich Anfragen gegenüber Servern identifizieren. Dies kann beim Zugriff auf für bestimmte Geräte optimierte Inhalte oder bei der Umgehung von Download-Beschränkungen bestimmter Websites hilfreich sein.\nsettings_download_size_unit=Download-Größeneinheit\nsettings_download_size_unit_description=Einheit zum Anzeigen der Downloadgröße\nsettings_download_speed_unit=Download-Geschwindigkeitseinheit\nsettings_download_speed_unit_description=Einheit zum Anzeigen der Downloadgeschwindigkeit\nsettings_theme=Thema\nsettings_theme_description=Wählen Sie ein Thema für die App\nsettings_default_dark_theme=Dunkles Standarddesign\nsettings_default_dark_theme_description=Gilt, wenn die App dem Systemdesign folgt und der Dunkelmodus aktiv ist\nsettings_default_light_theme=Helles Standarddesign\nsettings_default_light_theme_description=Gilt, wenn die App dem Systemdesign folgt und der helle Modus aktiv ist\nsettings_font=Schriftart\nsettings_font_description=Ändert die Schriftart, die in der App-Oberfläche verwendet wird. Einige Schriftarten werden in der App möglicherweise nicht korrekt angezeigt.\nsettings_ui_scale=UI Skalierung\nsettings_ui_scale_description=Die Größe der Oberflächenelemente der App anpassen\nsettings_language=Sprache\nsettings_compact_top_bar=Kompakte obere Leiste\nsettings_compact_top_bar_description=Die obere Leiste mit der Titelleiste zusammenführen, wenn das Hauptfenster breit genug ist\nsettings_use_native_menu_bar=Native Menüleiste verwenden\nsettings_use_native_menu_bar_description=Standard-Stil der Menüleiste verwenden\nsettings_use_relative_date_time=Verwende relatives Datums-/Zeitformat\nsettings_use_relative_date_time_description=Verwende in der App ein relatives Datums-/Zeitformat (z. B. „vor 2 Tagen“ statt des genauen Datums/der genauen Uhrzeit)\nsettings_show_icon_labels=Symbolbeschriftungen anzeigen\nsettings_show_icon_labels_description=Zeige wenn möglich Beschriftungen unter Symbolen an (z. B. bei Aktionen der Menüleiste)\nsettings_use_system_tray=Systemablage verwenden\nsettings_use_system_tray_description=Systemablage-Icon anzeigen, wenn die App läuft\nsettings_start_on_boot=Beim Systemstart ausführen\nsettings_start_on_boot_description=Anwendung mit Benutzeranmeldung automatisch starten\nsettings_notification_sound=Benachrichtigungston\nsettings_notification_sound_description=Ton bei neuer Benachrichtigung abspielen\nsettings_browser_integration=Browserintegration\nsettings_browser_integration_description=Downloads von Browsern akzeptieren\nsettings_browser_integration_server_port=Server-Port\nsettings_browser_integration_server_port_description=Port für Browserintegration\nsettings_browser_integration_server_port_describe=App wird auf Port {{port}} hören\nsettings_dynamic_part_creation=Dynamische Teileerstellung\nsettings_dynamic_part_creation_description=Wenn ein Teil fertiggestellt ist, einen weiteren Teil erstellen, indem andere Teile aufgeteilt werden, um die Downloadgeschwindigkeit zu verbessern\nsettings_show_completion_dialog=Dialog zur Download-Fertigstellung anzeigen\nsettings_show_completion_dialog_description=Automatisch den 'Download-Fertigstellung'-Dialog anzeigen, wenn ein Download abgeschlossen ist.\nsettings_show_download_progress_dialog=Dialog zum Download-Fortschritt anzeigen\nsettings_show_download_progress_dialog_description=Automatisch den \"Download-Fortschritt\"-Dialog anzeigen, wenn ein Download gestartet wurde.\nsettings_per_host_settings=Pro Host-Einstellungen\nsettings_per_host_settings_descriptions=Diese Einstellungen werden automatisch auf alle neuen Downloads angewendet, die dem angegebenen Host entsprechen.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Geschwindigkeitslimit\ndownload_item_settings_speed_limit_description=Download-Geschwindigkeit für diesen Eintrag begrenzen\ndownload_item_settings_show_download_completion_dialog=Dialog zur Download-Fertigstellung anzeigen\ndownload_item_settings_show_download_completion_dialog_description=Automatisch den \"Download-Fertigstellung\"-Dialog anzeigen, wenn dieser Download abgeschlossen ist.\ndownload_item_settings_shutdown_on_completion=System nach Abschluss Herunterfahren\ndownload_item_settings_shutdown_on_completion_description=Das System automatisch herunterfahren, wenn der Download abgeschlossen ist.\ndownload_item_settings_thread_count=Anzahl der Threads\ndownload_item_settings_thread_count_description=Wie viel Threads zum Herunterladen dieses Eintrags verwendet wurde (0 für Standard)\ndownload_item_settings_thread_count_describe={{count}} Threads für diesen Download\ndownload_item_settings_username_description=Geben Sie einen Benutzernamen an, wenn der Link eine geschützte Ressource ist\ndownload_item_settings_password_description=Geben Sie ein Passwort an, wenn der Link eine geschützte Ressource ist\ndownload_item_settings_download_page=Downloadseite\ndownload_item_settings_download_page_description=Die Webseite, von der dieser Download gestartet wurde\ndownload_item_settings_file_checksum=Datei-Prüfsumme\ndownload_item_settings_file_checksum_description=Eine Hash-Zeichenkette, die verwendet werden kann, um zu überprüfen, ob eine Datei korrekt heruntergeladen wurde\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Benutzerdefinierter User-Agent für diesen Download (leer lassen, um die Standardeinstellung zu verwenden)\nfile_checksum=Datei-Prüfsumme\nfile_checksum_page=Datei-Prüfsummen-Prüfer\nfile_checksum_page_file_checksum_default_algorithm=Standardalgorithmus\nfile_checksum_page_file_checksum_default_algorithm_help=Der Standardalgorithmus, der zur Berechnung von Datei-Prüfsummen verwendet wird, wenn diese nicht angegeben werden.\nstart=Starten\ncalculated_checksum=Berechnete Prüfsumme\nsaved_checksum=Gespeicherte Prüfsumme\nchecksum_algorithm=Algorithmus\nfile_not_found=Datei nicht gefunden\ndownload_not_finished=Download nicht abgeschlossen\ndone=Erledigt\nwaiting=Warten\nmatches=Treffer\nnot_matches=Keine Übereinstimmungen\ncopy_to_clipboard=In die Zwischenablage kopieren\nusername=Benutzername\npassword=Passwort\naverage_speed=Durchschnittliche Geschwindigkeit\nexact_speed=Exakte Geschwindigkeit\nunlimited=Unlimitiert\nuse_global_settings=Globale Einstellungen verwenden\ncant_run_browser_integration=Browserintegration kann nicht ausgeführt werden\ncant_open_file=Datei kann nicht geöffnet werden\ncant_open_folder=Ordner kann nicht geöffnet werden\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} Jahre\nrelative_time_long_months={{months}} Monate\nrelative_time_long_days={{days}} Tage\nrelative_time_long_hours={{hours}} Stunden\nrelative_time_long_minutes={{minutes}} Minuten\nrelative_time_long_seconds={{seconds}} Sekunden\nrelative_time_short_years={{years}} j\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} t\nrelative_time_short_hours={{hours}} Std\nrelative_time_short_minutes={{minutes}} Min.\nrelative_time_short_seconds={{seconds}} Sek\nrelative_time_left={{time}} verbleibend\nrelative_time_ago={{time}} her\nauto=Automatisch\nunspecified=Nicht angegeben\ncustom=Benutzerdefiniert\nicon=Symbol\nauthor=Autor\nlink=Link\nsize=Größe\nstatus=Status\nparts_info_downloaded_size=Heruntergeladen\nparts_info_total_size=Gesamt\nspeed=Geschwindigkeit\ntime_left=Verbleibende Zeit\ndate_added=Datum hinzugefügt\ninfo=Info\ndownload_page_downloaded_size=Heruntergeladen\ndownload_page_download_completed=Download abgeschlossen\nresume_support=Unterstützung fortsetzen\nyes=Ja\nno=Nein\nparts_info=Teileinfo\ndisconnected=Getrennt\nreceiving_data=Daten werden empfangen\nconnecting=Verbinde\nwarning=Warnung\nunsupported_resume_warning=Dieser Download unterstützt keine Fortsetzung\\! Möglicherweise musst du ihn später in der Download-Liste neu starten\nstop_anyway=Trotzdem stoppen\ncustomize_columns=Spalten anpassen\nreset=Zurücksetzen\nmonday=Montag\ntuesday=Dienstag\nwednesday=Mittwoch\nthursday=Donnerstag\nfriday=Freitag\nsaturday=Samstag\nsunday=Sonntag\nproxy_open_system_proxy_settings=System-Proxy-Einstellungen öffnen\nproxy_type=Proxytyp\nproxy_do_not_use_proxy_for=Proxy nicht verwenden für\nproxy_do_not_use_proxy_for_description=Eine Liste von URLs, die nicht über Proxys\\naufgerufen werden dürfen.\\nDu kannst Platzhalter mit * verwenden\\nzum Beispiel 192.168.1.* example.com (durch Leerzeichen getrennt)\nproxy_change_title=Proxy ändern\nchange_proxy=Proxy ändern\nproxy_no=Kein Proxy\nproxy_system=System-Proxy\nproxy_manual=Manueller Proxy\nproxy_pac=Proxy Auto Konfiguration\nproxy_pac_url=URL der Proxy Auto-Konfiguration\naddress=Adresse\nport=Port\naddress_and_port=Adresse & Port\nuse_authentication=Authentifizierung verwenden\nwarning_you_may_have_to_restart_the_download_later=Möglicherweise muss der Download später neu gestartet werden\\!\nedit_download_title=Download bearbeiten\nedit_download_update_from_download_page=Von Downloadseite aktualisieren\nedit_download_update_from_download_page_description=Bei geöffnetem Fenster kann die Download-Seite aufgerufen und der Download-Button betätigt werden. Die App erfasst und aktualisiert die neuen Download-Zugangsdaten, um deren Speicherung zu ermöglichen.\nedit_download_saved_download_item_size_not_match=Das gespeicherte Download-Element hat eine Größe von {{currentSize}}, die nicht mit der neuen Größe von {{newSize}} übereinstimmt.\ntranslators_page_thanks=Mit Dankbarkeit an diejenigen, die bei der Übersetzung dieses Projekts geholfen haben ❤️\ntranslators=Übersetzer\nlanguage=Sprache\ntranslators_contribute_title=Übersetzungen verbessern\ntranslators_contribute_description=Wollen Sie helfen, dieses Projekt zu verbessern? Wenn Ihre Sprache nicht aufgelistet ist oder es sind Verbesserungen nötig, dann können Sie Ihre Übersetzungen einbringen und es verbessern\\!\ncontribute=Mitwirken\nmeet_the_translators=Die Übersetzer kennenlernen\nlocalized_by_translators=Lokalisiert von Übersetzern\nconfirm_exit=Beenden bestätigen\nconfirm_exit_description=Sind Sie sicher, dass Sie AB Download Manager beenden möchten?\\nAktive Downloads/Warteschlangen werden gestoppt\\!\nupdate=aktualisieren\nupdate_updater=Updater\nupdate_available=Neue Version verfügbar\nupdate_error=Updatefehler\nupdate_available_suggest_to_to_update=Ein Update auf die neueste Version ermöglicht die Nutzung neuer Funktionen, Verbesserungen und Leistungssteigerungen.\nupdate_release_notes=Versionshinweise\nupdate_check_for_update=Nach neuer Version suchen\nupdate_checking_for_update=Es wird nach einer neuen Aktualisierung gesucht\nupdate_no_update=Es wird bereits die neuste Version genutzt\nupdate_check_error=Beim Suchen einer neuen Version ist ein Fehler aufgetreten\nupdate_app_updated_to_version_n=App auf Version {{version}} aktualisiert\ncreate_desktop_entry=Desktop-Eintrag erstellen\nshutdown_alert=Warnung bei Herunterfahren\nsystem_shutdown_soon=Herunterfahren wird gestartet\\!\nsystem_shutdown_failed=Herunterfahren fehlgeschlagen\\!\nsystem_shutdown_soon_description=Das System wird bald heruntergefahren. Wenn Sie den Computer noch verwenden, speichern Sie bitte Ihre Arbeit oder brechen Sie den Vorgang ab.\nsystem_shutdown_reason_queue_completed=Alle Downloads in der Warteschlange sind abgeschlossen.\nsystem_shutdown_reason_queue_end_time_reached=Geplante Endzeit für die Download-Warteschlange erreicht.\nsystem_shutdown_download_finished=Download abgeschlossen.\nshutdown_now=Jetzt herunterfahren\nsettings_per_host_settings_new_host=<Neuer Host>\nsettings_per_host_settings_not_selected=Erstellen oder wählen Sie zuerst ein neues Element\\!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=Diese Einstellungen werden auf Downloads angewendet, die diesem Hostnamen entsprechen. Wildcards (*) werden unterstützt (z. B. example.com, *.example.com – bitte nur eine verwenden).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sortieren nach\nwelcome=Willkommen\nnew_folder=Neuer Ordner\nskip=Überspringen\nlets_go=Los geht's\nnext=Weiter\nselect_all=Alle auswählen\nselect_inside=Innen auswählen\nselect_invert=Auswahl umkehren\nopen_settings=Einstellungen öffnen\nback=Zurück\nservice_is_running=Dienst wird ausgeführt\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=Sie können diese Einstellungen jederzeit später ändern\npermission_granted=Berechtigung gewährt\npermission_not_granted=Berechtigung verweigert\npermissions=Berechtigungen\ngive_permission=Zugriff erlauben\ngive_storage_permission=Speicherzugriff erlauben\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/en_US.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Auto categorize downloads\nconfirm_auto_categorize_downloads_description=Any uncategorized item will be automatically added to its related category.\nconfirm_reset_to_default_categories_title=Reset to Default Categories\nconfirm_reset_to_default_categories_description=This will REMOVE all categories and brings backs default categories\\!\nconfirm_delete_download_items_title=Confirm Delete\nconfirm_delete_download_items_description=Are you sure you want to delete {{count}} items?\nconfirm_delete_download_unfinished_items_description=Are you sure you want to delete {{count}} unfinished downloads?\nconfirm_delete_download_finished_and_unfinished_items_description=Are you sure you want to delete {{finishedCount}} finished and {{unfinishedCount}} unfinished downloads?\nalso_delete_file_from_disk=Also delete file from disk\nconfirm_delete_category_item_title=Removing {{name}} category\nconfirm_delete_category_item_description=Are you sure you want to delete \"{{value}}\" Category?\nyour_download_will_not_be_deleted=Your downloads won't be deleted\ndrag_the_file_to_another_app=Drag the file to another app\ndrop_link_or_file_here=Drop link or file here.\nnothing_will_be_imported=Nothing will be imported\nn_links_will_be_imported={{count}} links will be imported\nn_items_selected={{count}} items selected\nwindow_close=Close\nwindow_minimize=Minimize\nwindow_maximize=Maximize\nwindow_restore=Restore\ndelete=Delete\nremove=Remove\ncancel=Cancel\nclose=Close\nmenu=Menu\nmore_options=More Options\nok=OK\nadd=Add\npaste=Paste\nchange=Change\nedit=Edit\nchange_anyway=Change Anyway\ndownload=Download\nrefresh=Refresh\nsettings=Settings\non_completion=On Completion\nunknown=Unknown\nunknown_error=Unknown Error\ndownload_item_not_found=Download item not found\nname=Name\ndownload_link=Download link\nnot_finished=Not finished\nall=All\nfinished=Finished\nUnfinished=Unfinished\ncanceled=Canceled\nerror=Error\npaused=Paused\ndownloading=Downloading\nadded=Added\nidle=IDLE\npreparing_file=Preparing File\ncreating_file=Creating File\nresuming=Resuming\nretrying=Retrying\nlist_is_empty=List is empty!\nsearch_in_the_list=Search in the List\nsearch=Search\nclear=Clear\ngeneral=General\nenabled=Enabled\ndisabled=Disabled\ndefault=Default\nfile=File\ntasks=Tasks\ntools=Tools\nhelp=Help\nsystem=System\nall_missing_files=All Missing Files\nall_finished=All Finished\nall_unfinished=All Unfinished\nentire_list=Entire List\ndownload_browser_integration=Download Browser Integration\nexit=Exit\nshow_downloads=Show Downloads\nnew_download=New Download\nstop_all=Stop All\nimport_from_clipboard=Import From Clipboard\nbatch_download=Batch Download\nopen=Open\nshare=Share\nopen_file=Open File\nopen_folder=Open Folder\nresume=Resume\npause=Pause\nrestart_download=Restart Download\ncopy=Copy\ncopy_link=Copy link\ncopy_as_curl=Copy as cURL\nshow_properties=Show Properties\nmove_to_queue=Move To Queue\nmove_to_this_queue=Move to this Queue\nmove_to_category=Move To Category\nmove_to_this_category=Move to this category\ncategories=Categories\nadd_category=Add Category\nedit_category=Edit Category\ndelete_category=Delete Category\ncategory_name=Category Name\ncategory_download_location=Category Download Location\ncategory_download_location_description=When this category chosen in \"Add Download\" use this directory as \"Download Location\"\ncategory_file_types=Category file types\ncategory_file_types_description=Automatically put these file types to this category. (when you add new download)\\nSeparate file extensions with space (ext1 ext2 ...)\ncategory_url_patterns=URL Patterns\ncategory_url_patterns_description=Automatically put download from these URLs to this category. (when you add new download)\\nSeparate URLs with space, you can also use * for wildcard\nauto_categorize_downloads=Auto Categorize Downloads\nrestore_defaults=Restore Defaults\nabout=About\nversion_n=Version {{value}}\ndeveloped_with_love_for_you=Developed with ❤️ for you\ndonate=Donate\nvisit_the_project_website=Visit the project website\nthis_is_a_free_and_open_source_software=This is a free & Open Source software\nview_the_source_code=See the Source Code\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=Powered by Open Source Software\nview_the_open_source_licenses=View the Open-Source licenses\nsupport_and_community=Support & Community\ntelegram=Telegram\nchannel=Channel\ngroup=Group\nadd_download=Add Download\nadd_multi_download_page_header=Select Items you want to pick up for download\nsave_to=Save To\nwhere_should_each_item_saved=Where should each item be saved?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=There are multiple items! please select a way you want to save them\neach_item_on_its_own_category=Each item on its own category\neach_item_on_its_own_category_description=Each item will be placed in a category that have that file type\nall_items_in_one_category=All items in one Category\nall_items_in_one_category_description=All files will be saved in the selected category\nall_items_in_one_Location=All items in one Location\nall_items_in_one_Location_description=All items will be saved in the selected directory\nunselected_all_items_in_specific_location_description=All files will be saved in the selected category location\nno_category_selected=No Category Selected\nno_categories_found=No Categories Found\ndownload_location=Download Location\nlocation=Location\nselect_queue=Select Queue\nwithout_queue=Without Queue\nuse_category=Use Category\ncant_write_to_this_folder=Can't write to this folder\nfile_name_already_exists=File name already exists\ndownload_already_exists=Download already exists\ninvalid_file_name=Invalid filename\nshow_solutions=Show solutions...\nchange_solution=Change solution\nselect_a_solution=Select a solution\nselect_download_strategy_description=The link you provided is already in download lists please specify what you want to do\ndownload_strategy_add_a_numbered_file=Add a numbered file\ndownload_strategy_add_a_numbered_file_description=Add an index after the end of download file name\ndownload_strategy_override_existing_file=Override existing file\ndownload_strategy_override_existing_file_description=Remove existing download and write to that file\ndownload_strategy_update_download_link=Update existing download\ndownload_strategy_update_download_link_description=Update the existing download link and its credentials\ndownload_strategy_show_downloaded_file=Show downloaded file\ndownload_strategy_show_downloaded_file_description=Show already existing download item, so you can press on resume or open it\nbatch_download_link_help=Enter a link that contains wildcards (use *)\ninvalid_url=Invalid URL\nlist_is_too_large_maximum_n_items_allowed=List is too large! maximum {{count}} items allowed\nenter_range=Enter range\nrange_from=From\nrange_to=To\nbatch_download_wildcard_length=Wildcard length\nfirst_link=First Link\nlast_link=Last Link\nopen_source_software_used_in_this_app=Open Source Software used in this App\nlinks=Links\nwebsite=Website\ndevelopers=Developers\nsource_code=Source Code\nlicense=License\nno_license_found=No license found\norganization=Organization\nadd_new_queue=Add New Queue\nqueue_name=Queue Name\nqueues=Queues\nstop_queue=Stop Queue\nstart_queue=Start Queue\nclear_queue_items=Empty Queue\nconfig=Config\nitems=Items\nmove_down=Move down\nmove_up=Move up\nremove_queue=Remove Queue\nqueue_name_help=Specify a name for this queue\nqueue_name_describe=Queue name is {{value}}\nqueue_max_concurrent_download=Max Concurrent Download\nqueue_max_concurrent_download_description=Max download for this queue\nqueue_automatic_stop=Automatic Stop\nqueue_automatic_stop_description=Automatic stop queue when there is no item in it\nqueue_scheduler=Scheduler\nqueue_enable_scheduler=Enable Scheduler\nqueue_active_days=Active Days\nqueue_active_days_description=Which days schedulers function?\nqueue_scheduler_enable_auto_start_time=Enable Auto Start Time\nqueue_scheduler_auto_start_time=Auto Start Time\nqueue_scheduler_enable_auto_stop_time=Enable Auto Stop Time\nqueue_scheduler_auto_stop_time=Auto Stop Time\nqueue_shutdown_on_completion=Shutdown System On Completion\nqueue_shutdown_on_completion_description=Automatically shutdown the system when this queue is completed. or when the scheduled end time is reached.\nappearance=Appearance\ndownload_engine=Download Engine\nbrowser_integration=Browser Integration\nsettings_download_max_retries_count=Maximum Download Retries\nsettings_download_max_retries_count_description=The maximum number of times the app will retry a failed download before giving up\nsettings_download_max_retries_count_describe_no_retries=Failed downloads won't be retried\nsettings_download_max_retries_count_describe_n_retries=Failed downloads will be retried {{count}} time(s)\nsettings_download_thread_count=Thread Count\nsettings_download_thread_count_description=Maximum download thread per download item\nsettings_download_thread_count_describe=A download can have up to {{count}} threads\nsettings_download_thread_count_with_large_value_describe=Warning: Setting a high thread count may increase system resource usage, reduce performance, or cause connection issues with servers. Use higher values only if you understand the potential impact on your system and network.\nsettings_use_server_last_modified_time=Use Server's Last-Modified Time\nsettings_use_server_last_modified_time_description=When downloading a file, use server's last modified time for the local file\nsettings_append_extension_to_incomplete_downloads=Append Extension To Incomplete Downloads\nsettings_append_extension_to_incomplete_downloads_description=Append \".part\" extension to incomplete downloads. This helps to identify unfinished downloads and prevents accidental opening of incomplete files.\nsettings_use_sparse_file_allocation=Sparse File Allocation\nsettings_use_sparse_file_allocation_description=Create files more efficiently, especially on SSDs, by reducing unnecessary data writing. This can speed up download starts and reduce disk usage. If downloads start slowly or you experience unusual download speeds, consider disabling this option, as it may not be fully supported on some devices.\nsettings_ignore_ssl_certificates=Ignore SSL Certificates\nsettings_ignore_ssl_certificates_description=Disables SSL certificate verification. Use only if necessary, as it may expose your connection to security risks.\nsettings_global_speed_limiter=Global Speed Limiter\nsettings_global_speed_limiter_description=Global download speed limit (0 means unlimited)\nsettings_show_average_speed=Show Average Speed\nsettings_show_average_speed_description=Download speed in average or precision\nsettings_use_category_by_default=Use Category By Default\nsettings_use_category_by_default_description=Use category by default when adding a download.\nsettings_default_download_folder=Default Download Folder\nsettings_default_download_folder_description=When you add new download this location is used by default\nsettings_default_download_folder_describe=\"{{folder}}\" will be used\nsettings_use_proxy=Use Proxy\nsettings_use_proxy_description=Use proxy for downloading files\nsettings_use_proxy_describe_no_proxy=No Proxy will be used\nsettings_use_proxy_describe_system_proxy=System Proxy will be used\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" will be used\nsettings_use_proxy_describe_pac_proxy=PAC file \"{{value}}\" will be used\nsettings_track_deleted_files_on_disk=Track Deleted Files On Disk\nsettings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.\nsettings_delete_partial_file_on_download_cancellation=Delete Partial File On Download Cancellation\nsettings_delete_partial_file_on_download_cancellation_description=When a download is canceled, the partially downloaded file will be deleted from the disk. This helps keep your download folder clean and reduces unnecessary disk space usage. However, the download will restart from the beginning the next time you start it.\nsettings_default_user_agent=Default User-Agent\nsettings_default_user_agent_description=Specify the Default-User Agent string to define how requests identify to servers. This can help in accessing content optimized for particular devices or in circumventing download limitations imposed by certain websites.\nsettings_download_size_unit=Download Size Unit\nsettings_download_size_unit_description=Unit used to display the download size\nsettings_download_speed_unit=Download Speed Unit\nsettings_download_speed_unit_description=Unit used to display the download speed\nsettings_theme=Theme\nsettings_theme_description=Select a theme for the App\nsettings_default_dark_theme=Default Dark Theme\nsettings_default_dark_theme_description=Applies when the app follows the system theme and dark mode is active\nsettings_default_light_theme=Default Light Theme\nsettings_default_light_theme_description=Applies when the app follows the system theme and light mode is active\nsettings_font=Font\nsettings_font_description=Change the font used in the app interface, Some fonts might not display correctly in the app.\nsettings_ui_scale=UI Scale\nsettings_ui_scale_description=Adjust the size of the app's interface elements\nsettings_language=Language\nsettings_compact_top_bar=Compact Top Bar\nsettings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width\nsettings_use_native_menu_bar=Use Native Menu Bar\nsettings_use_native_menu_bar_description=Use the system's default menu bar style\nsettings_use_relative_date_time=Use relative date/time\nsettings_use_relative_date_time_description=Use relative date/time format for dates in the app (e.g., \"2 days ago\" instead of the exact date/time)\nsettings_show_icon_labels=Show Icon Labels\nsettings_show_icon_labels_description=Show labels under icons when possible ( like home toolbar actions )\nsettings_use_system_tray=Use System Tray\nsettings_use_system_tray_description=Show system tray icon when the app is running\nsettings_start_on_boot=Start On Boot\nsettings_start_on_boot_description=Auto start application on user logins\nsettings_notification_sound=Notification Sound\nsettings_notification_sound_description=Play sound on new notification\nsettings_browser_integration=Browser Integration\nsettings_browser_integration_description=Accept downloads from browsers\nsettings_browser_integration_server_port=Server Port\nsettings_browser_integration_server_port_description=Port for browser integration\nsettings_browser_integration_server_port_describe=App will listen to port {{port}}\nsettings_dynamic_part_creation=Dynamic Part Creation\nsettings_dynamic_part_creation_description=When a part is finished create another part by splitting other parts to improve download speed\nsettings_show_completion_dialog=Show Download Completion Dialog\nsettings_show_completion_dialog_description=Automatically show \"Download Complete\" dialog when a download finished.\nsettings_show_download_progress_dialog=Show Download Progress Dialog\nsettings_show_download_progress_dialog_description=Automatically show \"Download Progress\" dialog when a download started.\nsettings_per_host_settings=Per Host Settings\nsettings_per_host_settings_descriptions=These settings will be automatically applied to any new download that matches the specified host.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Speed Limit\ndownload_item_settings_speed_limit_description=Limit download speed for this item\ndownload_item_settings_show_download_completion_dialog=Show Download Completion Dialog\ndownload_item_settings_show_download_completion_dialog_description=Automatically Show the \"Download Complete\" dialog when this download is finished.\ndownload_item_settings_shutdown_on_completion=Shutdown System On Completion\ndownload_item_settings_shutdown_on_completion_description=Automatically shutdown the system when this download is finished.\ndownload_item_settings_thread_count=Thread Count\ndownload_item_settings_thread_count_description=How much thread used to download this download item (0 for default)\ndownload_item_settings_thread_count_describe={{count}} threads for this download\ndownload_item_settings_username_description=Provide a username if the link is a protected resource\ndownload_item_settings_password_description=Provide a password if the link is a protected resource\ndownload_item_settings_download_page=Download Page\ndownload_item_settings_download_page_description=The webpage where this download was initiated\ndownload_item_settings_file_checksum=File Checksum\ndownload_item_settings_file_checksum_description=A hash string which can be used to check if file is downloaded correctly\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Custom User-Agent for this item (leave empty to use the default)\nfile_checksum=File Checksum\nfile_checksum_page=File Checksum Checker\nfile_checksum_page_file_checksum_default_algorithm=Default Algorithm\nfile_checksum_page_file_checksum_default_algorithm_help=The default algorithm used to calculate file checksums when they are not provided.\nstart=Start\ncalculated_checksum=Calculated Checksum\nsaved_checksum=Saved Checksum\nchecksum_algorithm=Algorithm\nfile_not_found=File not found\ndownload_not_finished=Download not finished\ndone=Done\nwaiting=Waiting\nmatches=Matches\nnot_matches=Not Matches\ncopy_to_clipboard=Copy To Clipboard\nusername=Username\npassword=Password\naverage_speed=Average Speed\nexact_speed=Exact Speed\nunlimited=Unlimited\nuse_global_settings=Use Global Settings\ncant_run_browser_integration=Can't run browser integration\ncant_open_file=Can't Open File\ncant_open_folder=Can't Open Folder\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} years\nrelative_time_long_months={{months}} months\nrelative_time_long_days={{days}} days\nrelative_time_long_hours={{hours}} hours\nrelative_time_long_minutes={{minutes}} minutes\nrelative_time_long_seconds={{seconds}} seconds\nrelative_time_short_years={{years}} y\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} d\nrelative_time_short_hours={{hours}} hr\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} sec\nrelative_time_left={{time}} left\nrelative_time_ago={{time}} ago\nauto=Auto\nunspecified=Unspecified\ncustom=Custom\nicon=Icon\nauthor=Author\nlink=Link\nsize=Size\nstatus=Status\nparts_info_downloaded_size=Downloaded\nparts_info_total_size=Total\nspeed=Speed\ntime_left=Time Left\ndate_added=Date Added\ninfo=Info\ndownload_page_downloaded_size=Downloaded\ndownload_page_download_completed=Download Completed\nresume_support=Resume Support\nyes=Yes\nno=No\nparts_info=Parts Info\ndisconnected=Disconnected\nreceiving_data=Receiving Data\nconnecting=Connecting\nwarning=Warning\nunsupported_resume_warning=This download doesn't support resuming! You may have to RESTART it later in the Download List\nstop_anyway=Stop Anyway\ncustomize_columns=Customize Columns\nreset=Reset\nmonday=Monday\ntuesday=Tuesday\nwednesday=Wednesday\nthursday=Thursday\nfriday=Friday\nsaturday=Saturday\nsunday=Sunday\nproxy_open_system_proxy_settings=Open System Proxy Settings\nproxy_type=Proxy type\nproxy_do_not_use_proxy_for=Don't Use proxy for\nproxy_do_not_use_proxy_for_description=A list of urls that may not be proxied\\nYou can use wildcard with *\\nfor example 192.168.1.* example.com (space separated)\nproxy_change_title=Change Proxy\nchange_proxy=Change Proxy\nproxy_no=No Proxy\nproxy_system=System Proxy\nproxy_manual=Manual Proxy\nproxy_pac=Proxy Auto Configuration\nproxy_pac_url=Proxy Auto Configuration URL\naddress=Address\nport=Port\naddress_and_port=Address & Port\nuse_authentication=Use Authentication\nwarning_you_may_have_to_restart_the_download_later=You may have to restart the download later!\nedit_download_title=Edit Download\nedit_download_update_from_download_page=Update from Download Page\nedit_download_update_from_download_page_description=When this window is open, you can go to the Download Page and click the download button. The app will capture and update the new download credentials so you can save them.\nedit_download_saved_download_item_size_not_match=The saved download item has a size of {{currentSize}}, which does not match the new size of {{newSize}}.\ntranslators_page_thanks=With Gratitude to Those Who Helped Translate This Project ❤️\ntranslators=Translators\nlanguage=Language\ntranslators_contribute_title=Improve Translations\ntranslators_contribute_description=Want to help improve this project? If your language isn't listed or needs some tweaks, you can contribute your translations and make it better\\!\ncontribute=Contribute\nmeet_the_translators=Meet the Translators\nlocalized_by_translators=Localized by Translators\nconfirm_exit=Confirm Exit\nconfirm_exit_description=Are you sure you want to exit AB Download Manager?\\nActive downloads/queues will be stopped!\nupdate=Update\nupdate_updater=Updater\nupdate_available=Update Available\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=You can update to the latest version to enjoy new features, enhancements, and performance improvements.\nupdate_release_notes=Release Notes\nupdate_check_for_update=Check for Update\nupdate_checking_for_update=Checking for Update\nupdate_no_update=You are using the latest version\nupdate_check_error=Error while checking for update\nupdate_app_updated_to_version_n=App updated to version {{version}}\ncreate_desktop_entry=Create Desktop Entry\nshutdown_alert=Shut Down Alert\nsystem_shutdown_soon=System Will Shut Down Soon!\nsystem_shutdown_failed=System Shut Down Failed!\nsystem_shutdown_soon_description=The system will shut down soon. If you're still using the computer, please save your work or cancel the shutdown.\nsystem_shutdown_reason_queue_completed=All downloads in the queue are complete.\nsystem_shutdown_reason_queue_end_time_reached=Scheduled end time for the download queue reached.\nsystem_shutdown_download_finished=Download completed.\nshutdown_now=Shut Down Now\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Create or select a new item first!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=These settings will be applied to downloads matching this hostname. Wildcards (*) are supported (e.g., example.com, *.example.com — use only one).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/es_ES.properties",
    "content": "app_title=AB Administrador de descargas\nconfirm_auto_categorize_downloads_title=Clasificar automáticamente descargas\nconfirm_auto_categorize_downloads_description=Cualquier elemento no categorizado se añadirá automáticamente a su categoría correspondiente.\nconfirm_reset_to_default_categories_title=Reiniciar categorías por defecto\nconfirm_reset_to_default_categories_description=¡Esto ELIMINARÁ todas las categorías y restablecerá las predeterminadas\\!\nconfirm_delete_download_items_title=Confirmar Eliminación\nconfirm_delete_download_items_description=¿Estás seguro de que quieres eliminar {{count}} elementos?\nconfirm_delete_download_unfinished_items_description=¿Estás seguro de que quieres eliminar {{count}} descargas incompletas?\nconfirm_delete_download_finished_and_unfinished_items_description=¿Estás seguro de que quieres eliminar {{finishedCount}} descargas completadas y {{unfinishedCount}} descargas incompletas?\nalso_delete_file_from_disk=Eliminar también del disco\nconfirm_delete_category_item_title=Eliminando categoría {{name}}\nconfirm_delete_category_item_description=¿Estás seguro de que quieres eliminar la categoría \"{{value}}\"?\nyour_download_will_not_be_deleted=Sus descargas no serán eliminadas\ndrag_the_file_to_another_app=Arrastra el archivo a otra aplicación\ndrop_link_or_file_here=Suelta el enlace o archivo aquí.\nnothing_will_be_imported=Nada será importado\nn_links_will_be_imported={{count}} enlaces serán importados\nn_items_selected={{count}} elementos seleccionados\nwindow_close=Cerrar\nwindow_minimize=Minimizar\nwindow_maximize=Maximizar\nwindow_restore=Restaurar\ndelete=Eliminar\nremove=Quitar\ncancel=Cancelar\nclose=Cerrar\nmenu=Menú\nmore_options=Más opciones\nok=Aceptar\nadd=Añadir\npaste=Pegar\nchange=Cambiar\nedit=Editar\nchange_anyway=Cambiar igualmente\ndownload=Descargar\nrefresh=Actualizar\nsettings=Ajustes\non_completion=Al finalizar\nunknown=Desconocido\nunknown_error=Error desconocido\ndownload_item_not_found=No se encuentra el elemento descargado\nname=Nombre\ndownload_link=Enlace de descarga\nnot_finished=Sin completar\nall=Todo\nfinished=Completado\nUnfinished=Sin completar\ncanceled=Cancelado\nerror=Error\npaused=Pausado\ndownloading=Descargando\nadded=Añadido\nidle=INACTIVO\npreparing_file=Preparando archivo\ncreating_file=Creando archivo\nresuming=Resumiendo\nretrying=Reintentando\nlist_is_empty=¡La lista está vacía\\!\nsearch_in_the_list=Buscar en la Lista\nsearch=Buscar\nclear=Limpiar\ngeneral=General\nenabled=Habilitado\ndisabled=Deshabilitado\ndefault=Predeterminado\nfile=Archivo\ntasks=Tareas\ntools=Herramientas\nhelp=Ayuda\nsystem=Sistema\nall_missing_files=Todos los archivos que faltan\nall_finished=Todo completado\nall_unfinished=Sin completar\nentire_list=Lista completa\ndownload_browser_integration=Descargar integración con navegador\nexit=Salir\nshow_downloads=Mostrar descargas\nnew_download=Nueva descarga\nstop_all=Parar todo\nimport_from_clipboard=Importar desde el portapapeles\nbatch_download=Descarga por lotes\nopen=Abrir\nshare=Compartir\nopen_file=Abrir archivo\nopen_folder=Abrir carpeta\nresume=Reanudar\npause=Pausar\nrestart_download=Reiniciar descarga\ncopy=Copiar\ncopy_link=Copiar enlace\ncopy_as_curl=Copiar como cURL\nshow_properties=Mostrar propiedades\nmove_to_queue=Mover a la cola\nmove_to_this_queue=Mover a esta cola\nmove_to_category=Mover a la categoría\nmove_to_this_category=Mover a esta categoría\ncategories=Categorías\nadd_category=Añadir categoría\nedit_category=Modificar categoría\ndelete_category=Eliminar categoría\ncategory_name=Nombre de la categoría\ncategory_download_location=Ubicación de categoría\ncategory_download_location_description=Al eligir esta categoría en \"Añadir descarga\", usar este directorio como \"Ubicación de descarga\"\ncategory_file_types=Tipos de archivos de categoría\ncategory_file_types_description=Poner automáticamente estos tipos de archivos en esta categoría. (al añadir una nueva descarga)\\nSeparar las extensiones de archivo con espacio (ext1 ext2...)\ncategory_url_patterns=Patrones de URL\ncategory_url_patterns_description=Poner automáticamente las descargas de estas URLs en esta categoría. (al añadir una nueva descarga)\\nSeparar las URLs con espacios, también puede usar * como comodín\nauto_categorize_downloads=Categorizar descargas automáticamente\nrestore_defaults=Restaurar valores por defecto\nabout=Acerca de...\nversion_n=Versión {{value}}\ndeveloped_with_love_for_you=Desarrollado con ❤️ para ti\ndonate=Donar\nvisit_the_project_website=Visitar sitio web del proyecto\nthis_is_a_free_and_open_source_software=Este es un software gratuito y de código abierto\nview_the_source_code=Ver el código fuente\nthird_party_libraries=Bibliotecas de terceros\npowered_by_open_source_software=Producido por software de código abierto\nview_the_open_source_licenses=Ver las licencias de código abierto\nsupport_and_community=Soporte & Comunidad\ntelegram=Telegram                                            \nchannel=Canal\ngroup=Grupo\nadd_download=Añadir descarga\nadd_multi_download_page_header=Seleccione los elementos que desea tomar para descargar\nsave_to=Guardar en\nwhere_should_each_item_saved=¿Dónde debe guardarse cada elemento?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=¡Hay varios elementos\\! Elegir una forma de guardarlos\neach_item_on_its_own_category=Cada elemento en su propia categoría\neach_item_on_its_own_category_description=Cada elemento se colocará en una categoría que tenga ese tipo de archivo\nall_items_in_one_category=Todos los elementos en una categoría\nall_items_in_one_category_description=Todos los archivos serán guardados en la categoría destino seleccionada\nall_items_in_one_Location=Todos los elementos en una sola ubicación\nall_items_in_one_Location_description=Todos los elementos se guardarán en el directorio seleccionado\nunselected_all_items_in_specific_location_description=Todos los elementos se guardarán en el directorio seleccionado\nno_category_selected=Sin categoría\nno_categories_found=No se encontraron categorías\ndownload_location=Ubicación de descarga\nlocation=Ubicación\nselect_queue=Seleccionar cola\nwithout_queue=Sin cola\nuse_category=Usar categoría\ncant_write_to_this_folder=No se puede escribir en esta carpeta\nfile_name_already_exists=El nombre de archivo ya existe\ndownload_already_exists=La descarga ya existe\ninvalid_file_name=Archivo inválido\nshow_solutions=Mostrar soluciones...\nchange_solution=Cambiar solución\nselect_a_solution=Seleccionar una solución\nselect_download_strategy_description=El enlace ya se encuentra en la lista de descargas, por favor especifique lo que desea hacer\ndownload_strategy_add_a_numbered_file=Añadir un archivo numerado\ndownload_strategy_add_a_numbered_file_description=Añadir un índice al final del nombre del archivo de descarga\ndownload_strategy_override_existing_file=Sobreescribir el archivo existente\ndownload_strategy_override_existing_file_description=Quitar la descarga existente y escribir en ese archivo\ndownload_strategy_update_download_link=Actualizar descarga existente\ndownload_strategy_update_download_link_description=Actualizar el enlace de descarga existente y sus credenciales\ndownload_strategy_show_downloaded_file=Mostrar archivo descargado\ndownload_strategy_show_downloaded_file_description=Mostrar el elemento de descarga ya existente, para que pueda pulsar en reanudar o abrir\nbatch_download_link_help=Introducir un enlace que contenga comodines (utilizar *)\ninvalid_url=La URL no es válida.\nlist_is_too_large_maximum_n_items_allowed=¡La lista es demasiado grande\\! Máximo {{count}} elementos permitidos\nenter_range=Introducir rango\nrange_from=Desde\nrange_to=Hasta\nbatch_download_wildcard_length=Longitud de comodín\nfirst_link=Primer enlace\nlast_link=Último enlace\nopen_source_software_used_in_this_app=Software de código abierto utilizado en esta aplicación\nlinks=Enlaces\nwebsite=Sitio web\ndevelopers=Desarrolladores\nsource_code=Código fuente\nlicense=Licencia\nno_license_found=No se ha encontrado ninguna Licencia\norganization=Organización\nadd_new_queue=Añadir nueva cola\nqueue_name=Nombre de cola\nqueues=Colas\nstop_queue=Detener cola\nstart_queue=Iniciar cola\nclear_queue_items=Cola vacía\nconfig=Ajustes\nitems=Elementos\nmove_down=Mover abajo\nmove_up=Mover arriba\nremove_queue=Quitar cola\nqueue_name_help=Especificar un nombre para esta cola\nqueue_name_describe=El nombre de la cola es {{value}}\nqueue_max_concurrent_download=Máximas descargas simultáneas\nqueue_max_concurrent_download_description=Máximas descargas para esta cola\nqueue_automatic_stop=Detener automáticamente\nqueue_automatic_stop_description=Detener automáticamente la cola cuando no hay ningún elemento en ella\nqueue_scheduler=Programador\nqueue_enable_scheduler=Activar programador\nqueue_active_days=Días activos\nqueue_active_days_description=¿Qué días funcionan los programadores?\nqueue_scheduler_enable_auto_start_time=Habilitar hora de inicio automática\nqueue_scheduler_auto_start_time=Hora de inicio automático\nqueue_scheduler_enable_auto_stop_time=Activar hora de parada automática\nqueue_scheduler_auto_stop_time=Hora de parada automática\nqueue_shutdown_on_completion=Sistema de apagado al finalizar\nqueue_shutdown_on_completion_description=Apagar automáticamente el sistema cuando se complete esta cola o cuando se alcance la hora de finalización programada.\nappearance=Apariencia\ndownload_engine=Motor de descarga\nbrowser_integration=Integración con navegador\nsettings_download_max_retries_count=Reintentos máximos de descarga\nsettings_download_max_retries_count_description=El número máximo de veces que la aplicación reintentará una descarga fallida antes de rendirse\nsettings_download_max_retries_count_describe_no_retries=Las descargas fallidas no se volverán a intentar\nsettings_download_max_retries_count_describe_n_retries=Las descargas fallidas se reintentarán {{count}} vez/veces\nsettings_download_thread_count=Partes\nsettings_download_thread_count_description=Máximo de partes por descarga\nsettings_download_thread_count_describe=Una descarga puede tener hasta {{count}} partes\nsettings_download_thread_count_with_large_value_describe=Advertencia\\: Configurar un alto número de hilos puede aumentar el uso de recursos del sistema, reducir el rendimiento o causar problemas de conexión con los servidores. Utilice valores más altos sólo si comprende el impacto potencial en su sistema y su red.\nsettings_use_server_last_modified_time=Usar la última hora de modificación del servidor\nsettings_use_server_last_modified_time_description=Al descargar un archivo, usar la última hora de modificación del servidor para el archivo local\nsettings_append_extension_to_incomplete_downloads=Añadir extensión a las descargas incompletas\nsettings_append_extension_to_incomplete_downloads_description=Añadir extensión \".part\" a las descargas incompletas. Esto ayuda a identificar las descargas incompletas y evita la apertura accidental de archivos incompletos.\nsettings_use_sparse_file_allocation=Asignación dispersa de archivos\nsettings_use_sparse_file_allocation_description=Crea archivos de manera mas eficiente, especialmente en SSDs, reduciendo la escritura de datos innecesaria. Esto puede acelerar la velocidad de descarga y reducir el uso de disco. Si la descarga inicia de manera lenta o experimenta una inusual velocidad de descarga, considera deshabilitar esta opción, ya que puede que no sea soportada por algunos dispositivos.\nsettings_ignore_ssl_certificates=Ignorar certificados SSL\nsettings_ignore_ssl_certificates_description=Deshabilita la verificación de certificado SSL. Utilice sólo si es necesario, ya que puede exponer su conexión a riesgos de seguridad.\nsettings_global_speed_limiter=Limitador global de velocidad\nsettings_global_speed_limiter_description=Límite global de velocidad de descarga (0 significa ilimitado)\nsettings_show_average_speed=Mostrar velocidad promedio\nsettings_show_average_speed_description=Velocidad de descarga promedio o precisa\nsettings_use_category_by_default=Usar categoría por defecto\nsettings_use_category_by_default_description=Usar categoría por defecto al añadir una descarga.\nsettings_default_download_folder=Carpeta de descargas por defecto\nsettings_default_download_folder_description=Al añadir una nueva descarga, se usa esta ubicación por defecto\nsettings_default_download_folder_describe=Se usará \"{{folder}}\"\nsettings_use_proxy=Usar proxy\nsettings_use_proxy_description=Usar proxy para descargar archivos\nsettings_use_proxy_describe_no_proxy=Ningún proxy será utilizado\nsettings_use_proxy_describe_system_proxy=El proxy del sistema será utilizado\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" será utilizado\nsettings_use_proxy_describe_pac_proxy=Se utilizará el archivo pac \"{{value}}\"\nsettings_track_deleted_files_on_disk=Rastrear archivos borrados en disco\nsettings_track_deleted_files_on_disk_description=Quitar automáticamente los archivos de la lista cuando se eliminan o se mueven del directorio de descargas.\nsettings_delete_partial_file_on_download_cancellation=Eliminar archivo parcial al cancelar la descarga\nsettings_delete_partial_file_on_download_cancellation_description=Cuando se cancela una descarga, el archivo descargado parcialmente se eliminará del disco. Esto ayuda a mantener limpia la carpeta de descargas y reduce el uso innecesario de espacio en disco. Sin embargo, la descarga se reiniciará desde el principio la próxima vez que la inicie.\nsettings_default_user_agent=Agente de usuario predeterminado\nsettings_default_user_agent_description=Especifique la cadena de agente de usuario predeterminado para definir cómo se identifican las peticiones a los servidores. Esto puede ayudar a acceder a contenidos optimizados para determinados dispositivos o en evitar limitaciones de descarga impuestas por ciertos sitios web.\nsettings_download_size_unit=Unidad de tamaño de descarga\nsettings_download_size_unit_description=Unidad para mostrar el tamaño de la descarga\nsettings_download_speed_unit=Unidad de velocidad de descarga\nsettings_download_speed_unit_description=Unidad usada para mostrar la velocidad de descarga\nsettings_theme=Tema\nsettings_theme_description=Seleccione un tema para la aplicación\nsettings_default_dark_theme=Tema oscuro predeterminado\nsettings_default_dark_theme_description=Se aplica cuando la aplicación sigue el tema del sistema y el modo oscuro está activo\nsettings_default_light_theme=Tema claro predeterminado\nsettings_default_light_theme_description=Se aplica cuando la aplicación sigue el tema del sistema y el modo claro está activo\nsettings_font=Fuente\nsettings_font_description=Cambiar la fuente usada en la interfaz de la aplicación, algunas fuentes podrían no mostrarse correctamente en la aplicación.\nsettings_ui_scale=Escala de la interfaz\nsettings_ui_scale_description=Ajustar el tamaño de los elementos de la interfaz\nsettings_language=Idioma\nsettings_compact_top_bar=Barra superior compacta\nsettings_compact_top_bar_description=Combinar la barra superior con la barra de título cuando la ventana principal tiene suficiente espacio\nsettings_use_native_menu_bar=Usar barra de menú nativa\nsettings_use_native_menu_bar_description=Usar el estilo de barra de menú por defecto del sistema\nsettings_use_relative_date_time=Usar fecha y hora relativa\nsettings_use_relative_date_time_description=Usar formato relativo de fecha/hora para las fechas en la aplicación (por ejemplo, \"hace 2 días\" en lugar de la fecha / hora exacta)\nsettings_show_icon_labels=Mostrar etiquetas de los iconos\nsettings_show_icon_labels_description=Mostrar etiquetas debajo de los iconos cuando sea posible (en los iconos del lado derecho del botón de nueva descarga)\nsettings_use_system_tray=Usar bandeja del sistema\nsettings_use_system_tray_description=Mostrar el icono en la bandeja del sistema cuando la aplicación esté en ejecución\nsettings_start_on_boot=Iniciar con el equipo\nsettings_start_on_boot_description=Iniciar aplicación al iniciar sesión\nsettings_notification_sound=Sonido de notificación\nsettings_notification_sound_description=Reproducir sonido en una nueva notificación\nsettings_browser_integration=Integración con navegador\nsettings_browser_integration_description=Aceptar descargas desde el navegador\nsettings_browser_integration_server_port=Puerto del servidor\nsettings_browser_integration_server_port_description=Puerto para integración en navegador\nsettings_browser_integration_server_port_describe=La aplicación escuchará el puerto {{port}}\nsettings_dynamic_part_creation=Creación dinámica de partes\nsettings_dynamic_part_creation_description=Cuando una parte está terminada, crear otra dividiendo otras partes para mejorar la velocidad de descarga\nsettings_show_completion_dialog=Mostrar diálogo de descarga completada\nsettings_show_completion_dialog_description=Mostrar automáticamente el diálogo \"Descarga completada\" cuando haya finalizado una descarga.\nsettings_show_download_progress_dialog=Mostrar diálogo de progreso de descarga\nsettings_show_download_progress_dialog_description=Mostrar automáticamente diálogo de \"Progreso de descarga\" al iniciar una descarga.\nsettings_per_host_settings=Configuración por host\nsettings_per_host_settings_descriptions=Estos ajustes se aplicarán automáticamente a cualquier nueva descarga que coincida con el host especificado.\nsettings_download_max_concurrent_downloads=Máximo número de descargas simultáneas\nsettings_download_max_concurrent_downloads_description=El número máximo de archivos que se pueden descargar al mismo tiempo (las descargas administradas por las colas no son contadas; se establece en 0 para ilimitado)\ndownload_item_settings_speed_limit=Límite de velocidad\ndownload_item_settings_speed_limit_description=Limitar velocidad de descarga para este elemento\ndownload_item_settings_show_download_completion_dialog=Mostrar diálogo de descarga completada\ndownload_item_settings_show_download_completion_dialog_description=Mostrar automáticamente diálogo de \"Descarga completada\" al finalizar una descarga.\ndownload_item_settings_shutdown_on_completion=Sistema de apagado al finalizar\ndownload_item_settings_shutdown_on_completion_description=Apagar automáticamente el sistema cuando finalice esta descarga.\ndownload_item_settings_thread_count=Partes\ndownload_item_settings_thread_count_description=Número de partes usadas para esta descarga (0 por defecto)\ndownload_item_settings_thread_count_describe={{count}} partes para esta descarga\ndownload_item_settings_username_description=Colocar un nombre de usuario si el enlace está protegido\ndownload_item_settings_password_description=Colocar una contraseña si el enlace está protegido\ndownload_item_settings_download_page=Página de descarga\ndownload_item_settings_download_page_description=La página web donde se inició la descarga\ndownload_item_settings_file_checksum=Suma de verificación\ndownload_item_settings_file_checksum_description=Una cadena hash que puede ser usada para comprobar si el archivo se ha descargado correctamente\ndownload_item_settings_user_agent=Agente de usuario\ndownload_item_settings_user_agent_description=Agente de usuario personalizado para este elemento (dejar vacío para usar el predeterminado)\nfile_checksum=Suma de verificación\nfile_checksum_page=Comprobador de suma de verificación de archivo\nfile_checksum_page_file_checksum_default_algorithm=Algoritmo por defecto\nfile_checksum_page_file_checksum_default_algorithm_help=El algoritmo por defecto usado para calcular sumas de verificación de archivos cuando no se proporcionan.\nstart=Iniciar\ncalculated_checksum=Suma de verificación calculada\nsaved_checksum=Suma de verificación guardada\nchecksum_algorithm=Algoritmo\nfile_not_found=Archivo no encontrado\ndownload_not_finished=Descarga sin completar\ndone=Hecho\nwaiting=Esperando\nmatches=Coincidencias\nnot_matches=Sin coinciden\ncopy_to_clipboard=Copiar al portapapeles\nusername=Usuario\npassword=Contraseña\naverage_speed=Velocidad promedio\nexact_speed=Velocidad exacta\nunlimited=Ilimitado\nuse_global_settings=Usar ajustes globales\ncant_run_browser_integration=No se puede ejecutar la integración con el navegador\ncant_open_file=No se puede abrir el archivo\ncant_open_folder=No se puede abrir la carpeta\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} años\nrelative_time_long_months={{months}} meses\nrelative_time_long_days={{days}} días\nrelative_time_long_hours={{hours}} horas\nrelative_time_long_minutes={{minutes}} minutos\nrelative_time_long_seconds={{seconds}} segundos\nrelative_time_short_years={{years}} a\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} d\nrelative_time_short_hours={{hours}} hrs\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} seg\nrelative_time_left={{time}} quedan\nrelative_time_ago=Hace {{time}}\nauto=Auto\nunspecified=Sin especificar\ncustom=Personalizado\nicon=Icono\nauthor=Autor\nlink=Enlace\nsize=Tamaño\nstatus=Estado\nparts_info_downloaded_size=Descargado\nparts_info_total_size=Total\nspeed=Velocidad\ntime_left=Tiempo restante\ndate_added=Añadido\ninfo=Información\ndownload_page_downloaded_size=Descargado\ndownload_page_download_completed=Descarga completada\nresume_support=Reanudable\nyes=Si\nno=No\nparts_info=Información de partes\ndisconnected=Desconectado\nreceiving_data=Recibiendo datos\nconnecting=Conectando\nwarning=Advertencia\nunsupported_resume_warning=¡Esta descarga no se puede reanudar\\! Es posible que tenga que reanudarla más tarde en la lista de descargas\nstop_anyway=Parar igualmente\ncustomize_columns=Personalizar columnas\nreset=Reiniciar\nmonday=Lunes\ntuesday=Martes\nwednesday=Miércoles\nthursday=Jueves\nfriday=Viernes\nsaturday=Sábado\nsunday=Domingo\nproxy_open_system_proxy_settings=Abrir ajustes de proxy del sistema\nproxy_type=Tipo de proxy\nproxy_do_not_use_proxy_for=No usar proxy para\nproxy_do_not_use_proxy_for_description=La lista de los enlaces puede no estar en proxy\\nPuede usar un comodín con *\\npor ejemplo 192.168.1.* example.com (separado con un espacio)\nproxy_change_title=Cambiar proxy\nchange_proxy=Cambiar proxy\nproxy_no=Sin proxy\nproxy_system=Proxy del sistema\nproxy_manual=Proxy manual\nproxy_pac=Configuración automática de proxy\nproxy_pac_url=URL de configuración automática de proxy\naddress=Dirección\nport=Puerto\naddress_and_port=Dirección & puerto\nuse_authentication=Usar autenticación\nwarning_you_may_have_to_restart_the_download_later=¡Puede que tenga que reiniciar la descarga más tarde\\!\nedit_download_title=Editar descarga\nedit_download_update_from_download_page=Actualizar desde la página de descarga\nedit_download_update_from_download_page_description=Cuando esta ventana esté abierta, puede ir a la página de descarga y hacer clic en el botón de descarga. La aplicación capturará y actualizará las nuevas credenciales de descarga para que puedas guardarlas.\nedit_download_saved_download_item_size_not_match=El elemento de descarga guardado tiene un tamaño de {{currentSize}}, que no coincide con el nuevo tamaño de {{newSize}}.\ntranslators_page_thanks=Con gratitud a quienes ayudaron a traducir este proyecto ❤️\ntranslators=Traductores\nlanguage=Idioma\ntranslators_contribute_title=Mejorar traducciones\ntranslators_contribute_description=¿Quieres ayudar a mejorar este proyecto? Si tu idioma no está en la lista o necesita algunos retoques, ¡puedes contribuir con tus traducciones y mejorarlo\\!\ncontribute=Contribuir\nmeet_the_translators=Conoce a los traductores\nlocalized_by_translators=Localizado por traductores\nconfirm_exit=Confirmar salida\nconfirm_exit_description=¿Desea salir de AB Download Manager?\\nLas descargas/colas activas se detendrán.\nupdate=Actualizar\nupdate_updater=Actualizador\nupdate_available=Actualización disponible\nupdate_error=Error al actualizar\nupdate_available_suggest_to_to_update=Puede actualizar a la última versión para disfrutar de nuevas características, mejoras y mejoras de rendimiento.\nupdate_release_notes=Notas de lanzamiento\nupdate_check_for_update=Buscar actualizaciones\nupdate_checking_for_update=Buscando actualizaciones\nupdate_no_update=Estás usando la última versión\nupdate_check_error=Error al buscar actualización\nupdate_app_updated_to_version_n=Aplicación actualizada a la versión {{version}}\ncreate_desktop_entry=Crear acceso directo en el escritorio\nshutdown_alert=Alerta de apagado\nsystem_shutdown_soon=¡El sistema se apagará pronto\\!\nsystem_shutdown_failed=¡Error al apagar el sistema\\!\nsystem_shutdown_soon_description=El sistema se apagará pronto. Si todavía está utilizando el ordenador, guarde su trabajo o cancele el apagado.\nsystem_shutdown_reason_queue_completed=Todas las descargas cola se han completado.\nsystem_shutdown_reason_queue_end_time_reached=Se ha alcanzado la hora de finalización programada para la cola de descargas.\nsystem_shutdown_download_finished=Descarga completada.\nshutdown_now=Apagar ahora\nsettings_per_host_settings_new_host=<Nuevo Host>\nsettings_per_host_settings_not_selected=¡Crea o selecciona un nuevo elemento primero\\!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=Estos ajustes se aplicarán a las descargas que coincidan con este nombre de host. Se admiten comodines (*) (por ejemplo, example.com, *.example.com — usa solo uno).\nsettings_browser_in_launcher=Icono del navegador en el lanzador de aplicaciones\nsettings_browser_in_launcher_description=Mostrar u ocultar el icono del navegador en el lanzador (lista de aplicaciones).\nsort_by=Ordenar por\nwelcome=Bienvenido\nnew_folder=Nueva carpeta\nskip=Omitir\nlets_go=Vamos\nnext=Siguiente\nselect_all=Seleccionar todo\nselect_inside=Seleccione dentro\nselect_invert=Invertir selección\nopen_settings=Abrir configuración\nback=Atrás\nservice_is_running=El servicio se está ejecutando\ninitial_setup_description=Configuremos las opciones\ninitial_setup_notice=Puedes cambiar esta configuración en cualquier momento más tarde\npermission_granted=Permiso concedido\npermission_not_granted=Permiso no concedido\npermissions=Permisos\ngive_permission=Permitir permisos\ngive_storage_permission=Permitir el acceso al almacenamiento\nstorage_roots=Storage Roots\npermissions_initial_title=Configuración de permisos\npermissions_initial_description=Para funcionar correctamente, la aplicación necesita algunos permisos. En la siguiente pantalla, verás para qué se utiliza cada permiso y podrás decidir cuáles permitir o saltar.\npermissions_done_title=Está todo listo\npermissions_done_description=Todo está listo. Se han concedido todos los permisos necesarios y la aplicación está lista para funcionar.\npermissions_manage_storage_title=Administrar acceso de almacenamiento\npermissions_manage_storage_reason=Este permiso permite que la aplicación cambie la carpeta de descargas, detecte descargas duplicadas con mayor precisión y habilite algunas funciones adicionales. Es opcional, pero recomendado para tener la mejor experiencia.\npermission_read_write_external_storage_title=Almacenamiento de lectura y escritura\npermission_read_write_external_storage_reason=Este permiso permite que la aplicación guarde y administre archivos descargados, cambie la ubicación de descarga y mejore la detección de descargas duplicadas.\npermissions_post_notification_title=Enviar notificación\npermissions_post_notification_reason=La aplicación necesita ejecutarse en segundo plano para gestionar las descargas. Las notificaciones se utilizan para mantenerte informado y permitir la operación en segundo plano.\npermissions_ignore_battery_optimization_title=Ignorar optimización de la batería\npermissions_ignore_battery_optimization_reason=Algunos dispositivos limitan agresivamente la actividad en segundo plano para ahorrar batería, lo que puede pausar o detener las descargas cuando la aplicación no está abierta. Opcionalmente, puedes excluir la aplicación de la optimización de la batería para garantizar que las descargas continúen sin interrupciones.\nopen_in_browser=Abrir en navegador\nbrowser=Navegador\nbrowser_new_tab=Nueva pestaña\nbrowser_close_tab=Cerrar pestaña\nbrowser_open_in_new_tab=Abrir en una Nueva Pestaña\nbrowser_open_in_new_background_tab=Abrir en nueva pestaña en segundo plano\nbrowser_no_tab_open=No hay pestañas abiertas\nbrowser_tabs=Pestañas\nbrowser_paste_and_go=Pegar e ir\nbrowser_bookmarks=Marcadores\nbrowser_add_bookmark=Añadir marcador\nbrowser_edit_bookmark=Editar marcador\nbrowser_add_to_bookmarks=Añadir a marcadores\nbrowser_remove_from_bookmarks=Eliminar de marcadores\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/fa_IR.properties",
    "content": "app_title=مدیریت دانلود آبی\nconfirm_auto_categorize_downloads_title=دسته‌بندی خودکار دانلودها\nconfirm_auto_categorize_downloads_description=هر آیتم بدون دسته‌بندی به‌طور خودکار به دسته‌بندی مرتبط افزوده می‌شود.\nconfirm_reset_to_default_categories_title=بازنشانی به دسته‌بندی‌های پیش‌فرض\nconfirm_reset_to_default_categories_description=این کار تمام دسته‌بندی‌ها را حذف کرده و دسته‌بندی‌های پیش‌فرض را بازمی‌گرداند\\!\nconfirm_delete_download_items_title=تأیید حذف\nconfirm_delete_download_items_description=آیا مطمئن هستید که می‌خواهید {{count}} آیتم را حذف کنید؟\nconfirm_delete_download_unfinished_items_description=آیا واقعا میخواهید {{count}} دانلود ناتمام را حذف کنید؟\nconfirm_delete_download_finished_and_unfinished_items_description=آیا واقعا میخواهید {{finishedCount}} دانلود تکمیل شده و {{unfinishedCount}} دانلود ناتمام را حذف کنید؟\nalso_delete_file_from_disk=همچنین فایل را از دیسک حذف کن\nconfirm_delete_category_item_title=حذف دسته‌بندی {{name}}\nconfirm_delete_category_item_description=آیا مطمئن هستید که می‌خواهید دسته‌بندی \"{{value}}\" را حذف کنید؟\nyour_download_will_not_be_deleted=دانلودهای شما حذف نخواهند شد\ndrag_the_file_to_another_app=کشیدن فایل به برنامه دیگر\ndrop_link_or_file_here=لینک یا فایل را اینجا رها کنید.\nnothing_will_be_imported=لینکی وارد نخواهد شد\\!\nn_links_will_be_imported={{count}} لینک وارد خواهد شد\nn_items_selected={{count}} آیتم انتخاب شده\nwindow_close=بستن\nwindow_minimize=کمینه\nwindow_maximize=بیشینه\nwindow_restore=بازگردانی\ndelete=حذف\nremove=حذف\ncancel=لغو\nclose=بستن\nmenu=منو\nmore_options=گزینه‌های بیش‌تر\nok=تأیید\nadd=افزودن\npaste=چسباندن\nchange=ویرایش\nedit=ویرایش\nchange_anyway=به هر حال تغییرش بده\ndownload=دانلود\nrefresh=تازه‌سازی\nsettings=تنظیمات\non_completion=در پایان\nunknown=نامشخص\nunknown_error=خطای نامشخص\ndownload_item_not_found=آیتم دانلود یافت نشد\nname=نام\ndownload_link=لینک دانلود\nnot_finished=تمام نشده\nall=همه\nfinished=تمام شده\nUnfinished=نا‌تمام\ncanceled=لغو شده\nerror=خطا\npaused=متوقف شده\ndownloading=در حال دانلود\nadded=افزوده شده\nidle=بی‌کار\npreparing_file=در حال آماده‌سازی فایل\ncreating_file=در حال ایجاد فایل\nresuming=در حال از سرگیری\nretrying=درحال تلاش مجدد\nlist_is_empty=لیست خالی است\\!\nsearch_in_the_list=جستجو در لیست\nsearch=جستجو\nclear=پاک‌کردن\ngeneral=عمومی\nenabled=فعال\ndisabled=غیرفعال\ndefault=پیش‌فرض\nfile=فایل\ntasks=کارها\ntools=ابزارها\nhelp=راهنما\nsystem=سیستم\nall_missing_files=همه فایل های از دست رفته\nall_finished=همه تمام شده ها\nall_unfinished=همه نا‌تمام ها\nentire_list=کل لیست\ndownload_browser_integration=یکپارچه‌سازی دانلود با مرورگر\nexit=خروج\nshow_downloads=نمایش دانلودها\nnew_download=دانلود جدید\nstop_all=توقف همه\nimport_from_clipboard=وارد کردن از کلیپ بورد\nbatch_download=دانلود دسته‌ای\nopen=باز کردن\nshare=همرسانی\nopen_file=باز کردن فایل\nopen_folder=باز کردن پوشه\nresume=ادامه\npause=توقف\nrestart_download=شروع مجدد دانلود\ncopy=کپی کردن\ncopy_link=کپی لینک\ncopy_as_curl=به صورت cURL کپی شود\nshow_properties=نمایش ویژگی‌ها\nmove_to_queue=انتقال به صف\nmove_to_this_queue=انتقال به این صف\nmove_to_category=انتقال به دسته‌بندی\nmove_to_this_category=انتقال به این دسته بندی\ncategories=دسته‌بندی ها\nadd_category=افزودن دسته‌بندی\nedit_category=ویرایش دسته‌بندی\ndelete_category=حذف دسته‌بندی\ncategory_name=نام دسته بندی\ncategory_download_location=محل ذخیره دسته بندی\ncategory_download_location_description=وقتی این دسته بندی در صفحه \"افزودن دانلود\" انتخاب شد از این مسیر برای ذخیره فایل استفاده میشود\ncategory_file_types=نوع فایل های دسته بندی\ncategory_file_types_description=به‌صورت خودکار این فایل نوع فایل ها به این دسته بندی اضافه میشوند (وقتی که یک دانلود جدید اضافه میشود)\\n از \"فاصله\" برای جدا کردن نوع فایل ها استفاده کنید (ext1 ext2 ext3...)\ncategory_url_patterns=پترن های URL\ncategory_url_patterns_description=هنگامی که دانلود جدیدی اضافه میشود به صورت خودکار آن هایی که از این پترن لینک پیروی میکنن به این دسته بندی اضافه خواهند شد\\n از فاصله برای جدا کردن الگو ها استفاده کنید\\nمیتوانید از * (ستاره) برای wildcard استفاده کنید\nauto_categorize_downloads=دسته‌بندی خودکار دانلودها\nrestore_defaults=بازگرداندن پیش‌فرض‌ها\nabout=درباره\nversion_n=نسخه {{value}}\ndeveloped_with_love_for_you=با ❤️ برای شما توسعه داده شده\ndonate=حمایت مالی\nvisit_the_project_website=از وبسایت پروژه بازدید کنید\nthis_is_a_free_and_open_source_software=این نرم‌افزار رایگان و منبع باز است\nview_the_source_code=کد منبع را ببینید\nthird_party_libraries=کتابخانه‌های شخص ثالث\npowered_by_open_source_software=قدرت گرفته از نرم‌افزار های منبع باز\nview_the_open_source_licenses=نمایش مجوزهای منبع باز\nsupport_and_community=پشتیبانی و انجمن\ntelegram=تلگرام\nchannel=کانال\ngroup=گروه\nadd_download=افزودن دانلود\nadd_multi_download_page_header=آیتم‌هایی که می‌خواهید را برای دانلود انتخاب کنید\nsave_to=ذخیره در\nwhere_should_each_item_saved=هر آیتم در کجا ذخیره شود؟\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=چندین آیتم موجود است\\! لطفاً روش ذخیره را انتخاب کنید\neach_item_on_its_own_category=هر آیتم در دسته‌بندی خود\neach_item_on_its_own_category_description=هر آیتم در دسته‌بندی خود بر اساس نوع فایل قرار خواهد گرفت\nall_items_in_one_category=همه آیتم‌ها در یک دسته‌بندی\nall_items_in_one_category_description=همه فایل‌ها در دسته‌بندی انتخاب‌شده ذخیره می‌شوند\nall_items_in_one_Location=همه آیتم‌ها در یک مکان\nall_items_in_one_Location_description=همه آیتم‌ها در پوشه انتخابی ذخیره خواهند شد\nunselected_all_items_in_specific_location_description=همه فایل‌ها در مکان دسته‌بندی انتخاب شده ذخیره می‌شوند\nno_category_selected=دسته‌بندی انتخاب نشده\nno_categories_found=دسته‌بندی‌ای پیدا نشد\ndownload_location=محل دانلود\nlocation=مکان\nselect_queue=انتخاب صف\nwithout_queue=بدون صف\nuse_category=استفاده از دسته‌بندی\ncant_write_to_this_folder=نمی‌توان در این پوشه نوشت\nfile_name_already_exists=نام فایل موجود است\ndownload_already_exists=دانلود از قبل وجود دارد\ninvalid_file_name=نام فایل نامعتبر است\nshow_solutions=نمایش راه‌حل‌ها...\nchange_solution=تغییر راه‌حل\nselect_a_solution=انتخاب یک راه‌حل\nselect_download_strategy_description=لینکی که ارائه دادید در لیست دانلود موجود است، لطفاً مشخص کنید که می‌خواهید چه کاری انجام دهید\ndownload_strategy_add_a_numbered_file=افزودن فایل با شماره\ndownload_strategy_add_a_numbered_file_description=اضافه‌کردن شماره در انتهای نام فایل دانلود\ndownload_strategy_override_existing_file=بازنویسی فایل موجود\ndownload_strategy_override_existing_file_description=حذف دانلود موجود و نوشتن روی آن فایل\ndownload_strategy_update_download_link=بروزرسانی دانلود موجود\ndownload_strategy_update_download_link_description=بروزرسانی لینک و مجوزهای دانلود موجود\ndownload_strategy_show_downloaded_file=نمایش فایل دانلودشده\ndownload_strategy_show_downloaded_file_description=نمایش آیتم دانلود موجود برای ادامه دادن یا باز کردن\nbatch_download_link_help=لینکی وارد کنید که شامل کاراکترهای جایگزین (wildcard) باشد (از * استفاده کنید)\ninvalid_url=آدرس نامعتبر است\nlist_is_too_large_maximum_n_items_allowed=لیست خیلی بزرگ است\\! حداکثر {{count}} آیتم مجاز است\nenter_range=وارد کردن محدوده\nrange_from=از\nrange_to=تا\nbatch_download_wildcard_length=طول نویسه‌های wildcard\nfirst_link=لینک اول\nlast_link=لینک آخر\nopen_source_software_used_in_this_app=نرم‌افزارهای منبع باز استفاده‌شده در این برنامه\nlinks=لینک‌ها\nwebsite=وبسایت\ndevelopers=توسعه‌دهندگان\nsource_code=کد منبع\nlicense=مجوز\nno_license_found=مجوزی یافت نشد\norganization=سازمان\nadd_new_queue=افزودن صف جدید\nqueue_name=نام صف\nqueues=صف‌ها\nstop_queue=توقف صف\nstart_queue=شروع صف\nclear_queue_items=خالی کردن صف\nconfig=پیکربندی\nitems=آیتم‌ها\nmove_down=پایین بردن\nmove_up=بالا بردن\nremove_queue=حذف صف\nqueue_name_help=نامی برای این صف مشخص کنید\nqueue_name_describe=نام صف {{value}} است\nqueue_max_concurrent_download=حداکثر دانلود همزمان\nqueue_max_concurrent_download_description=حداکثر دانلود همزمان برای این صف\nqueue_automatic_stop=توقف خودکار\nqueue_automatic_stop_description=توقف خودکار صف وقتی آیتمی در آن وجود ندارد\nqueue_scheduler=زمان‌بندی\nqueue_enable_scheduler=فعال‌سازی زمان‌بندی\nqueue_active_days=روزهای فعال\nqueue_active_days_description=در چه روزهایی باید زمان‌بندی فعال باشد؟\nqueue_scheduler_enable_auto_start_time=فعالسازی زمان شروع خودکار\nqueue_scheduler_auto_start_time=زمان شروع خودکار\nqueue_scheduler_enable_auto_stop_time=فعال‌سازی زمان توقف خودکار\nqueue_scheduler_auto_stop_time=زمان توقف خودکار\nqueue_shutdown_on_completion=در پایان سیستم خاموش شود\nqueue_shutdown_on_completion_description=خاموش کردن خودکار سیستم هنگامی که این صف به پایان برسد یا زمان پایان برنامه‌ریزی‌شده فرا برسد.\nappearance=ظاهر\ndownload_engine=موتور دانلود\nbrowser_integration=یکپارچه‌سازی با مرورگر\nsettings_download_max_retries_count=حداکثر تلاش های مجدد برای دانلود\nsettings_download_max_retries_count_description=حداکثر تعداد دفعاتی که برنامه تلاش می‌کند یک دانلود ناموفق را دوباره انجام دهد پیش از آنکه تسلیم شود\nsettings_download_max_retries_count_describe_no_retries=برای دانلودهای ناموفق دوباره تلاش نخواهد شد\nsettings_download_max_retries_count_describe_n_retries=برای دانلودهای ناموفق {{count}} بار تلاش خواهد شد\nsettings_download_thread_count=تعداد کانکشن ها\nsettings_download_thread_count_description=حداکثر تعداد کانکشن ها برای هر آیتم دانلود\nsettings_download_thread_count_describe=هر دانلود می‌تواند تا {{count}} کانکشن داشته باشد\nsettings_download_thread_count_with_large_value_describe=هشدار\\: تنظیم تعداد کانکشن های بالا ممکن است استفاده از منابع سیستم را افزایش دهد، عملکرد سیستم را تضعیف کند یا باعث ایجاد مشکل در اتصال به بعضی از سرورها شود. تنها در صورتی از مقادیر بالاتر استفاده کنید که متوجه تأثیرات منفی احتمالی آن بر سیستم و شبکه خود باشید.\nsettings_use_server_last_modified_time=استفاده از زمان آخرین تغییر سرور\nsettings_use_server_last_modified_time_description=هنگام دانلود، زمان آخرین تغییر سرور برای فایل محلی استفاده می‌شود\nsettings_append_extension_to_incomplete_downloads=افزودن پسوند به دانلود های ناتمام\nsettings_append_extension_to_incomplete_downloads_description=افزودن پسوند \".part\" به دانلود های ناتمام. این کار باعث تشخیص بهتر دانلود های ناتمام و جلوگیری از باز کردن آن ها توسط کاربر میشود.\nsettings_use_sparse_file_allocation=ایجاد فایل به‌صورت پویا (Sparse)\nsettings_use_sparse_file_allocation_description=ایجاد فایل به صورت بهینه تر به خصوص برای حافظه های SSD (با کاهش میزان نوشتن روی حافظه\\!) این کار میتونه باعث سریع شدن زمان شروع دانلود ها و مصرف کمتر از حافظه بشه. اگر دانلود ها به صورت آهسته شروع شدن و یا سرعت دانلود پایین بود این گزینه را غیر فعال کنید چرا که ممکنه روی همه دستگاه ها پشتیبانی نشه.\nsettings_ignore_ssl_certificates=نادیده گرفتن گواهی های SSL\nsettings_ignore_ssl_certificates_description=بررسی گواهی SSL را غیرفعال می‌کند. فقط در صورت نیاز از این گزینه استفاده کنید، زیرا ممکن است اتصال شما را در معرض خطرات امنیتی قرار دهد.\nsettings_global_speed_limiter=محدودکننده سرعت کلی\nsettings_global_speed_limiter_description=سرعت کلی دانلود به این مقدار محدود می شود (0 به معنای بدون محدودیت)\nsettings_show_average_speed=نمایش سرعت میانگین\nsettings_show_average_speed_description=سرعت دانلود به‌صورت میانگین یا دقیق نمایش داده شود\nsettings_use_category_by_default=استفاده از دسته‌بندی به‌صورت پیشفرض\nsettings_use_category_by_default_description=هنگام افزودن دانلود به طور پیش‌فرض از دسته‌بندی استفاده شود.\nsettings_default_download_folder=پوشه دانلود پیش‌فرض\nsettings_default_download_folder_description=هنگامی که دانلود جدیدی اضافه می‌کنید، این مکان به‌طور پیش‌فرض استفاده می‌شود\nsettings_default_download_folder_describe=\"{{folder}}\" استفاده خواهد شد\nsettings_use_proxy=استفاده از پروکسی\nsettings_use_proxy_description=برای دانلود فایل‌ها از پروکسی استفاده شود\nsettings_use_proxy_describe_no_proxy=پروکسی استفاده نخواهد شد\nsettings_use_proxy_describe_system_proxy=پروکسی سیستم استفاده خواهد شد\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" استفاده خواهد شد\nsettings_use_proxy_describe_pac_proxy=فایل pac با این آدرس استفاده خواهد شد\\: {{value}}\nsettings_track_deleted_files_on_disk=رهگیری فایل‌های از دست‌رفته از روی حافظه\nsettings_track_deleted_files_on_disk_description=اگر فایل ها از مسیر دانلود حذف یا جابجا شوند، به‌صورت خودکار از لیست دانلود پاک خواهند شد.\nsettings_delete_partial_file_on_download_cancellation=پاک کردن فایل ناتمام هنگام لغو دانلود\nsettings_delete_partial_file_on_download_cancellation_description=وقتی یک دانلود لغو می‌شود، فایل نیمه‌دانلودشده از روی دیسک حذف خواهد شد. این کار به تمیز نگه داشتن پوشه‌ی دانلود و کاهش استفاده‌ی بیهوده از فضای دیسک کمک می‌کند. با این حال، در صورت شروع مجدد، دانلود از ابتدا آغاز خواهد شد.\nsettings_default_user_agent=User-Agent پیشفرض\nsettings_default_user_agent_description=می‌توانید یک User Agent پیش‌فرض را مشخص کنید تا نحوه شناسایی درخواست‌ها برای سرورها تعیین شود. این‌کار می‌تواند در دسترسی به محتوای بهینه‌شده برای دستگاه‌های خاص یا دور زدن محدودیت‌های دانلود برخی وب‌سایت‌ها مفید باشد.\nsettings_download_size_unit=واحد حجم دانلود\nsettings_download_size_unit_description=واحد مورد استفاده برای نمایش حجم دانلود\nsettings_download_speed_unit=واحد سرعت دانلود\nsettings_download_speed_unit_description=واحد مورد استفاده برای نمایش سرعت دانلود\nsettings_theme=تم\nsettings_theme_description=انتخاب تم برای برنامه\nsettings_default_dark_theme=تم تیره پیش‌فرض\nsettings_default_dark_theme_description=هنگامی که برنامه از تم سیستم پیروی می‌کند و تم سیستم تیره هست از این تم استفاده می‌شود\nsettings_default_light_theme=تم روشن پیش‌فرض\nsettings_default_light_theme_description=هنگامی که برنامه از تم سیستم پیروی می‌کند و تم سیستم روشن هست از این تم استفاده می‌شود\nsettings_font=فونت\nsettings_font_description=تغییر فونتی که در برنامه استفاده میشود. بعضی از فونت ها ممکن است در این برنامه به درستی نمایش داده نشوند.\nsettings_ui_scale=اندازه رابط کاربری\nsettings_ui_scale_description=تغییر اندازه المان و متن ها در صفحات\nsettings_language=زبان - Language\nsettings_compact_top_bar=نوار بالای جمع‌وجور\nsettings_compact_top_bar_description=ترکیب کردن نوار بالا با نوار عنوان هنگامی که پنجره اصلی به اندازه کافی فضا داشته باشد\nsettings_use_native_menu_bar=استفاده از نوار منوی سیستم\nsettings_use_native_menu_bar_description=از نوار منوی پیشفرض سیستم استفاده شود\nsettings_use_relative_date_time=استفاده از زمان نسبی\nsettings_use_relative_date_time_description=از قالب زمان/تاریخ نسبی برای نمایش تاریخ‌ها در برنامه استفاده کنید (مثلاً «2 روز پیش» به جای تاریخ و زمان دقیق)\nsettings_show_icon_labels=نمایش متن آیکون ها\nsettings_show_icon_labels_description=لیبل ها در صورت امکان زیر آیکون‌ها نمایش داده میشوند (مانند دکمه‌های نوار ابزار صفحه اصلی)\nsettings_use_system_tray=استفاده از System Tray\nsettings_use_system_tray_description=نمایش System Tray هنگامی که برنامه در حال اجراست\nsettings_start_on_boot=شروع هنگام ورود به سیستم\nsettings_start_on_boot_description=اجرای خودکار برنامه هنگام لاگین شدن کاربر\nsettings_notification_sound=صدای اعلان\nsettings_notification_sound_description=پخش صدا هنگام اعلان جدید\nsettings_browser_integration=یکپارچه‌سازی با مرورگر\nsettings_browser_integration_description=دریافت دانلودها از مرورگر\nsettings_browser_integration_server_port=پورت سرور\nsettings_browser_integration_server_port_description=پورت برای یکپارچه‌سازی با مرورگر\nsettings_browser_integration_server_port_describe=برنامه به پورت {{port}} گوش می‌دهد\nsettings_dynamic_part_creation=ایجاد پارت به‌صورت پویا\nsettings_dynamic_part_creation_description=هنگامی که یک پارت کامل شد، پارت دیگری ایجاد میشود تا سرعت دانلود افزایش یابد\nsettings_show_completion_dialog=نمایش پنجره تکمیل دانلود\nsettings_show_completion_dialog_description=وقتی که یک دانلود تمام شد به‌صورت خودکار صفحه تکمیل دانلود نمایش داده شود.\nsettings_show_download_progress_dialog=نمایش پنجره پیشرفت دانلود\nsettings_show_download_progress_dialog_description=وقتی که یک دانلود شروع شد به‌صورت خودکار پنجره پیشرفت دانلود نمایش داده شود.\nsettings_per_host_settings=تنظیمات برای هر هاست\nsettings_per_host_settings_descriptions=این تنظیمات به‌صورت خودکار روی دانلود های جدیدی که از یک هاست مشخص استفاده می‌کنند اعمال میشود.\nsettings_download_max_concurrent_downloads=حداکثر دانلود همزمان\nsettings_download_max_concurrent_downloads_description=حداکثر تعداد فایل‌هایی که می‌توانند به‌صورت هم‌زمان دانلود شوند (دانلودهایی که توسط صف‌ها مدیریت می‌شوند محاسبه نمی‌شوند؛ برای نامحدود بودن مقدار را روی 0 قرار دهید)\ndownload_item_settings_speed_limit=محدودیت سرعت\ndownload_item_settings_speed_limit_description=محدودیت سرعت دانلود برای این آیتم\ndownload_item_settings_show_download_completion_dialog=نمایش پنجره تکمیل دانلود\ndownload_item_settings_show_download_completion_dialog_description=وقتی که این دانلود تمام شد به‌صورت خودکار صفحه تکمیل دانلود نمایش داده شود.\ndownload_item_settings_shutdown_on_completion=در پایان سیستم خاموش شود\ndownload_item_settings_shutdown_on_completion_description=خاموش کردن خودکار سیستم هنگامی که این دانلود به پایان برسد.\ndownload_item_settings_thread_count=تعداد کانکشن ها\ndownload_item_settings_thread_count_description=چند کانکشن برای دانلود این آیتم استفاده شود (0 برای پیش‌فرض)\ndownload_item_settings_thread_count_describe={{count}} کانکشن برای این دانلود\ndownload_item_settings_username_description=اگر لینک نیازمند احراز هویت است، نام کاربری ارائه کنید\ndownload_item_settings_password_description=اگر لینک یک نیازمند احراز هویت است، رمز عبور ارائه کنید\ndownload_item_settings_download_page=صفحه دانلود\ndownload_item_settings_download_page_description=صفحه وبی که این دانلود در آن ایجاد شده\ndownload_item_settings_file_checksum=امضای فایل\ndownload_item_settings_file_checksum_description=درهَمَک (هش) به شما کمک می‌کند تا صحت فایل دانلود شده را بررسی کنید\ndownload_item_settings_user_agent=عامل کاربر\ndownload_item_settings_user_agent_description=یک User-Agent اختصاصی برای استفاده در این دانلود (برای استفاده از مقدار پیشفرض مقدار را خالی بگذارید)\nfile_checksum=جمع‌آزمای فایل\nfile_checksum_page=بررسی‌کننده‌ی جمع‌آزمای فایل\nfile_checksum_page_file_checksum_default_algorithm=الگوریتم پیش‌فرض\nfile_checksum_page_file_checksum_default_algorithm_help=در صورتی که جمع‌آزمای فایل‌ها ارائه نشده باشد از این الگوریتم پیش‌فرض برای محاسبه جمع‌آزمای فایل استفاده می‌شود.\nstart=شروع\ncalculated_checksum=امضای محاسبه شده\nsaved_checksum=جمع‌آزمای ذخیره شده\nchecksum_algorithm=الگوریتم\nfile_not_found=فایل پیدا نشد\ndownload_not_finished=دانلود تکمیل نشده\ndone=انجام شد\nwaiting=در انتظار\nmatches=مطابقت دارد\nnot_matches=مطابقت ندارد\ncopy_to_clipboard=کپی در کلیپ‌بورد\nusername=نام کاربری\npassword=رمز عبور\naverage_speed=سرعت میانگین\nexact_speed=سرعت دقیق\nunlimited=نامحدود\nuse_global_settings=استفاده از تنظیمات عمومی\ncant_run_browser_integration=نمی‌توان یکپارچه‌سازی مرورگر را اجرا کرد\ncant_open_file=نمی‌توان فایل را باز کرد\ncant_open_folder=نمی‌توان پوشه را باز کرد\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} سال\nrelative_time_long_months={{months}} ماه\nrelative_time_long_days={{days}} روز\nrelative_time_long_hours={{hours}} ساعت\nrelative_time_long_minutes={{minutes}} دقیقه\nrelative_time_long_seconds={{seconds}} ثانیه\nrelative_time_short_years={{years}} سال\nrelative_time_short_months={{months}} ماه\nrelative_time_short_days={{days}} روز\nrelative_time_short_hours={{hours}} ساعت\nrelative_time_short_minutes={{minutes}} دقیقه\nrelative_time_short_seconds={{seconds}} ثانیه\nrelative_time_left={{time}} مانده\nrelative_time_ago={{time}} پیش\nauto=خودکار\nunspecified=نامشخص\ncustom=دلخواه\nicon=آیکون\nauthor=سازنده\nlink=لینک\nsize=حجم\nstatus=وضعیت\nparts_info_downloaded_size=دانلود شده\nparts_info_total_size=کل\nspeed=سرعت\ntime_left=زمان باقیمانده\ndate_added=تاریخ اضافه شدن\ninfo=جزئیات\ndownload_page_downloaded_size=دانلود شده\ndownload_page_download_completed=دانلود پایان یافت\nresume_support=امکان از سرگیری\nyes=بله\nno=خیر\nparts_info=جزئیات پارت ها\ndisconnected=قطع شد\nreceiving_data=دریافت دیتا\nconnecting=در حال اتصال\nwarning=هشدار\nunsupported_resume_warning=این دانلود امکان از سرگیری ندارد و ممکن است بعدا مجبور شوید آن را در لیست دانلود ها \"ریستارت\" کنید\nstop_anyway=به هرحال متوقف شود\ncustomize_columns=ویرایش ستون ها\nreset=بازنشانی\nmonday=دوشنبه\ntuesday=سه‌شنبه\nwednesday=چهارشنبه\nthursday=پنجشنبه\nfriday=جمعه\nsaturday=شنبه\nsunday=یکشنبه\nproxy_open_system_proxy_settings=باز کردن تنظیمات پروکسی سیستم\nproxy_type=نوع پروکسی\nproxy_do_not_use_proxy_for=برای این ها از پروکسی استفاده نکن\nproxy_do_not_use_proxy_for_description=لیستی از لینک هایی که نباید برای آن ها از پروکسی استفاده شود\\nشما میتونین از * بعنوان wildcard استفاده کنید\\nبرای مثال 192.168.1.* example.com (با فاصله از هم جدا شوند)\nproxy_change_title=ویرایش پروکسی\nchange_proxy=ویرایش پروکسی\nproxy_no=بدون پروکسی\nproxy_system=پروکسی سیستم\nproxy_manual=پروکسی دستی\nproxy_pac=کانفیگ خودکار پروکسی (pac)\nproxy_pac_url=آدرس فایل کانفیگ خودکار پروکسی\naddress=آدرس\nport=پورت\naddress_and_port=آدرس و پورت\nuse_authentication=استفاده از احراز هویت\nwarning_you_may_have_to_restart_the_download_later=شما ممکنه بعدا مجبور بشید این دانلود را ریستارت کنید\\!\nedit_download_title=ویرایش دانلود\nedit_download_update_from_download_page=بروزرسانی از صفحه دانلود\nedit_download_update_from_download_page_description=مادامی که این صفحه باز است شما میتونانید به صفحه دانلود بروید و از آنجا روی دکمه دانلود کلیک کنید برناامه به‌صورت خودکار اطلاعات دانلود را دریافت و اپدیت میکند که شما میتوانید آن را ذخیره کنید.\nedit_download_saved_download_item_size_not_match=آیتم دانلود با سایز {{currentSize}} ذخیره شده است، که با سایز جدید {{newSize}} مطابقت ندارد.\ntranslators_page_thanks=با سپاس و قدردانی از کسانی که به ترجمه این پروژه کمک کردند ❤️\ntranslators=مترجم ها\nlanguage=زبان\ntranslators_contribute_title=بهبود ترجمه ها\ntranslators_contribute_description=میخواهید این پروژه را بهبود بدید؟ اگر زبان شما در لیست نیست یا به یک سری تغییرات نیاز دارد میتوانید ترجمه های خود را اضافه کنید و آن را بهتر کنید\\!\ncontribute=مشارکت\nmeet_the_translators=آشنایی با مترجمان\nlocalized_by_translators=بومی سازی شده توسط مترجمان\nconfirm_exit=تایید خروج\nconfirm_exit_description=آیا مطمئن هستید که میخواهید از AB Download Manager خارج شوید ؟\\nدانلود ها و صف های فعال متوقف خواهند شد\\!\nupdate=بروزرسانی\nupdate_updater=بروز کننده\nupdate_available=بروزرسانی دردسترس است\nupdate_error=خطای بروزرسانی\nupdate_available_suggest_to_to_update=شما با بروزرسانی میتوانید از آخرین قابلیت ها، پیشرفت ها بهبود های عملکردی بهره‌مند شوید.\nupdate_release_notes=یادداشت‌های انتشار\nupdate_check_for_update=بررسی برای بروزرسانی\nupdate_checking_for_update=درحال بررسی برای بروزرسانی\nupdate_no_update=شما از آخرین نسخه استفاده میکنید\nupdate_check_error=خطایی هنگام بررسی بروزرسانی رخ داد\nupdate_app_updated_to_version_n=برنامه به نسخه {{version}} بروزرسانی شد\ncreate_desktop_entry=ساخت ورودی دسکتاپ\nshutdown_alert=هشدار خاموشی\nsystem_shutdown_soon=سیستم به‌زودی خاموش می‌شود\\!\nsystem_shutdown_failed=خاموش کردن سیستم ناموفق بود\\!\nsystem_shutdown_soon_description=سیستم به‌زودی خاموش می‌شود. اگر هنوز در حال استفاده از رایانه هستید، لطفاً کارهای خود را ذخیره کنید یا خاموش شدن را لغو نمایید.\nsystem_shutdown_reason_queue_completed=تمام دانلودهای صف به پایان رسیده‌اند.\nsystem_shutdown_reason_queue_end_time_reached=زمان پایان برنامه‌ریزی‌شده برای صف دانلود فرا رسیده است.\nsystem_shutdown_download_finished=دانلود به پایان رسید.\nshutdown_now=همین حالا خاموش کن\nsettings_per_host_settings_new_host=<هاست جدید>\nsettings_per_host_settings_not_selected=ابتدا یک مورد جدید ایجاد یا انتخاب کنید\\!\nsettings_per_host_settings_host=هاست\nsettings_per_host_settings_host_description=این تنظیمات برای دانلودهایی که با این نام هاست (میزبان یا دامنه) مطابقت دارند اعمال می‌شوند.\\nکاراکترهای جایگزین (*) پشتیبانی می‌شوند (مثلاً\\: example.com, *.example.com — فقط یکی را استفاده کنید).\nsettings_browser_in_launcher=آیکون مرورگر در لانچر\nsettings_browser_in_launcher_description=نمایش یا عدم نمایش آیکون مرورگر در صفحه برنامه ها (لانچر).\nsort_by=مرتب‌سازی بر اساس\nwelcome=خوش آمدید\nnew_folder=پوشه جدید\nskip=رد شدن\nlets_go=بزن بریم\nnext=بعدی\nselect_all=انتخاب همه\nselect_inside=انتخاب داخل\nselect_invert=انتخاب معکوس\nopen_settings=باز کردن تنظیمات\nback=برگشت\nservice_is_running=سرویس فعال است\ninitial_setup_description=بیا یکسری چیز هارو تنظیم کنیم\ninitial_setup_notice=شما میتوانید این تنظیمات را بعدا تغییر دهید\npermission_granted=اجازه داده شد\npermission_not_granted=اجازه داده نشد\npermissions=دسترسی ها\ngive_permission=اجازه دادن\ngive_storage_permission=اجازه دسترسی به حافظه\nstorage_roots=ریشه‌های فضای ذخیره‌سازی\npermissions_initial_title=تنظیم مجوز ها\npermissions_initial_description=برای اینکه برنامه به درستی کار کنه. برنامه به یکسری دسترسی ها نیاز داره. در صفحه بعدی خواهی دید هر دسترسی برای چه کاری استفاده میشه و میتونی تصمیم بگیری کدوم رو میخوای اجازه بدی یا رد بشی.\npermissions_done_title=همه چی آماده ست\npermissions_done_description=همه چیز آماده ست. به دسترسی های مورد نیاز اجازه داده شده و برنامه آماده استفاده ست.\npermissions_manage_storage_title=دسترسی مدیریت حافظه\npermissions_manage_storage_reason=این دسترسی به برنامه اجازه میده بتونه مسیر دانلود رو تغییر بده، دانلود های تکراری رو با دقت بیشتری تشخیص بده و یک سری قابلیت های دیگه. این دسترسی اختیاریه ولی برای تجربه بهتر، فعال بودنش پیشنهاد میشه.\npermission_read_write_external_storage_title=خواندن و نوشتن در حافظه\npermission_read_write_external_storage_reason=این دسترسی به برنامه اجازه میده دانلود هارو روی حافظه ذخیره و مدیریت کنه، مسیر دانلود رو تغییر بده و دانلود های تکراری رو بهتر تشخیص بده.\npermissions_post_notification_title=ارسال اعلان ها\npermissions_post_notification_reason=برای مدیریت دانلودها، برنامه باید در پس‌زمینه اجرا شود. اعلان‌ها برای اطلاع‌رسانی به شما و امکان اجرای برنامه در پس‌زمینه استفاده می‌شوند.\npermissions_ignore_battery_optimization_title=نادیده گرفتن بهینه‌سازی باتری\npermissions_ignore_battery_optimization_reason=برخی دستگاه‌ها فعالیت برنامه‌ها در پس‌زمینه را محدود می‌کنند و ممکن است دانلودها هنگام بسته بودن برنامه متوقف شوند. به‌صورت اختیاری می‌توانید برنامه را از بهینه‌سازی باتری مستثنا کنید تا دانلودها بدون وقفه ادامه داشته باشند\nopen_in_browser=باز کردن در مرورگر\nbrowser=مرورگر\nbrowser_new_tab=زبانه جدید\nbrowser_close_tab=بستن زبانه\nbrowser_open_in_new_tab=باز کردن در زبانه جدید\nbrowser_open_in_new_background_tab=باز کردن در زبانه جدید (پس زمینه)\nbrowser_no_tab_open=هیچ زبانه‌ای باز نیست\nbrowser_tabs=زبانه‌ها\nbrowser_paste_and_go=بچسبون و بریم\nbrowser_bookmarks=نشانک ها\nbrowser_add_bookmark=افزودن نشانک\nbrowser_edit_bookmark=ویرایش نشانک\nbrowser_add_to_bookmarks=افزودن به نشانک ها\nbrowser_remove_from_bookmarks=حذف از نشانک‌ ها\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/fi_FI.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Luokittele lataukset kategorioihin automaattisesti\nconfirm_auto_categorize_downloads_description=Kaikki luokittelemattomat kohteet lisätään automaattisesti niitä vastaaviin kategorioihin.\nconfirm_reset_to_default_categories_title=Palauta oletuskategoriat\nconfirm_reset_to_default_categories_description=Tämä POISTAA kaikki kategoriat ja palauttaa oletukset\\!\nconfirm_delete_download_items_title=Vahvista poisto\nconfirm_delete_download_items_description=Haluatko varmasti poistaa {{count}} kohdetta?\nconfirm_delete_download_unfinished_items_description=Haluatko varmasti poistaa {{count}} keskeneräistä latausta?\nconfirm_delete_download_finished_and_unfinished_items_description=Haluatko varmasti poistaa {{finishedCount}} valmistunutta ja {{unfinishedCount}} keskeneräistä latausta?\nalso_delete_file_from_disk=Poista tiedosto myös levyltä\nconfirm_delete_category_item_title=Poistetaan kategoriaa {{name}}\nconfirm_delete_category_item_description=Haluatko varmasti poistaa kategorian \"{{value}}\"?\nyour_download_will_not_be_deleted=Latauksiasi ei poisteta\ndrag_the_file_to_another_app=Vedä tiedosto toiseen sovellukseen\ndrop_link_or_file_here=Pudota linkki tai tiedosto tähän.\nnothing_will_be_imported=Mitään ei tuoda\nn_links_will_be_imported={{count}} linkkiä tuodaan\nn_items_selected={{count}} kohdetta valittu\nwindow_close=Sulje\nwindow_minimize=Pienennä\nwindow_maximize=Suurenna\nwindow_restore=Palauta\ndelete=Poista\nremove=Poista\ncancel=Peru\nclose=Sulje\nmenu=Valikko\nmore_options=Lisää vaihtoehtoja\nok=Ok\nadd=Lisää\npaste=Liitä\nchange=Vaihda\nedit=Muokkaa\nchange_anyway=Vaihda silti\ndownload=Lataa\nrefresh=Päivitä\nsettings=Asetukset\non_completion=Valmistuminen\nunknown=Tuntematon\nunknown_error=Tuntematon virhe\ndownload_item_not_found=Latauskohdetta ei löytynyt\nname=Nimi\ndownload_link=Latauslinkki\nnot_finished=Kesken\nall=Kaikki\nfinished=Valmistunut\nUnfinished=Keskeneräiset\ncanceled=Peruttu\nerror=Virhe\npaused=Pysäytetty\ndownloading=Ladataan\nadded=Lisätty\nidle=TOIMETON\npreparing_file=Valmistellaan tiedostoa\ncreating_file=Luodaan tiedostoa\nresuming=Jatketaan\nretrying=Yritetään uudelleen\nlist_is_empty=Lista on tyhjä\\!\nsearch_in_the_list=Etsi listalta\nsearch=Haku\nclear=Tyhjennä\ngeneral=Yleiset\nenabled=Käytössä\ndisabled=Ei käytössä\ndefault=Oletus\nfile=Tiedosto\ntasks=Tehtävät\ntools=Työkalut\nhelp=Tuki\nsystem=Järjestelmä\nall_missing_files=Kaikki puuttuvat tiedostot\nall_finished=Kaikki valmistuneet\nall_unfinished=Kaikki keskeneräiset\nentire_list=Koko lista\ndownload_browser_integration=Lataa selainintegraatio\nexit=Sulje\nshow_downloads=Näytä lataukset\nnew_download=Lisää lataus\nstop_all=Pysäytä kaikki\nimport_from_clipboard=Tuo leikepöydältä\nbatch_download=Joukkolataus\nopen=Avaa\nshare=Jaa\nopen_file=Avaa tiedosto\nopen_folder=Avaa kansio\nresume=Jatka\npause=Pysäytä\nrestart_download=Aloita lataus uudelleen\ncopy=Kopioi\ncopy_link=Kopioi linkki\ncopy_as_curl=Kopioi cURL\nshow_properties=Näytä ominaisuudet\nmove_to_queue=Siirrä jonoon\nmove_to_this_queue=Siirrä tähän jonoon\nmove_to_category=Siirrä kategoriaan\nmove_to_this_category=Siirrä tähän kategoriaan\ncategories=Kategoriat\nadd_category=Lisää kategoria\nedit_category=Muokkaa kategoriaa\ndelete_category=Poista kategoria\ncategory_name=Kategorian nimi\ncategory_download_location=Kategorian lataussijainti\ncategory_download_location_description=Kun tämä kategoria valitaan \"Lisää lataus\" -ikkunasta, korvaa yleinen \"Lataussijainti\" tällä kansiolla.\ncategory_file_types=Kategorian tiedostotyypit\ncategory_file_types_description=Määritä uutta latausta lisättäessä automaattisesti tähän kategoriaan luokiteltavat tiedostotyypit. Erottele tiedostopäätteet välilyönneillä (ext1 ext2 ...).\ncategory_url_patterns=URL-osoitesäännöt\ncategory_url_patterns_description=Määritä uutta latausta lisättäessä automaattisesti tähän kategoriaan luokiteltavat URL-osoitteet. Erottele osoitteet välilyönneillä ja käytä * (tähti) jokerimerkkinä.\nauto_categorize_downloads=Luokittele lataukset kategorioihin automaattisesti\nrestore_defaults=Palauta oletukset\nabout=Tietoja\nversion_n=Versio {{value}}\ndeveloped_with_love_for_you=Kehitetty ❤️ sinulle\ndonate=Lahjoita\nvisit_the_project_website=Avaa projektin verkkosivusto\nthis_is_a_free_and_open_source_software=Ilmainen avoimen lähdekoodin ohjelmisto\nview_the_source_code=Tarkastele lähdekoodia\nthird_party_libraries=Ulkopuoliset lisenssit\npowered_by_open_source_software=Toimii avoimen lähdekoodin voimalla\nview_the_open_source_licenses=Näytä avoimen lähdekoodin lisenssit\nsupport_and_community=Tuki ja yhteisö\ntelegram=Telegram\nchannel=Kanava\ngroup=Ryhmä\nadd_download=Lisää lataus\nadd_multi_download_page_header=Valitse kohteet, jotka haluat ladata\nsave_to=Tallennuskohde\nwhere_should_each_item_saved=Mihin kohteet tulee tallentaa?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Kohteita on useita\\! Valitse tapa, jolla haluat tallentaa ne.\neach_item_on_its_own_category=Jokainen kohde sitä vastaavaan kategoriaan\neach_item_on_its_own_category_description=Tiedostot tallennetaan niiden tyyppejä vastaaviin kategorioihin.\nall_items_in_one_category=Kaikki kohteet samaan kategoriaan\nall_items_in_one_category_description=Kaikki tiedostot tallennetaan valittuun kategoriaan.\nall_items_in_one_Location=Kaikki kohteet samaan sijaintiin\nall_items_in_one_Location_description=Kaikki tiedostot tallennetaan valittuun kansioon.\nunselected_all_items_in_specific_location_description=Kaikki tiedostot tallennetaan valitun kategorian sijaintiin.\nno_category_selected=Kategoriaa ei valittu\nno_categories_found=Kategorioita ei ole\ndownload_location=Lataussijainti\nlocation=Sijainti\nselect_queue=Valitse jono\nwithout_queue=Ilman jonoa\nuse_category=Käytä kategoriaa\ncant_write_to_this_folder=Tähän kansioon ei voida tallentaa\nfile_name_already_exists=Tiedostonimi on jo olemassa\ndownload_already_exists=Lataus on jo olemassa\ninvalid_file_name=Virheellinen tiedostonimi\nshow_solutions=Näytä ratkaisut...\nchange_solution=Vaihda ratkaisu\nselect_a_solution=Valitse ratkaisu\nselect_download_strategy_description=Lisättävä linkki löytyy jo latauslistoilta. Valitse mitä tehdään.\ndownload_strategy_add_a_numbered_file=Luo numeroitu tiedosto\ndownload_strategy_add_a_numbered_file_description=Lisää lataustiedoston nimeen järjestysnumero.\ndownload_strategy_override_existing_file=Korvaa olemassa oleva tiedosto\ndownload_strategy_override_existing_file_description=Poista aiempi lataus ja korvaa se.\ndownload_strategy_update_download_link=Päivitä olemassa oleva lataus\ndownload_strategy_update_download_link_description=Päivitä olemassa oleva latauslinkki ja sen käyttäjätiedot.\ndownload_strategy_show_downloaded_file=Näytä ladattu tiedosto\ndownload_strategy_show_downloaded_file_description=Näytä aiempi lataus, jotta voit jatkaa sen latausta tai avata sen.\nbatch_download_link_help=Syötä jokerimerkkejä sisältävä osoite (käytä *)\ninvalid_url=Virheellinen URL-osoite\nlist_is_too_large_maximum_n_items_allowed=Lista on liian suuri\\! Enintään {{count}} kohdetta sallitaan.\nenter_range=Syötä alue\nrange_from=Alkaen\nrange_to=Päättyen\nbatch_download_wildcard_length=Jokerimerkin muuttujan pituus\nfirst_link=Ensimmäinen osoite\nlast_link=Viimeinen osoite\nopen_source_software_used_in_this_app=Sovelluksessa käytetyt avoimen lähdekoodin projektit\nlinks=Linkit\nwebsite=Verkkosivusto\ndevelopers=Kehittäjät\nsource_code=Lähdekoodi\nlicense=Lisenssi\nno_license_found=Lisenssiä ei löytynyt\norganization=Organisaatio\nadd_new_queue=Lisää uusi jono\nqueue_name=Jonon nimi\nqueues=Jonot\nstop_queue=Pysäytä jono\nstart_queue=Käynnistä jono\nclear_queue_items=Tyhjennä jono\nconfig=Määritykset\nitems=Kohteet\nmove_down=Siirrä alemmas\nmove_up=Siirrä ylemmäs\nremove_queue=Poista jono\nqueue_name_help=Anna jonolle tunnistettava nimi\nqueue_name_describe=Jonon nimi on {{value}}\nqueue_max_concurrent_download=Samanaikaisten latausten määrä\nqueue_max_concurrent_download_description=Samanaikaisten latausten määrä tässä jonossa.\nqueue_automatic_stop=Automaattinen pysäytys\nqueue_automatic_stop_description=Pysäytä jono automaattisesti, kun siinä ei ole kohteita.\nqueue_scheduler=Ajoitus\nqueue_enable_scheduler=Käytä ajoitusta\nqueue_active_days=Aktiiviset päivät\nqueue_active_days_description=Päivät, joina ajoituksia käytetään.\nqueue_scheduler_enable_auto_start_time=Käytä automaattista käynnistystä\nqueue_scheduler_auto_start_time=Automaattisen käynnistyksen aika\nqueue_scheduler_enable_auto_stop_time=Käytä automaattista pysäytystä\nqueue_scheduler_auto_stop_time=Automaattisen pysäytyksen aika\nqueue_shutdown_on_completion=Sammuta järjestelmä jonon valmistuessa\nqueue_shutdown_on_completion_description=Sammuta järjestelmä automaattisesti, kun tämä jono valmistuu tai ajoitettu pysäytysaika saavutetaan.\nappearance=Ulkoasu\ndownload_engine=Latausmoottori\nbrowser_integration=Selainintegraatio\nsettings_download_max_retries_count=Latausyritysten enimmäismäärä\nsettings_download_max_retries_count_description=Määritä miten monta kertaa epäonnistunutta latausta yritetään uudelleen ennen luovuttamista.\nsettings_download_max_retries_count_describe_no_retries=Epäonnistuneita latauksia ei yritetä uudelleen\nsettings_download_max_retries_count_describe_n_retries=Epäonnistuneita latauksia yritetään uudelleen {{count}} kerran/kertaa\nsettings_download_thread_count=Säiemäärä\nsettings_download_thread_count_description=Säikeiden enimmäismäärä latausta kohden.\nsettings_download_thread_count_describe=Latauksella voi olla enintään {{count}} säiettä\nsettings_download_thread_count_with_large_value_describe=Varoitus\\: Korkea säiemäärä voi lisätä järjestelmän kuormitusta, heikentää suorituskykyä tai aiheuttaa yhteysongelmia palvelimien kanssa. Käytä korkeampia arvoja vain, jos ymmärrät miten ne voivat vaikuttaa järjestelmääsi ja yhteyteesi.\nsettings_use_server_last_modified_time=Käytä palvelimen viimeisintä muokkausaikaa\nsettings_use_server_last_modified_time_description=Käytä paikalliselle tiedostolle palvelimen viimeisintä muokkausaikaa.\nsettings_append_extension_to_incomplete_downloads=Lisää keskeneräisiin latauksiin tiedostopääte\nsettings_append_extension_to_incomplete_downloads_description=Lisää keskeneräisiin latauksiin \".part\"-tiedostopääte. Tämä auttaa tunnistamaan keskeneräiset lataukset ja estää keskeneräisten tiedostojen avaamisen tai käsittelyn vahingossa.\nsettings_use_sparse_file_allocation=Tiedostojen sparse-varaus\nsettings_use_sparse_file_allocation_description=Luo tiedostot tehokkaammin ja vähennä erityisesti SSD-laitteissa tarpeetonta tiedontallennusta. Tämä voi nopeuttaa latauksia ja vähentää levyn käyttöä. Jos lataukset käynnistyvät hitaasti tai koet poikkeavia latausnopeuksia, harkitse tämän käytöstä poistoa, koska sitä ei välttämättä tueta täysin joissakin laitteissa.\nsettings_ignore_ssl_certificates=Älä huomioi SSL-varmenteita\nsettings_ignore_ssl_certificates_description=Poistaa SSL-varmenteiden vahvistuksen käytöstä. Altistaa yhteytesi tietoturvariskeille, joten käytä vain tarvittaessa.\nsettings_global_speed_limiter=Yleinen nopeusrajoitin\nsettings_global_speed_limiter_description=Yleinen latausnopeuden rajoitus (0 on rajoittamaton).\nsettings_show_average_speed=Näytä keskinopeus\nsettings_show_average_speed_description=Näytä keskimääräinen tai tarkka latausnopeus.\nsettings_use_category_by_default=Käytä kategorioita oletusarvoisesti\nsettings_use_category_by_default_description=Luokittele lataukset oletusarvoisesti kategorioihin latauksia lisättäessä.\nsettings_default_download_folder=Oletusarvoinen latauskansio\nsettings_default_download_folder_description=Kun lisäät uuden latauksen, tallennetaan se oletusarvoisesti tähän sijaintiin.\nsettings_default_download_folder_describe=Käytetään sijaintia \"{{folder}}\".\nsettings_use_proxy=Käytä välityspalvelinta\nsettings_use_proxy_description=Lataa tiedostot välityspalvelimen kautta.\nsettings_use_proxy_describe_no_proxy=Välityspalvelinta ei käytetä.\nsettings_use_proxy_describe_system_proxy=Käytetään järjestelmän välityspalvelinta.\nsettings_use_proxy_describe_manual_proxy=Käytetään välityspalvelinta \"{{value}}\".\nsettings_use_proxy_describe_pac_proxy=Käytetään PAC-tiedostoa \"{{value}}\".\nsettings_track_deleted_files_on_disk=Valvo tiedostojen poistumista levyltä\nsettings_track_deleted_files_on_disk_description=Poista tiedostot latauslistalta automaattisesti, kun ne poistetaan tai siirretään latauskansiosta.\nsettings_delete_partial_file_on_download_cancellation=Poista osittainen tiedosto kun lataus perutaan\nsettings_delete_partial_file_on_download_cancellation_description=Osittain ladattu tiedosto poistetaan levyltä, kun lataus perutaan. Tämä pitää latauskansion siistinä ja vähentää tallennustilan tarpeetonta varausta, joskin lataus aloitetaan alusta kun se seuraavan kerran käynnistetään.\nsettings_default_user_agent=Oletusarvoinen käyttäjäagentti\nsettings_default_user_agent_description=Määritä oletusarvoinen merkkijono palvelimille lähetettävälle käyttäjäagentille. Tämä voi auttaa käyttämään tietyille laitteille optimoitua sisältöä tai kiertämään joidenkin verkkosivustojen asettamia latausrajoituksia.\nsettings_download_size_unit=Latauskoon yksikkö\nsettings_download_size_unit_description=Yksikkö, jota käytetään latauksen koon esitykseen.\nsettings_download_speed_unit=Latausnopeuden yksikkö\nsettings_download_speed_unit_description=Latausnopeuksien esitykseen käytettävä yksikkö.\nsettings_theme=Teema\nsettings_theme_description=Valitse sovelluksen ulkoasuteema.\nsettings_default_dark_theme=Oletusarvoinen tumma teema\nsettings_default_dark_theme_description=Käytetään, kun sovellus seuraa järjestelmän teemaa ja se on tummassa tilassa.\nsettings_default_light_theme=Oletusarvoinen vaalea teema\nsettings_default_light_theme_description=Käytetään, kun sovellus seuraa järjestelmän teemaa ja se on vaaleassa tilassa.\nsettings_font=Fontti\nsettings_font_description=Muuta sovelluksen käyttöliittymän kirjasin. Jotkin kirjasimet eivät välttämättä näy sovelluksessa oikein.\nsettings_ui_scale=Käyttöliittymän skaalaus\nsettings_ui_scale_description=Säädä sovelluksen käyttöliittymän elementtien kokoa.\nsettings_language=Kieli\nsettings_compact_top_bar=Kompakti yläpalkki\nsettings_compact_top_bar_description=Yhdistä yläpalkki otsikkopalkkiin pääikkunan ollessa riittävän leveä.\nsettings_use_native_menu_bar=Käytä natiivia valikkopalkkia\nsettings_use_native_menu_bar_description=Käytä järjestelmän oletusarvoista valikkopalkin tyyliä.\nsettings_use_relative_date_time=Käytä suhteellista päiväystä/aikaa\nsettings_use_relative_date_time_description=Näytä päiväykset suhteellisessa muodossa, eli tarkan päiväyksen sijaan näytetään esim. \"2 päivää sitten\".\nsettings_show_icon_labels=Näytä kuvakkeiden tekstit\nsettings_show_icon_labels_description=Näytä tekstit kuvakkeiden alla mikäli mahdollista (työkalupalkin toimintojen tapaan).\nsettings_use_system_tray=Käytä järjestelmän ilmaisinaluetta\nsettings_use_system_tray_description=Näytä kuvake ilmaisinalueella sovelluksen ollessa käynnissä.\nsettings_start_on_boot=Käynnistä automaattisesti\nsettings_start_on_boot_description=Käynnistä sovellus automaattisesti käyttäjän kirjautuessa.\nsettings_notification_sound=Ilmoitusääni\nsettings_notification_sound_description=Toista ääni uusille ilmoituksille.\nsettings_browser_integration=Selainintegraatio\nsettings_browser_integration_description=Vastaanota latauksia selaimista.\nsettings_browser_integration_server_port=Palvelimen portti\nsettings_browser_integration_server_port_description=Selainintegrointiin käytettävä portti.\nsettings_browser_integration_server_port_describe=Sovellus kuuntelee porttia {{port}}.\nsettings_dynamic_part_creation=Dynaaminen osiointi\nsettings_dynamic_part_creation_description=Kun osan lataus valmistuu, paranna latausnopeutta jakamalla jäljellä olevat osat uudelleen.\nsettings_show_completion_dialog=Näytä ilmoitus latauksen valmistuessa\nsettings_show_completion_dialog_description=Ilmoita latauksen valmistumisesta avaamalla \"Lataus on valmistunut\" -ikkuna.\nsettings_show_download_progress_dialog=Näytä latauskohtaiset ikkunat\nsettings_show_download_progress_dialog_description=Näytä latauksen tilan kertova ikkuna kun lataus alkaa.\nsettings_per_host_settings=Isäntäkohtaiset asetukset\nsettings_per_host_settings_descriptions=Näitä asetuksia sovelletaan automaattisesti uusiin latauksiin, jotka vastaavat määritettyä isäntää.\nsettings_download_max_concurrent_downloads=Samanaikaisten latausten määrä\nsettings_download_max_concurrent_downloads_description=Enimmäismäärä tiedostoja, jotka voidaan ladata samaan aikaan (jonojen hallinnoimia latauksia ei lasketa; poista rajoitus asettamalla arvoksi 0).\ndownload_item_settings_speed_limit=Nopeusrajoitus\ndownload_item_settings_speed_limit_description=Rajoita tämän kohteen latausnopeutta.\ndownload_item_settings_show_download_completion_dialog=Näytä ilmoitus latauksen valmistuessa\ndownload_item_settings_show_download_completion_dialog_description=Ilmoita latauksen valmistumisesta avaamalla \"Lataus on valmistunut\" -ikkuna.\ndownload_item_settings_shutdown_on_completion=Sammuta järjestelmä latauksen valmistuessa\ndownload_item_settings_shutdown_on_completion_description=Sammuta järjestelmä automaattisesti, kun tämä lataus valmistuu.\ndownload_item_settings_thread_count=Säiemäärä\ndownload_item_settings_thread_count_description=Montaako säiettä kohteen lataukseen käytetään (0 \\= oletus).\ndownload_item_settings_thread_count_describe={{count}} säiettä tälle lataukselle\ndownload_item_settings_username_description=Syötä käyttäjätunnus, jos lähde edellyttää tunnistautumista.\ndownload_item_settings_password_description=Syötä salasana, jos lähde edellyttää tunnistautumista.\ndownload_item_settings_download_page=Lataussivu\ndownload_item_settings_download_page_description=Verkkosivu, jolta tämä lataus lisättiin.\ndownload_item_settings_file_checksum=Tiedoston tarkistussumma\ndownload_item_settings_file_checksum_description=Hajautusarvo, jonka avulla voidaan tarkastaa onko tiedosto ladattu oikein.\ndownload_item_settings_user_agent=Käyttäjäagentti\ndownload_item_settings_user_agent_description=Mukautettu käyttäjä-agentti tälle kohteelle (käytä oletusta jättämällä tyhjäksi).\nfile_checksum=Tiedoston tarkistussumma\nfile_checksum_page=Tiedoston tarkistussumman tarkistin\nfile_checksum_page_file_checksum_default_algorithm=Oletusarvoinen algoritmi\nfile_checksum_page_file_checksum_default_algorithm_help=Oletusalgoritmi, jota käytetään tarkistussumman laskentaan, kun tiedostokohtaista summaa ei ole ennalta ilmoitettu.\nstart=Käynnistä\ncalculated_checksum=Laskettu tarkistussumma\nsaved_checksum=Tallennettu tarkistussumma\nchecksum_algorithm=Algoritmi\nfile_not_found=Tiedostoa ei löytynyt\ndownload_not_finished=Lataus on vielä kesken\ndone=Suoritettu\nwaiting=Odottaa\nmatches=Vastaavuudet\nnot_matches=Vastaavuuksia ei ole\ncopy_to_clipboard=Kopioi leikepöydälle\nusername=Käyttäjätunnus\npassword=Salasana\naverage_speed=Keskinopeus\nexact_speed=Tarkka nopeus\nunlimited=Rajoittamaton\nuse_global_settings=Käytetään yleistä asetusta\ncant_run_browser_integration=Selainintegrointiin käytettävä portti\ncant_open_file=Tiedostoa ei voida avata\ncant_open_folder=Kansiota ei voida avata\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} vuotta\nrelative_time_long_months={{months}} kuukautta\nrelative_time_long_days={{days}} päivää\nrelative_time_long_hours={{hours}} tuntia\nrelative_time_long_minutes={{minutes}} minuuttia\nrelative_time_long_seconds={{seconds}} sekuntia\nrelative_time_short_years={{years}} v\nrelative_time_short_months={{months}} kk\nrelative_time_short_days={{days}} pv\nrelative_time_short_hours={{hours}} t\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} s\nrelative_time_left={{time}} jäljellä\nrelative_time_ago={{time}} sitten\nauto=Automaattinen\nunspecified=Ei määritetty\ncustom=Mukautettu\nicon=Kuvake\nauthor=Tekijä\nlink=Osoite\nsize=Koko\nstatus=Tila\nparts_info_downloaded_size=Ladattu\nparts_info_total_size=Koko\nspeed=Nopeus\ntime_left=Aikaa jäljellä\ndate_added=Lisäysaika\ninfo=Tiedot\ndownload_page_downloaded_size=Ladattu\ndownload_page_download_completed=Lataus on valmistunut\nresume_support=Tukee tauotusta\nyes=Kyllä\nno=Ei\nparts_info=Osien tiedot\ndisconnected=Ei yhteyttä\nreceiving_data=Vastaanotetaan\nconnecting=Yhdistetään\nwarning=Varoitus\nunsupported_resume_warning=Latauksen palvelin ei tue keskeytetyn latauksen jatkamista\\! Joudut myöhemmin aloittamaan sen uudelleen alusta.\nstop_anyway=Pysäytä silti\ncustomize_columns=Muokkaa sarakkeita\nreset=Palauta\nmonday=Maanantai\ntuesday=Tiistai\nwednesday=Keskiviikko\nthursday=Torstai\nfriday=Perjantai\nsaturday=Lauantai\nsunday=Sunnuntai\nproxy_open_system_proxy_settings=Avaa järjestelmän välityspalvelinasetukset\nproxy_type=Välityspalvelimen tyyppi\nproxy_do_not_use_proxy_for=Älä käytä välityspalvelinta seuraaville\nproxy_do_not_use_proxy_for_description=Luettelo URL-osoitteista, joille ei käytetä välityspalvelinta.\\nErottele osoitteet välilyönneillä ja käytä * (tähti) jokerimerkkinä,\\nesim. 192.168.1.* esimerkki.fi.\nproxy_change_title=Määritä välityspalvelin\nchange_proxy=Määritä välityspalvelin\nproxy_no=Ei välityspalvelinta\nproxy_system=Järjestelmän välityspalvelin\nproxy_manual=Määritä välityspalvelin itse\nproxy_pac=Automaattinen välityspalvelin\nproxy_pac_url=Välityspalvelimen automaattimäärityksen URL-osoite\naddress=Osoite\nport=Portti\naddress_and_port=Osoite ja portti\nuse_authentication=Käytä tunnistautumista\nwarning_you_may_have_to_restart_the_download_later=Saatat joutua aloittamaan latauksen myöhemmin uudelleen\\!\nedit_download_title=Muokkaa latausta\nedit_download_update_from_download_page=Päivitä lataussivulta\nedit_download_update_from_download_page_description=Tämän ikkunan ollessa avoinna voit avata lataussivun ja painaa latauspainiketta, jolloin sovellus kaappaa ja päivittää uudet käyttäjätiedot, jotta voit tallentaa ne.\nedit_download_saved_download_item_size_not_match=Tallennetun latauskohteen koko on {{currentSize}}, joka ei vastaa uutta kokoa {{newSize}}.\ntranslators_page_thanks=Kiitos projektin käännökseen osallistuneille ❤️\ntranslators=Kääntäjät\nlanguage=Kieli\ntranslators_contribute_title=Paranna käännöksiä\ntranslators_contribute_description=Haluatko auttaa projektissa? Mikäli kieltäsi ei ole listattu tai se kaipaa korjausta, voit osallistua käännökseen ja parantaa sitä\\!\ncontribute=Osallistu\nmeet_the_translators=Tutustu kääntäjiin\nlocalized_by_translators=Kääntäjien lokalisoima\nconfirm_exit=Vahvista sulku\nconfirm_exit_description=Haluatko varmasti sulkea AB Download Managerin?\\nAktiiviset lataukset/jonot pysäytetään\\!\nupdate=Päivitä\nupdate_updater=Päivittäjä\nupdate_available=Päivitys saatavilla\nupdate_error=Päivitysvirhe\nupdate_available_suggest_to_to_update=Päivitä uusimpaan versioon nauttiaksesi uusista ominaisuuksista, korjauksista ja suorituskykyparannuksista.\nupdate_release_notes=Muutoshistoria\nupdate_check_for_update=Tarkista päivitykset\nupdate_checking_for_update=Tarkistetaan päivityksiä\nupdate_no_update=Käytössäsi on uusin versio\nupdate_check_error=Virhe tarkistettaessa päivityksiä\nupdate_app_updated_to_version_n=Sovellus päivitettiin versioon {{version}}\ncreate_desktop_entry=Luo kuvake työpöydälle\nshutdown_alert=Sammutusilmoitus\nsystem_shutdown_soon=Järjestelmä sammuu pian\\!\nsystem_shutdown_failed=Järjestelmän sammutus epäonnistui\\!\nsystem_shutdown_soon_description=Järjestelmä sammuu pian. Jos käytät laitetta vielä, tallenna työsi tai peru sammutus.\nsystem_shutdown_reason_queue_completed=Kaikki jonon lataukset ovat valmistuneet.\nsystem_shutdown_reason_queue_end_time_reached=Jonolle asetettu pysäytysaika on saavutettu.\nsystem_shutdown_download_finished=Lataus on valmistunut.\nshutdown_now=Sammuta nyt\nsettings_per_host_settings_new_host=<Uusi isäntä>\nsettings_per_host_settings_not_selected=Luo tai valitse uusi kohde ensin\\!\nsettings_per_host_settings_host=Isäntä\nsettings_per_host_settings_host_description=Näitä asetuksia sovelletaan tätä isäntää vastaaviin latauksiin. Jokerimerkkejä (*) tuetaan (esim. esimerkki.fi, *.esimerkki.fi — käytä vain yhtä).\nsettings_browser_in_launcher=Selaimen kuvake käynnistimessä\nsettings_browser_in_launcher_description=Näytä selaimen kuvake käynnistimessä tai piilota se (sovelluslista).\nsort_by=Järjestä\nwelcome=Tervetuloa\nnew_folder=Uusi kansio\nskip=Ohita\nlets_go=Aloitetaan\nnext=Seuraava\nselect_all=Valitse kaikki\nselect_inside=Valitse sisältö\nselect_invert=Käännä valinta\nopen_settings=Avaa asetukset\nback=Takaisin\nservice_is_running=Palvelu on käynnissä\ninitial_setup_description=Määritetään ominaisuudet\ninitial_setup_notice=Voit muuttaa näitä asetuksia milloin tahansa\npermission_granted=Käyttöoikeus on myönnetty\npermission_not_granted=Käyttöoikeutta ei ole myönnetty\npermissions=Käyttöoikeudet\ngive_permission=Myönnä käyttöoikeus\ngive_storage_permission=Myönnä tallennustilan käyttöoikeus\nstorage_roots=Storage Roots\npermissions_initial_title=Määritetään asetukset\npermissions_initial_description=Jotta sovellus toimii oikein, tarvitsee se muutamia käyttöoikeuksia. Seuraavalla näytöllä näet miten kutakin käyttöoikeutta käytetään ja voit valita, mitkä sallitaan ja mitkä ohitetaan.\npermissions_done_title=Kaikki kunnossa\npermissions_done_description=Määritykset on tehty, kaikki tarvittavat käyttöoikeudet on myönnetty ja sovellus on valmis käyttöön.\npermissions_manage_storage_title=Hallitse tallennustilan käyttöoikeutta\npermissions_manage_storage_reason=Tällä käyttöoikeudella sovellus voi vaihtaa latauskohdetta, tunnistaa latausten kaksoiskappaleet paremmin ja käyttää joitakin lisäominaisuuksia. Tämä on valinnainen, mutta suositeltava parasta käyttökokemusta varten.\npermission_read_write_external_storage_title=Tiedostojen luku- ja tallennusoikeus\npermission_read_write_external_storage_reason=Tämä käyttöoikeus sallii sovelluksen tallentaa ja hallita ladattuja tiedostoja, vaihtaa latauskohdetta ja parantaa latausten kaksoiskappaleiden tunnistusta.\npermissions_post_notification_title=Ilmoitusoikeus\npermissions_post_notification_reason=Latausten hallinta edellyttää, että sovellus suoritetaan taustalla ja ilmoitusten avulla taustatoiminnot sallitaan, ja sinut pidetään ajan tasalla.\npermissions_ignore_battery_optimization_title=Älä huomioi akkuvirran säästöä\npermissions_ignore_battery_optimization_reason=Jotkin laiteet rajoittavat taustatoimintoja aggressiivisesti akun säästämiseksi, jonka seurauksena lataukset saattavat pysähtyä kun sovellus ei ole avoinna. Halutessasi voit sulkea sovelluksen optimoinnin ulkopuolelle, jotta lataukset jatkuvat keskeytyksettä.\nopen_in_browser=Avaa selaimessa\nbrowser=Selain\nbrowser_new_tab=Uusi välilehti\nbrowser_close_tab=Sulje välilehti\nbrowser_open_in_new_tab=Avaa uudella välilehdellä\nbrowser_open_in_new_background_tab=Avaa uudella taustavälilehdellä\nbrowser_no_tab_open=Välilehtiä ei ole avoinna\nbrowser_tabs=Välilehdet\nbrowser_paste_and_go=Liitä ja avaa\nbrowser_bookmarks=Kirjamerkit\nbrowser_add_bookmark=Lisää kirjanmerkki\nbrowser_edit_bookmark=Muokkaa kirjanmerkkiä\nbrowser_add_to_bookmarks=Lisää kirjanmerkkeihin\nbrowser_remove_from_bookmarks=Poista kirjanmerkeistä\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/fr_FR.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Catégoriser automatiquement les téléchargements\nconfirm_auto_categorize_downloads_description=Tout élément non classé sera automatiquement ajouté à sa catégorie correspondante.\nconfirm_reset_to_default_categories_title=Rétablir les catégories par défaut\nconfirm_reset_to_default_categories_description=Ceci SUPPRIMERA toutes vos catégories et rétablira les catégories par défaut \\!\nconfirm_delete_download_items_title=Confirmer la suppression\nconfirm_delete_download_items_description=Êtes-vous sûr de vouloir supprimer {{count}} éléments ?\nconfirm_delete_download_unfinished_items_description=Êtes-vous sûr de vouloir supprimer {{count}} téléchargements non terminés ?\nconfirm_delete_download_finished_and_unfinished_items_description=Êtes-vous sûr de vouloir supprimer les {{finishedCount}} téléchargements terminés et {{unfinishedCount}} non terminés ?\nalso_delete_file_from_disk=Supprimer également du disque\nconfirm_delete_category_item_title=Suppression de la catégorie {{name}}\nconfirm_delete_category_item_description=Êtes-vous sûr de vouloir supprimer la catégorie \"{{value}}\" ?\nyour_download_will_not_be_deleted=Vos téléchargements ne seront pas supprimés\ndrag_the_file_to_another_app=Glissez le fichier vers une autre application\ndrop_link_or_file_here=Déposez un lien ou un fichier ici.\nnothing_will_be_imported=Rien ne sera importé\nn_links_will_be_imported={{count}} liens seront importés\nn_items_selected={{count}} éléments sélectionnés\nwindow_close=Fermer\nwindow_minimize=Réduire\nwindow_maximize=Agrandir\nwindow_restore=Rétablir\ndelete=Supprimer\nremove=Retirer\ncancel=Annuler\nclose=Fermer\nmenu=Menu\nmore_options=Plus d'options\nok=D'accord\nadd=Ajouter\npaste=Coller\nchange=Changer\nedit=Modifier\nchange_anyway=Changer quand même\ndownload=Télécharger\nrefresh=Actualiser\nsettings=Paramètres\non_completion=À l'achèvement\nunknown=Inconnu\nunknown_error=Erreur inconnue\ndownload_item_not_found=L'élément à télécharger n'a pas été trouvé\nname=Nom\ndownload_link=Lien de téléchargement\nnot_finished=Pas terminé\nall=Tout\nfinished=Terminé\nUnfinished=Incomplet\ncanceled=Annulé\nerror=Erreur\npaused=Suspendu\ndownloading=Téléchargement en cours\nadded=Ajouté\nidle=INACTIF\npreparing_file=Préparation du fichier\ncreating_file=Création du fichier\nresuming=Reprise en cours\nretrying=Nouvelle tentative\nlist_is_empty=Rien ici pour l'instant \\!\nsearch_in_the_list=Rechercher\nsearch=Rechercher\nclear=Vider\ngeneral=Général\nenabled=Activé\ndisabled=Désactivé\ndefault=Par défaut\nfile=Fichier\ntasks=Tâches\ntools=Outils\nhelp=Aide\nsystem=Système\nall_missing_files=Tous les fichiers manquants\nall_finished=Terminés\nall_unfinished=Incomplets\nentire_list=Tout\ndownload_browser_integration=Télécharger l'extension du navigateur\nexit=Quitter\nshow_downloads=Afficher les téléchargements\nnew_download=Nouveau téléchargement\nstop_all=Arrêter tout\nimport_from_clipboard=Importer depuis le presse-papiers\nbatch_download=Téléchargement groupé\nopen=Ouvrir\nshare=Partager\nopen_file=Ouvrir le fichier\nopen_folder=Ouvrir le dossier\nresume=Reprendre\npause=Suspendre\nrestart_download=Redémarrer le téléchargement\ncopy=Copier\ncopy_link=Copier le lien\ncopy_as_curl=Copier comme cURL\nshow_properties=Propriétés\nmove_to_queue=Déplacer vers une file d'attente\nmove_to_this_queue=Déplacer vers cette file d'attente\nmove_to_category=Déplacer vers une catégorie\nmove_to_this_category=Déplacer vers cette catégorie\ncategories=Catégories\nadd_category=Ajouter une catégorie\nedit_category=Modifier la catégorie\ndelete_category=Supprimer la catégorie\ncategory_name=Nom de la catégorie\ncategory_download_location=Emplacement de téléchargement de la catégorie\ncategory_download_location_description=Lorsque cette catégorie est choisie dans \"Ajouter un téléchargement\", utilisez ce répertoire comme \"Emplacement de téléchargement\"\ncategory_file_types=Types de fichiers de la catégorie\ncategory_file_types_description=Placez automatiquement ces types de fichiers dans cette catégorie. (lors de l'ajout d'un nouveau téléchargement)\\nSéparez les extensions de fichiers par un espace (ext1 ext2 ...)\ncategory_url_patterns=Modèles d'URL\ncategory_url_patterns_description=Placez automatiquement le téléchargement à partir de ces URL dans cette catégorie. (lors de l'ajout d'un nouveau téléchargement)\\nSéparez les URL par un espace, vous pouvez également utiliser * comme caractère générique\nauto_categorize_downloads=Catégoriser automatiquement les téléchargements\nrestore_defaults=Rétablir les valeurs par défaut\nabout=À propos\nversion_n=v{{value}}\ndeveloped_with_love_for_you=Développé avec ❤️ pour vous\ndonate=Contribuer\nvisit_the_project_website=Visiter le site web du projet\nthis_is_a_free_and_open_source_software=Ceci est un logiciel libre et gratuit\nview_the_source_code=Voir le code source\nthird_party_libraries=Bibliothèques tierces\npowered_by_open_source_software=Propulsé par des logiciels libres\nview_the_open_source_licenses=Voir les licences Open-Source\nsupport_and_community=Assistance et Communauté\ntelegram=Telegram\nchannel=Chaîne\ngroup=Groupe\nadd_download=Ajouter un téléchargement\nadd_multi_download_page_header=Sélectionnez les éléments que vous souhaitez récupérer pour le téléchargement\nsave_to=Enregistrer dans\nwhere_should_each_item_saved=Où chaque élément doit-il être sauvegardé ?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Il y a plusieurs éléments \\! Veuillez sélectionner la manière dont vous souhaitez les enregistrer\neach_item_on_its_own_category=Chaque élément dans sa propre catégorie\neach_item_on_its_own_category_description=Chaque élément sera placé dans une catégorie qui contient ce type de fichier\nall_items_in_one_category=Tous les éléments dans une catégorie\nall_items_in_one_category_description=Tous les fichiers seront enregistrés à l'emplacement de la catégorie sélectionnée\nall_items_in_one_Location=Tous les éléments dans un seul emplacement\nall_items_in_one_Location_description=Tous les éléments seront enregistrés dans le répertoire sélectionné\nunselected_all_items_in_specific_location_description=Tous les fichiers seront enregistrés à l'emplacement de la catégorie sélectionnée\nno_category_selected=Aucune catégorie sélectionnée\nno_categories_found=Aucune catégorie trouvée\ndownload_location=Emplacement du téléchargement\nlocation=Emplacement\nselect_queue=Sélectionner la file d’attente\nwithout_queue=Sans file d'attente\nuse_category=Catégoriser\ncant_write_to_this_folder=Impossible d'écrire dans ce dossier\nfile_name_already_exists=Le nom du fichier existe déjà\ndownload_already_exists=Le téléchargement existe déjà\ninvalid_file_name=Nom de fichier invalide\nshow_solutions=Afficher les solutions...\nchange_solution=Changer la solution\nselect_a_solution=Sélectionnez une solution\nselect_download_strategy_description=Le lien que vous avez fourni est déjà dans les listes de téléchargement, veuillez préciser ce que vous souhaitez faire\ndownload_strategy_add_a_numbered_file=Ajouter un fichier numéroté\ndownload_strategy_add_a_numbered_file_description=Ajouter un index à la fin du nom du fichier de téléchargement\ndownload_strategy_override_existing_file=Écraser le fichier existant\ndownload_strategy_override_existing_file_description=Supprimer le téléchargement existant et écrire dans ce fichier\ndownload_strategy_update_download_link=Mettre à jour le téléchargement existant\ndownload_strategy_update_download_link_description=Mettre à jour le lien de téléchargement existant et ses identifiants\ndownload_strategy_show_downloaded_file=Afficher le fichier téléchargé\ndownload_strategy_show_downloaded_file_description=Afficher l'élément de téléchargement déjà existant, afin que vous puissiez le reprendre ou l'ouvrir\nbatch_download_link_help=Entrez un lien contenant des caractères génériques (utilisez *)\ninvalid_url=URL invalide\nlist_is_too_large_maximum_n_items_allowed=La liste est trop longue \\! {{count}} éléments maximum autorisés\nenter_range=Entrez la plage\nrange_from=De\nrange_to=à\nbatch_download_wildcard_length=Longueur du caractère générique\nfirst_link=Premier lien\nlast_link=Dernier lien\nopen_source_software_used_in_this_app=Logiciels Open-Source utilisés dans cette application\nlinks=Liens\nwebsite=Site web\ndevelopers=Développeurs\nsource_code=Code source\nlicense=Licence\nno_license_found=Aucune licence trouvée\norganization=Organisation\nadd_new_queue=Ajouter une nouvelle file d'attente\nqueue_name=Nom de la file d'attente\nqueues=Files d'attente\nstop_queue=Arrêter la file d'attente\nstart_queue=Démarrer la file d'attente\nclear_queue_items=Vider la file d'attente\nconfig=Configuration\nitems=Éléments\nmove_down=Descendre\nmove_up=Monter\nremove_queue=Supprimer la file d'attente\nqueue_name_help=Spécifiez un nom pour cette file d'attente\nqueue_name_describe=Le nom de la file d’attente est {{value}}\nqueue_max_concurrent_download=Téléchargements simultanés max\nqueue_max_concurrent_download_description=Téléchargements maximum pour cette file d'attente\nqueue_automatic_stop=Arrêt automatique\nqueue_automatic_stop_description=Arrêt automatique de la file d'attente lorsqu'elle ne contient plus d'éléments\nqueue_scheduler=Planificateur\nqueue_enable_scheduler=Activer le planificateur\nqueue_active_days=Jours d'activité\nqueue_active_days_description=Quels sont les jours de fonctionnement du planificateur ?\nqueue_scheduler_enable_auto_start_time=Activer le temps de démarrage automatique\nqueue_scheduler_auto_start_time=Temps de démarrage automatique\nqueue_scheduler_enable_auto_stop_time=Activer le temps d'arrêt automatique\nqueue_scheduler_auto_stop_time=Temps d'arrêt automatique\nqueue_shutdown_on_completion=Éteindre le système à l'achèvement\nqueue_shutdown_on_completion_description=Arrêtez automatiquement le système lorsque cette file d'attente est terminée ou lorsque l'heure de fin prévue est atteinte.\nappearance=Apparence\ndownload_engine=Moteur de téléchargement\nbrowser_integration=Intégration du navigateur\nsettings_download_max_retries_count=Nombre maximum de tentatives de téléchargement\nsettings_download_max_retries_count_description=Le nombre maximum de fois que l'application tentera à nouveau un téléchargement échoué avant d'abandonner\nsettings_download_max_retries_count_describe_no_retries=Les téléchargements qui ont échoué ne seront pas réessayés\nsettings_download_max_retries_count_describe_n_retries=Les téléchargements qui ont échoué seront réessayés {{count}} fois\nsettings_download_thread_count=Nombre de threads\nsettings_download_thread_count_description=Nombre maximal de threads de téléchargement par élément\nsettings_download_thread_count_describe=Un téléchargement peut utiliser jusqu''à {{count}} threads\nsettings_download_thread_count_with_large_value_describe=Avertissement \\: La définition d'un nombre de threads élevé peut augmenter l'utilisation des ressources système, réduire les performances ou provoquer des problèmes de connexion avec les serveurs. Utilisez des valeurs plus élevées uniquement si vous comprenez l'impact potentiel sur votre système et votre réseau.\nsettings_use_server_last_modified_time=Utiliser l'heure de dernière modification du serveur\nsettings_use_server_last_modified_time_description=Lors du téléchargement d'un fichier, utiliser l'heure de dernière modification du serveur pour le fichier local\nsettings_append_extension_to_incomplete_downloads=Ajouter une extension aux téléchargements incomplets\nsettings_append_extension_to_incomplete_downloads_description=Ajouter l'extension \".part\" aux téléchargements incomplets. Cela permet d'identifier les téléchargements inachevés et d'éviter l'ouverture accidentelle de fichiers incomplets.\nsettings_use_sparse_file_allocation=Allocation de fichier partiellement alloué\nsettings_use_sparse_file_allocation_description=Créez des fichiers plus efficacement, en particulier sur les disques SSD, en réduisant l'écriture de données inutiles. Cela peut accélérer le démarrage des téléchargements et réduire l'utilisation du disque. Si les téléchargements démarrent lentement ou si vous constatez des vitesses de téléchargement inhabituelles, envisagez de désactiver cette option, car il se peut qu'elle ne soit pas entièrement prise en charge sur certains appareils.\nsettings_ignore_ssl_certificates=Ignorer les certificats SSL\nsettings_ignore_ssl_certificates_description=Désactive la vérification des certificats SSL. À n'utiliser qu'en cas de nécessité, car cela peut exposer votre connexion à des risques de sécurité.\nsettings_global_speed_limiter=Limiteur de vitesse global\nsettings_global_speed_limiter_description=Limite globale de vitesse de téléchargement (0 \\= illimité)\nsettings_show_average_speed=Afficher la vitesse moyenne\nsettings_show_average_speed_description=Vitesse de téléchargement en moyenne ou exacte\nsettings_use_category_by_default=Catégoriser par défaut\nsettings_use_category_by_default_description=Catégoriser par défaut lors de l'ajout d'un fichier de téléchargement.\nsettings_default_download_folder=Dossier de téléchargement par défaut\nsettings_default_download_folder_description=Lorsque vous ajoutez un nouveau téléchargement, cet emplacement est utilisé par défaut\nsettings_default_download_folder_describe=\"{{folder}}\" sera utilisé\nsettings_use_proxy=Utiliser un proxy\nsettings_use_proxy_description=Utiliser un proxy pour télécharger des fichiers\nsettings_use_proxy_describe_no_proxy=Aucun proxy ne sera utilisé\nsettings_use_proxy_describe_system_proxy=Le proxy système sera utilisé\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" sera utilisé\nsettings_use_proxy_describe_pac_proxy=Le fichier pac \"{{value}}\" sera utilisé\nsettings_track_deleted_files_on_disk=Suivre les fichiers supprimés sur le disque\nsettings_track_deleted_files_on_disk_description=Supprimer automatiquement les fichiers de la liste lorsqu'ils sont supprimés ou déplacés du répertoire de téléchargement.\nsettings_delete_partial_file_on_download_cancellation=Supprimer le fichier partiel lors de l'annulation du téléchargement\nsettings_delete_partial_file_on_download_cancellation_description=Lorsqu'un téléchargement est annulé, le fichier partiellement téléchargé sera supprimé du disque. Cela aide à garder votre dossier de téléchargement propre et réduit l'utilisation inutile de l'espace disque. Cependant, le téléchargement redémarrera depuis le début la prochaine fois que vous le démarrez.\nsettings_default_user_agent=Agent utilisateur par défaut\nsettings_default_user_agent_description=Spécifiez la chaîne User-Agent par défaut pour définir la manière dont les requêtes s'identifient aux serveurs. Cela peut permettre d'accéder à des contenus optimisés pour des appareils particuliers ou de contourner les limitations de téléchargement imposées par certains sites web.\nsettings_download_size_unit=Unité de téléchargement\nsettings_download_size_unit_description=Unité utilisée pour afficher la taille du téléchargement\nsettings_download_speed_unit=Unité de vitesse de téléchargement\nsettings_download_speed_unit_description=Unité utilisée pour afficher la vitesse de téléchargement\nsettings_theme=Thème\nsettings_theme_description=Sélectionnez un thème pour l'application\nsettings_default_dark_theme=Thème sombre par défaut\nsettings_default_dark_theme_description=S'applique lorsque l'application suit le thème du système et que le mode sombre est activé\nsettings_default_light_theme=Thème clair par défaut\nsettings_default_light_theme_description=S'applique lorsque l'application suit le thème du système et que le mode clair est activé\nsettings_font=Police\nsettings_font_description=Modifier la police utilisée dans l'interface de l'application. Certaines polices peuvent ne pas s'afficher correctement dans l'application.\nsettings_ui_scale=Échelle de l'interface\nsettings_ui_scale_description=Ajuster la taille des éléments de l'interface de l'application\nsettings_language=Langue\nsettings_compact_top_bar=Barre supérieure compacte\nsettings_compact_top_bar_description=Fusionner la barre supérieure avec la barre de titre lorsque la fenêtre principale a suffisamment de largeur\nsettings_use_native_menu_bar=Utiliser la barre de menu native\nsettings_use_native_menu_bar_description=Utiliser le style de barre de menu par défaut du système\nsettings_use_relative_date_time=Utiliser la date/heure relative\nsettings_use_relative_date_time_description=Utiliser le format date/heure relatif pour les dates dans l'application (par exemple, \"il y a 2 jours\" au lieu de la date/heure exacte)\nsettings_show_icon_labels=Afficher les libellés des icônes\nsettings_show_icon_labels_description=Afficher les étiquettes sous les icônes lorsque possible (comme les actions de la barre d'outils)\nsettings_use_system_tray=Utiliser la barre d'état système\nsettings_use_system_tray_description=Afficher l'icône dans la barre d'état système lorsque l'application est en cours d'exécution\nsettings_start_on_boot=Lancer au démarrage\nsettings_start_on_boot_description=Démarrer automatiquement l'application lors de la connexion de l'utilisateur\nsettings_notification_sound=Son de notification\nsettings_notification_sound_description=Jouer un son lors d'une nouvelle notification\nsettings_browser_integration=Intégration du navigateur\nsettings_browser_integration_description=Accepter les téléchargements depuis le navigateur\nsettings_browser_integration_server_port=Port du serveur\nsettings_browser_integration_server_port_description=Port pour l'intégration du navigateur\nsettings_browser_integration_server_port_describe=L''application écoutera le port {{port}}\nsettings_dynamic_part_creation=Création dynamique de segments\nsettings_dynamic_part_creation_description=Lorsqu'un segment est terminé, créez un nouveau segment en divisant d'autres segments pour améliorer la vitesse de téléchargement\nsettings_show_completion_dialog=Fenêtre de fin de téléchargement\nsettings_show_completion_dialog_description=Afficher automatiquement la fenêtre \"Téléchargement terminé\" lorsqu'un téléchargement est terminé.\nsettings_show_download_progress_dialog=Fenêtre de progression du téléchargement\nsettings_show_download_progress_dialog_description=Afficher automatiquement la fenêtre \"Téléchargement en cours\" lorsqu'un téléchargement a commencé.\nsettings_per_host_settings=Paramètres par hôte\nsettings_per_host_settings_descriptions=Ces paramètres seront automatiquement appliqués à tout nouveau téléchargement qui correspond à l'hôte spécifié.\nsettings_download_max_concurrent_downloads=Téléchargements simultanés max.\nsettings_download_max_concurrent_downloads_description=Le nombre maximum de fichiers pouvant être téléchargés en même temps (les téléchargements gérés par les files d'attente ne sont pas comptés ; réglez sur 0 pour illimité)\ndownload_item_settings_speed_limit=Limite de vitesse\ndownload_item_settings_speed_limit_description=Limiter la vitesse de téléchargement pour cet élément\ndownload_item_settings_show_download_completion_dialog=Fenêtre de fin de téléchargement\ndownload_item_settings_show_download_completion_dialog_description=Afficher automatiquement la fenêtre \"Téléchargement terminé\" lorsque ce téléchargement est terminé.\ndownload_item_settings_shutdown_on_completion=Éteindre le système à l'achèvement\ndownload_item_settings_shutdown_on_completion_description=Éteindre automatiquement le système lorsque ce téléchargement est terminé.\ndownload_item_settings_thread_count=Nombre de threads\ndownload_item_settings_thread_count_description=Nombre de threads utilisés pour télécharger cet élément (0 \\= valeur par défaut)\ndownload_item_settings_thread_count_describe={{count}} threads pour ce téléchargement\ndownload_item_settings_username_description=Fournissez un nom d'utilisateur si le lien est une ressource protégée\ndownload_item_settings_password_description=Fournissez un mot de passe si le lien est une ressource protégée\ndownload_item_settings_download_page=Page de téléchargement\ndownload_item_settings_download_page_description=La page web où ce téléchargement a été lancé\ndownload_item_settings_file_checksum=Somme de contrôle du fichier\ndownload_item_settings_file_checksum_description=Une chaîne de hachage qui peut être utilisée pour vérifier si le fichier a été téléchargé correctement\ndownload_item_settings_user_agent=Agent utilisateur\ndownload_item_settings_user_agent_description=Agent utilisateur personnalisé pour cet élément (laisser vide pour utiliser la valeur par défaut)\nfile_checksum=Somme de contrôle du fichier\nfile_checksum_page=Vérificateur de somme de contrôle du fichier\nfile_checksum_page_file_checksum_default_algorithm=Algorithme par défaut\nfile_checksum_page_file_checksum_default_algorithm_help=L'algorithme utilisé par défaut pour calculer les sommes de contrôle des fichiers lorsqu'elles ne sont pas fournies.\nstart=Démarrer\ncalculated_checksum=Somme de contrôle calculée\nsaved_checksum=Somme de contrôle enregistrée\nchecksum_algorithm=Algorithme\nfile_not_found=Fichier introuvable\ndownload_not_finished=Téléchargement incomplet\ndone=Terminé\nwaiting=En attente\nmatches=Correspond\nnot_matches=Ne correspond pas\ncopy_to_clipboard=Copier dans le presse-papiers\nusername=Nom d’utilisateur\npassword=Mot de passe\naverage_speed=Vitesse moyenne\nexact_speed=Vitesse exacte\nunlimited=Illimité\nuse_global_settings=Utiliser les paramètres globaux\ncant_run_browser_integration=Impossible d'exécuter l'intégration du navigateur\ncant_open_file=Impossible d'ouvrir le fichier\ncant_open_folder=Impossible d'ouvrir le dossier\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} ans\nrelative_time_long_months={{months}} mois\nrelative_time_long_days={{days}} jours\nrelative_time_long_hours={{hours}} heures\nrelative_time_long_minutes={{minutes}} minutes\nrelative_time_long_seconds={{seconds}} secondes\nrelative_time_short_years={{years}} a\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} j\nrelative_time_short_hours={{hours}} h\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} s\nrelative_time_left={{time}} restant\nrelative_time_ago=Il y a {{time}}\nauto=Auto\nunspecified=Non spécifié\ncustom=Personnalisé\nicon=Icône\nauthor=Auteur\nlink=Lien\nsize=Taille\nstatus=État\nparts_info_downloaded_size=Téléchargé\nparts_info_total_size=Total\nspeed=Vitesse\ntime_left=Temps restant\ndate_added=Date d'ajout\ninfo=Informations\ndownload_page_downloaded_size=Téléchargé\ndownload_page_download_completed=Téléchargement terminé\nresume_support=Capacité de reprise\nyes=Oui\nno=Non\nparts_info=Plus de détails\ndisconnected=Déconnecté\nreceiving_data=Réception de données\nconnecting=Connexion en cours\nwarning=Avertissement\nunsupported_resume_warning=Ce téléchargement ne prend pas en charge la reprise \\! Vous devrez peut-être le redémarrer plus tard dans la liste des téléchargements\nstop_anyway=Arrêter quand même\ncustomize_columns=Personnaliser les colonnes\nreset=Réinitialiser\nmonday=Lundi\ntuesday=Mardi\nwednesday=Mercredi\nthursday=Jeudi\nfriday=Vendredi\nsaturday=Samedi\nsunday=Dimanche\nproxy_open_system_proxy_settings=Ouvrir les paramètres du proxy système\nproxy_type=Type de proxy\nproxy_do_not_use_proxy_for=Ne pas utiliser le proxy pour\nproxy_do_not_use_proxy_for_description=Une liste d'URL qui ne peuvent pas être mandatées\\nVous pouvez utiliser des caractères génériques avec *\\nPar exemple \\: 192.168.1.* exemple.com (séparés par des espaces)\nproxy_change_title=Changer de proxy\nchange_proxy=Changer de proxy\nproxy_no=Pas de proxy\nproxy_system=Proxy du système\nproxy_manual=Proxy manuel\nproxy_pac=Configuration automatique du proxy\nproxy_pac_url=URL de configuration automatique du proxy\naddress=Adresse\nport=Port\naddress_and_port=Adresse et port\nuse_authentication=Utiliser l'authentification\nwarning_you_may_have_to_restart_the_download_later=Vous devrez peut-être redémarrer le téléchargement plus tard \\!\nedit_download_title=Modifier le téléchargement\nedit_download_update_from_download_page=Mise à jour depuis la page de téléchargement\nedit_download_update_from_download_page_description=Lorsque cette fenêtre est ouverte, vous pouvez accéder à la page de téléchargement et cliquer sur le bouton de téléchargement. L'application capturera et mettra à jour les nouvelles références de téléchargement afin que vous puissiez les enregistrer.\nedit_download_saved_download_item_size_not_match=L''élément téléchargé a une taille de {{currentSize}}, qui ne correspond pas à la nouvelle taille de {{newSize}}.\ntranslators_page_thanks=Avec gratitude à ceux qui ont aidé à traduire ce projet ❤️\ntranslators=Traducteurs\nlanguage=Langue\ntranslators_contribute_title=Améliorer les traductions\ntranslators_contribute_description=Vous souhaitez contribuer à l'amélioration de ce projet ? Si votre langue n'est pas listée ou a besoin de quelques ajustements, vous pouvez contribuer avec vos traductions \\!\ncontribute=Contribuer\nmeet_the_translators=Rencontrez les traducteurs\nlocalized_by_translators=Localisé par des traducteurs\nconfirm_exit=Confirmer la fermeture\nconfirm_exit_description=Êtes-vous sûr de vouloir quitter AB Download Manager ?\\nLes téléchargements/files d'attente actifs seront arrêtés \\!\nupdate=Mettre à jour\nupdate_updater=Mises à jour\nupdate_available=Mise à jour disponible\nupdate_error=Erreur lors de la mise à jour\nupdate_available_suggest_to_to_update=Vous pouvez mettre à jour vers la dernière version pour profiter de nouvelles fonctionnalités, améliorations et gains de performances.\nupdate_release_notes=Notes de version\nupdate_check_for_update=Vérifier les mises à jour\nupdate_checking_for_update=Vérification de la mise à jour\nupdate_no_update=Vous utilisez la dernière version\nupdate_check_error=Erreur lors de la vérification de la mise à jour\nupdate_app_updated_to_version_n=Application mise à jour vers la version {{version}}\ncreate_desktop_entry=Créer un raccourci bureau\nshutdown_alert=Avertissement d'arrêt\nsystem_shutdown_soon=Le système va bientôt s'éteindre \\!\nsystem_shutdown_failed=L'arrêt du système a échoué \\!\nsystem_shutdown_soon_description=Le système va bientôt s'éteindre. Si vous utilisez toujours l'ordinateur, veuillez enregistrer votre travail ou annuler l'arrêt.\nsystem_shutdown_reason_queue_completed=Tous les téléchargements dans la file d'attente sont terminés.\nsystem_shutdown_reason_queue_end_time_reached=L'heure de fin prévue pour la file d'attente de téléchargement atteint.\nsystem_shutdown_download_finished=Téléchargement terminé.\nshutdown_now=Éteindre maintenant\nsettings_per_host_settings_new_host=<Nouvel Hôte>\nsettings_per_host_settings_not_selected=Créez ou sélectionnez d'abord un nouvel élément \\!\nsettings_per_host_settings_host=Hôte\nsettings_per_host_settings_host_description=Ces paramètres seront appliqués aux téléchargements correspondant à ce nom d'hôte. Les astérisques (*) sont pris en charge (par ex \\: example.com, *.example.com - n'utilisez qu'un seul).\nsettings_browser_in_launcher=Icône du navigateur dans le lanceur\nsettings_browser_in_launcher_description=Afficher/Masquer l'icône du navigateur dans le lanceur (liste d'applications).\nsort_by=Trier par\nwelcome=Bienvenue\nnew_folder=Nouveau dossier\nskip=Ignorer\nlets_go=C'est parti\nnext=Suivant\nselect_all=Sélectionner tout\nselect_inside=Sélectionner l'intérieur\nselect_invert=Sélectionner l'inverse\nopen_settings=Ouvrir les paramètres\nback=Précédent\nservice_is_running=Le service est en cours d'exécution\ninitial_setup_description=Mettons les choses en place\ninitial_setup_notice=Vous pouvez modifier ces paramètres à tout moment plus tard\npermission_granted=Autorisation accordée\npermission_not_granted=Autorisation non accordée\npermissions=Autorisations\ngive_permission=Accorder les autorisations\ngive_storage_permission=Accorder l'accès au stockage\nstorage_roots=Racines de stockage\npermissions_initial_title=Mettons les choses en place\npermissions_initial_description=Pour fonctionner correctement, l'application a besoin de quelques autorisations. Sur l’écran suivant, vous verrez à quoi sert chaque permission et vous pouvez décider laquelle accorder ou ignorer.\npermissions_done_title=Tout est prêt\npermissions_done_description=Tout est prêt. Toutes les autorisations requises ont été accordées et l'application est prête à fonctionner.\npermissions_manage_storage_title=Gérer l'accès au stockage\npermissions_manage_storage_reason=Cette autorisation permet à l'application de modifier le dossier de téléchargement, de détecter plus précisément les téléchargements en double et d'activer certaines fonctionnalités supplémentaires. C’est facultatif, mais recommandé pour une meilleure expérience.\npermission_read_write_external_storage_title=Stockage en lecture et écriture\npermission_read_write_external_storage_reason=Cette autorisation permet à l'application d'enregistrer et de gérer les fichiers téléchargés, de modifier l'emplacement de téléchargement et d'améliorer la détection des téléchargements en double.\npermissions_post_notification_title=Accès aux notifications\npermissions_post_notification_reason=L'application doit s'exécuter en arrière-plan pour gérer les téléchargements. Les notifications sont utilisées pour vous tenir informé et permettre des opérations en arrière-plan.\npermissions_ignore_battery_optimization_title=Ignorer l'optimisation de la batterie\npermissions_ignore_battery_optimization_reason=Certains appareils limitent agressivement l'activité en arrière-plan pour économiser de la batterie, ce qui peut mettre en pause ou arrêter les téléchargements lorsque l'application n'est pas ouverte. Vous pouvez éventuellement exclure l'application de l'optimisation de la batterie pour vous assurer que les téléchargements se poursuivent sans interruption\nopen_in_browser=Ouvrir dans le navigateur\nbrowser=Navigateur\nbrowser_new_tab=Nouvel onglet\nbrowser_close_tab=Fermer l'onglet\nbrowser_open_in_new_tab=Ouvrir dans un nouvel onglet\nbrowser_open_in_new_background_tab=Ouvrir dans un nouvel onglet en arrière-plan\nbrowser_no_tab_open=Aucun onglet n'est ouvert\nbrowser_tabs=Onglets\nbrowser_paste_and_go=Coller et accéder\nbrowser_bookmarks=Signets\nbrowser_add_bookmark=Ajouter un signet\nbrowser_edit_bookmark=Modifier le signet\nbrowser_add_to_bookmarks=Ajouter aux signets\nbrowser_remove_from_bookmarks=Retirer des signets\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/hu_HU.properties",
    "content": "app_title=AB Letöltéskezelő\nconfirm_auto_categorize_downloads_title=Automatikusan kategorizálja a letöltéseket\nconfirm_auto_categorize_downloads_description=Bármely nem kategorizálatlan elem automatikusan hozzáadódik a kapcsolódó kategóriához.\nconfirm_reset_to_default_categories_title=Alapértelmezett kategóriákra állítsa vissza\nconfirm_reset_to_default_categories_description=Ez eltávolítja az összes kategóriát, és alapértelmezett kategóriákat hoz vissza\\!\nconfirm_delete_download_items_title=Erősítse meg a törlést\nconfirm_delete_download_items_description=Biztos benne, hogy törölni akarja a {{{count}} elemeket?\nconfirm_delete_download_unfinished_items_description=Biztos benne, hogy törölni akarja a {{{count}} befejezetlen letöltéseket?\nconfirm_delete_download_finished_and_unfinished_items_description=Biztos benne, hogy törölni szeretné a {{finishedCount}} kész és a {{unfinishedCount}} befejezetlen letöltéseket?\nalso_delete_file_from_disk=Törölje a fájlt a lemezről is\nconfirm_delete_category_item_title=A {{name}} kategória eltávolítása\nconfirm_delete_category_item_description=Biztos benne, hogy törölni szeretné a \"{{value}}\" kategóriát?\nyour_download_will_not_be_deleted=A letöltések nem kerülnek törlésre\ndrag_the_file_to_another_app=Húzza a fájlt egy másik alkalmazásba\ndrop_link_or_file_here=Dobjon egy linket vagy egy fájlt ide.\nnothing_will_be_imported=Semmi sem lesz importálva\nn_links_will_be_imported={{count}} linkek importálásra kerülnek\nn_items_selected={{count}} kiválasztott elemek\nwindow_close=Bezárás\nwindow_minimize=Tálcára\nwindow_maximize=Teljes méret\nwindow_restore=Visszaállítás\ndelete=Törlés\nremove=Eltávolítás\ncancel=Mégse\nclose=Bezárás\nmenu=Menü\nmore_options=További lehetőségek\nok=Ok\nadd=Hozzáadás\npaste=Beillesztés\nchange=Csere\nedit=Szerkesztés\nchange_anyway=Mindenképpen változtasson\ndownload=Letöltés\nrefresh=Frissítés\nsettings=Beállítások\non_completion=Befejezéskor\nunknown=Ismeretlen\nunknown_error=Ismeretlen hiba\ndownload_item_not_found=Letölthető elem nem található\nname=Név\ndownload_link=Letöltési link\nnot_finished=Befejezetlen\nall=Mind\nfinished=Befejezett\nUnfinished=Befejezetlen\ncanceled=Törölt\nerror=Hiba\npaused=Szüneteltetve\ndownloading=Letöltés\nadded=Hozzáadva\nidle=TÉTLEN\npreparing_file=Fájl előkészítése\ncreating_file=Fájl létrehozása\nresuming=Folytatás\nretrying=Újrapróbálkozás\nlist_is_empty=A lista üres\\!\nsearch_in_the_list=Keresés a listában\nsearch=Keresés\nclear=Tisztítás\ngeneral=Általános\nenabled=Engedélyezve\ndisabled=Letiltva\ndefault=Alapértelmezett\nfile=Fájl\ntasks=Feladatok\ntools=Eszközök\nhelp=Súgó\nsystem=Rendszer\nall_missing_files=Minden hiányzó fájl\nall_finished=Minden befejezett\nall_unfinished=Minden befejezetlen\nentire_list=Teljes lista\ndownload_browser_integration=Töltse le a böngészőbővítményt\nexit=Kilépés\nshow_downloads=Letöltések megjelenítése\nnew_download=Új letöltés\nstop_all=Összes leállítása\nimport_from_clipboard=Importálás vágólapról\nbatch_download=Kötegelt letöltés\nopen=Megnyitás\nshare=Megosztás\nopen_file=Fájl megnyitása\nopen_folder=Mappa megnyitása\nresume=Folytatás\npause=Szünet\nrestart_download=Letöltés újraindítása\ncopy=Másolás\ncopy_link=Link másolása\ncopy_as_curl=Másolás cURL-ként\nshow_properties=Tulajdonságok megjelenítése\nmove_to_queue=Mozgatás a várólistára\nmove_to_this_queue=Áthelyezés ebbe a várólistába\nmove_to_category=Mozgatás a kategóriába\nmove_to_this_category=Mozgatás ebbe a kategóriába\ncategories=Kategóriák\nadd_category=Kategória hozzáadása\nedit_category=Kategória szerkesztése\ndelete_category=Kategória törlése\ncategory_name=Kategória neve\ncategory_download_location=Kategória letöltési helye\ncategory_download_location_description=Ha ezt a kategóriát választja a „Letöltés hozzáadása” menüpontban, használja ezt a könyvtárat „Letöltési helyként”\ncategory_file_types=Kategória fájltípusok\ncategory_file_types_description=Ezeket a fájltípusokat automatikusan ebbe a kategóriába helyezi. (új letöltés hozzáadásakor)\\\\nA fájlkiterjesztések különválasztása szóközzel (ext1 ext2 ...)\ncategory_url_patterns=URL -minták\ncategory_url_patterns_description=Automatikusan tegye a letöltést ezekről az URL-címekről ebbe a kategóriába. (amikor új letöltést adsz hozzá)\\\\nTávolítsd el az URL-eket szóközzel, használhatsz * karaktert is, mint dzsóker\nauto_categorize_downloads=Letöltések automatikus kategorizálása\nrestore_defaults=Alapértelmezések visszaállítása\nabout=Névjegy\nversion_n=Verzió {{value}}\ndeveloped_with_love_for_you=Fejlesztve ❤️ az Ön számára\ndonate=Támogatás\nvisit_the_project_website=Látogasson el a projekt weboldalára\nthis_is_a_free_and_open_source_software=Ez egy ingyenes és nyílt forráskódú szoftver\nview_the_source_code=Lásd a forráskódot\nthird_party_libraries=Harmadik fél könyvtárai\npowered_by_open_source_software=Open Source Software által üzemeltetett\nview_the_open_source_licenses=Tekintse meg a Open Source Software licenszeket\nsupport_and_community=Támogatás és közösség\ntelegram=Telegram\nchannel=Csatorna\ngroup=Csoport\nadd_download=Letöltés hozzáadása\nadd_multi_download_page_header=Válassza ki az elemeket, amelyeket letölteni szeretne\nsave_to=Mentés ide\nwhere_should_each_item_saved=Hová kell menteni az egyes elemeket?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Több elem van\\! kérjük, válassza ki, hogyan szeretné elmenteni őket\neach_item_on_its_own_category=Minden elem saját kategóriában\neach_item_on_its_own_category_description=Minden elem egy olyan kategóriába kerül, amely az adott fájltípussal rendelkezik\nall_items_in_one_category=Minden elem egy kategóriában\nall_items_in_one_category_description=Minden fájl a kiválasztott kategóriába kerül mentésre\nall_items_in_one_Location=Minden elem egy helyen\nall_items_in_one_Location_description=Az összes elemet a kiválasztott könyvtárba menti\nunselected_all_items_in_specific_location_description=Minden fájl a kiválasztott kategória helyére kerül mentésre\nno_category_selected=Nincs kiválasztott kategória\nno_categories_found=Nem találhatók kategóriák\ndownload_location=Letöltési hely\nlocation=Hely\nselect_queue=Várólista kiválasztása\nwithout_queue=Várólista nélkül\nuse_category=Kategória használata\ncant_write_to_this_folder=Nem lehet írni erre a mappába\nfile_name_already_exists=A fájlnév már létezik\ndownload_already_exists=A letöltés már létezik\ninvalid_file_name=Érvénytelen fájlnév\nshow_solutions=Megoldások megjelenítése...\nchange_solution=Megoldás módosítása\nselect_a_solution=Válasszon ki egy megoldást\nselect_download_strategy_description=A megadott link már a letöltési listákban található, kérjük, adja meg, mit szeretne csinálni\ndownload_strategy_add_a_numbered_file=Adjon hozzá egy számozott fájlt\ndownload_strategy_add_a_numbered_file_description=Adjon hozzá egy indexet a letöltési fájl nevének vége után\ndownload_strategy_override_existing_file=Meglévő fájl felülírása\ndownload_strategy_override_existing_file_description=Távolítsa el a meglévő letöltést, és írjon ehhez a fájlhoz\ndownload_strategy_update_download_link=A meglévő letöltés frissítése\ndownload_strategy_update_download_link_description=Frissítse a meglévő letöltési linket és annak hitelesítő adatait\ndownload_strategy_show_downloaded_file=A letöltött fájl megjelenítése\ndownload_strategy_show_downloaded_file_description=A már meglévő letöltési elem megjelenítése, majd megnyomhatja a folytatás vagy a megnyitás gombot\nbatch_download_link_help=Írjon be egy linket, amely helyettesítő karaktereket tartalmaz (használja a *-t)\ninvalid_url=Érvénytelen URL\nlist_is_too_large_maximum_n_items_allowed=A lista túl nagy\\! maximálisan megengedett {{count}} tételek száma}\nenter_range=Tartomány megadása\nrange_from=ettől\nrange_to=eddig\nbatch_download_wildcard_length=Helyettesítő karakterek hossza\nfirst_link=Első link\nlast_link=Utolsó link\nopen_source_software_used_in_this_app=Ebben az alkalmazásban használt nyílt forráskódú szoftver\nlinks=Linkek\nwebsite=Webhely\ndevelopers=Fejlesztők\nsource_code=Forráskód\nlicense=Licensz\nno_license_found=Nem található licensz\norganization=Szervezet\nadd_new_queue=Új várólista hozzáadása\nqueue_name=Várólista neve\nqueues=Várólisták\nstop_queue=Várólista leállítása\nstart_queue=Várólista indítása\nclear_queue_items=Üres várólista\nconfig=Konfiguráció\nitems=Elemek\nmove_down=Lejjebb\nmove_up=Feljebb\nremove_queue=Várólista eltávolítása\nqueue_name_help=Adja meg a várólista nevét\nqueue_name_describe=A várólista neve {{value}}\nqueue_max_concurrent_download=Egyidejű letöltések maximális száma\nqueue_max_concurrent_download_description=Maximális letöltés ehhez a várólistához\nqueue_automatic_stop=Automatikus leállítás\nqueue_automatic_stop_description=Automatikusan leállítja a várólistát, ha nincs benne elem\nqueue_scheduler=Ütemező\nqueue_enable_scheduler=Ütemező engedélyezése\nqueue_active_days=Aktív napok\nqueue_active_days_description=Mely napokon működjenek az ütemezők?\nqueue_scheduler_enable_auto_start_time=Automatikus indítási idő engedélyezése\nqueue_scheduler_auto_start_time=Automatikus indítási idő\nqueue_scheduler_enable_auto_stop_time=Automatikus leállítási idő engedélyezése\nqueue_scheduler_auto_stop_time=Automatikus leállítási idő\nqueue_shutdown_on_completion=A rendszer leállítása a rendszer befejezésekor\nqueue_shutdown_on_completion_description=Automatikusan leállítja a rendszert, amikor ez a várólista befejeződik, vagy amikor a tervezett befejezési idő elérkezik.\nappearance=Megjelenés\ndownload_engine=Letöltő motor\nbrowser_integration=Böngésző integráció\nsettings_download_max_retries_count=Maximális letöltési újrapróbálkozások száma\nsettings_download_max_retries_count_description=Az alkalmazás legfeljebb annyi alkalommal próbálkozik újra egy sikertelen letöltéssel, mielőtt feladja\nsettings_download_max_retries_count_describe_no_retries=A sikertelen letöltéseket nem próbálja meg újra\nsettings_download_max_retries_count_describe_n_retries=A sikertelen letöltés(eke)t újra megpróbáljuk {{count}} alkalommal\nsettings_download_thread_count=Szálszám\nsettings_download_thread_count_description=Letöltési szál maximális száma letöltési elemenként\nsettings_download_thread_count_describe=Egy letöltésnek legfeljebb {{count}} szála lehet\nsettings_download_thread_count_with_large_value_describe=Figyelmeztetés\\: A magas szálszám beállítása növelheti a rendszer erőforrás-felhasználását, csökkentheti a teljesítményt, vagy kapcsolódási problémákat okozhat a kiszolgálókkal. Csak akkor használjon magasabb értékeket, ha tisztában van a rendszerre és a hálózatra gyakorolt lehetséges hatással.\nsettings_use_server_last_modified_time=A kiszolgáló utolsó módosítási idejének használata\nsettings_use_server_last_modified_time_description=Fájl letöltésekor használja a kiszolgáló utolsó módosítási idejét a helyi fájlhoz\nsettings_append_extension_to_incomplete_downloads=Bővítmény hozzáadása a befejezetlen letöltésekhez\nsettings_append_extension_to_incomplete_downloads_description=A \".part\" kiterjesztés hozzáadása a nem teljes letöltésekhez. Ez segít azonosítani a befejezetlen letöltéseket, és megakadályozza a befejezetlen fájlok véletlen megnyitását.\nsettings_use_sparse_file_allocation=Apró fájlok helyfoglalása\nsettings_use_sparse_file_allocation_description=Hatékonyabban hozhat létre fájlokat, különösen SSD lemezeken, a felesleges adatírás csökkentésével. Ez felgyorsíthatja a letöltések indítását és csökkentheti a lemezhasználatot. Ha a letöltések lassan indulnak, vagy szokatlan letöltési sebességet tapasztal, fontolja meg ennek az opciónak a letiltását, mivel előfordulhat, hogy egyes eszközök nem támogatják teljes mértékben.\nsettings_ignore_ssl_certificates=Figyelmen kívül hagyja az SSL tanúsítványokat\nsettings_ignore_ssl_certificates_description=Letiltja az SSL tanúsítvány ellenőrzését. Csak szükség esetén használja, mert ez biztonsági kockázatoknak teheti ki a kapcsolatot.\nsettings_global_speed_limiter=Globális sebességkorlátozó\nsettings_global_speed_limiter_description=Globális letöltési sebességkorlátozás (0 azt jelenti, hogy korlátlan)\nsettings_show_average_speed=Átlagos sebesség megjelenítése\nsettings_show_average_speed_description=Letöltési sebesség átlagban vagy pontosságban\nsettings_use_category_by_default=Kategória használata alapértelmezés szerint\nsettings_use_category_by_default_description=Alapértelmezés szerint használja a kategóriát a letöltés hozzáadásakor.\nsettings_default_download_folder=Alapértelmezett letöltési mappa\nsettings_default_download_folder_description=Új letöltés hozzáadásakor a rendszer alapértelmezés szerint ezt a helyet használja\nsettings_default_download_folder_describe=A(z) \"{{folder}}\" lesz használva\\nsettings_use_proxy\\=Proxy használata\nsettings_use_proxy=Proxy használata\nsettings_use_proxy_description=Proxy használata fájlok letöltéséhez\nsettings_use_proxy_describe_no_proxy=Proxy nem lesz használva\nsettings_use_proxy_describe_system_proxy=A rendszer proxyját fogjuk használni\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" lesz használva\nsettings_use_proxy_describe_pac_proxy=\\ \"{{value}}\" PAC fájlt használja a rendszer\nsettings_track_deleted_files_on_disk=Kövesse nyomon a törölt fájlokat a lemezen\nsettings_track_deleted_files_on_disk_description=Fájlok automatikus eltávolítása a listáról, amikor törlik vagy áthelyezik őket a letöltési könyvtárból.\nsettings_delete_partial_file_on_download_cancellation=Részleges fájl törlése a letöltés törlésekor\nsettings_delete_partial_file_on_download_cancellation_description=A letöltés törlésekor a részben letöltött fájl törlődik a lemezről. Ez segít tisztán tartani a letöltési mappát, és csökkenti a felesleges lemezterület-használatot. A letöltés azonban a következő indításkor újraindul az elejéről.\nsettings_default_user_agent=Alapértelmezett felhasználói ügynök\nsettings_default_user_agent_description=Adja meg az Alapértelmezett felhasználói ügynök karakterláncot, hogy meghatározza, hogyan azonosítják a kérelmeket a kiszolgálók felé. Ez segíthet az egyes eszközökre optimalizált tartalmak elérésében, vagy az egyes webhelyek által előírt letöltési korlátozások megkerülésében.\nsettings_download_size_unit=Letöltési méret egység\nsettings_download_size_unit_description=A letöltési méret megjelenítésére használt egység\nsettings_download_speed_unit=Letöltési sebesség egység\nsettings_download_speed_unit_description=A letöltési sebesség megjelenítéséhez használt egység\nsettings_theme=Téma\nsettings_theme_description=Válasszon egy témát az alkalmazáshoz\nsettings_default_dark_theme=Alapértelmezett sötét téma\nsettings_default_dark_theme_description=Akkor alkalmazandó, ha az alkalmazás a rendszer témáját követi, és a sötét üzemmód aktív\nsettings_default_light_theme=Alapértelmezett világos téma\nsettings_default_light_theme_description=Akkor alkalmazandó, ha az alkalmazás a rendszer témáját követi, és a világos üzemmód aktív\nsettings_font=Betűtípus\nsettings_font_description=Az alkalmazás felületén használt betűtípus megváltoztatása, Egyes betűtípusok esetleg nem jelennek meg helyesen az alkalmazásban.\nsettings_ui_scale=UI -skála\nsettings_ui_scale_description=Az alkalmazás kezelőfelületi elemek méretének beállítása\nsettings_language=Nyelv\nsettings_compact_top_bar=Kompakt felső sáv\nsettings_compact_top_bar_description=Egyesítse a felső sávot a címsorral, ha a főablak elég széles\nsettings_use_native_menu_bar=Natív menüsor használata\nsettings_use_native_menu_bar_description=A rendszer alapértelmezett menüsor stílusának használata\nsettings_use_relative_date_time=Relatív dátum/idő használata\nsettings_use_relative_date_time_description=Relatív dátum/idő formátum használata az alkalmazásban a dátumokhoz (pl.\\: \"2 nappal ezelőtt\" a pontos dátum/idő helyett)\nsettings_show_icon_labels=Az ikoncímkék megjelenítése\nsettings_show_icon_labels_description=A címkék megjelenítése az ikonok alatt, ha lehetséges (mint például a kezdőlap eszköztár műveletei)\nsettings_use_system_tray=Használja a rendszer tálcát\nsettings_use_system_tray_description=A rendszer tálca ikon megjelenítése, amikor az alkalmazás fut\nsettings_start_on_boot=Indítás a rendszerrel\nsettings_start_on_boot_description=Alkalmazás automatikus indítása felhasználói bejelentkezéskor\nsettings_notification_sound=Értesítési hang\nsettings_notification_sound_description=Hang lejátszása új értesítéskor\nsettings_browser_integration=Böngésző integráció\nsettings_browser_integration_description=Letöltések elfogadása böngészőkből\nsettings_browser_integration_server_port=Szerverport\nsettings_browser_integration_server_port_description=Port a böngésző integrációjához\nsettings_browser_integration_server_port_describe=Az alkalmazás figyelni fogja a(z) {{port}} portot\nsettings_dynamic_part_creation=Dinamikus darabok létrehozása\nsettings_dynamic_part_creation_description=Amikor egy darab elkészült, hozzon létre egy másik darabot a többi rész felosztásával a letöltési sebesség javítása érdekében\nsettings_show_completion_dialog=Letöltés befejezése párbeszédpanel megjelenítése\nsettings_show_completion_dialog_description=A \"Letöltés befejeződött\" párbeszédpanel automatikus megjelenítése a letöltés befejezésekor.\nsettings_show_download_progress_dialog=Letöltési folyamat párbeszédpanel megjelenítése\nsettings_show_download_progress_dialog_description=A \"Letöltés folyamata\" párbeszédpanel automatikus megjelenítése a letöltés megkezdésekor.\nsettings_per_host_settings=Gazdagépenkénti beállítások\nsettings_per_host_settings_descriptions=Ezek a beállítások automatikusan alkalmazásra kerülnek minden olyan új letöltésre, amely megfelel a megadott Gazdagépnek.\nsettings_download_max_concurrent_downloads=Maximális egyidejű letöltések száma\nsettings_download_max_concurrent_downloads_description=Az egyszerre letölthető fájlok maximális száma (a várólisták által kezelt letöltések nem számítanak bele; korlátlan letöltéshez állítsa 0-ra)\ndownload_item_settings_speed_limit=Sebességhatár\ndownload_item_settings_speed_limit_description=Korlátozza a letöltési sebességet ehhez az elemhez\ndownload_item_settings_show_download_completion_dialog=Letöltés befejezése párbeszédpanel megjelenítése\ndownload_item_settings_show_download_completion_dialog_description=A \"Letöltés befejeződött\" párbeszédpanel automatikus megjelenítése a letöltés befejezése után.\ndownload_item_settings_shutdown_on_completion=A rendszer leállítása befejezéskor\ndownload_item_settings_shutdown_on_completion_description=Automatikusan leállítja a rendszert, amikor a letöltés befejeződik.\ndownload_item_settings_thread_count=Szálszám\ndownload_item_settings_thread_count_description=Mennyi szálat használtak a letöltési elem letöltéséhez (0 alapértelmezett)\ndownload_item_settings_thread_count_describe={{count}} szál ehhez a letöltéshez\ndownload_item_settings_username_description=Adjon meg egy felhasználónevet, ha a hivatkozás védett erőforrás\ndownload_item_settings_password_description=Adjon meg egy felhasználónevet, ha a hivatkozás védett erőforrás\ndownload_item_settings_download_page=Letöltési oldal\ndownload_item_settings_download_page_description=Az a weboldal, ahol ezt a letöltést kezdeményezték\ndownload_item_settings_file_checksum=Fájl ellenőrző összeg\ndownload_item_settings_file_checksum_description=Egy hash karakterlánc, amelynek segítségével ellenőrizhető, hogy a fájl megfelelően lett-e letöltve.\ndownload_item_settings_user_agent=Felhasználói ügynök\ndownload_item_settings_user_agent_description=Egyéni felhasználói ügynök ehhez az elemhez (hagyja üresen az alapértelmezett használatához)\nfile_checksum=Fájl ellenőrző összeg\nfile_checksum_page=Fájl ellenőrzőösszeg-ellenőrző\nfile_checksum_page_file_checksum_default_algorithm=Alapértelmezett algoritmus\nfile_checksum_page_file_checksum_default_algorithm_help=A fájlellenőrző összegek kiszámítására használt alapértelmezett algoritmus, ha azok nincsenek megadva.\nstart=Indítás\ncalculated_checksum=Számított ellenőrzőösszeg\nsaved_checksum=Mentett ellenőrzőösszeg\nchecksum_algorithm=Algoritmus\nfile_not_found=A fájl nem található\ndownload_not_finished=A letöltés nem fejeződött be\ndone=Kész\nwaiting=Várakozás\nmatches=Egyezések\nnot_matches=Nincsenek egyezések\ncopy_to_clipboard=Másolás vágólapra\nusername=Felhasználónév\npassword=Jelszó\naverage_speed=Átlagsebesség\nexact_speed=Pontos sebesség\nunlimited=Korlátlan\nuse_global_settings=Használja a globális beállításokat\ncant_run_browser_integration=Nem lehet futtatni a böngésző integrációját\ncant_open_file=Nem lehet megnyitni a fájlt\ncant_open_folder=Nem lehet megnyitni a mappát\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} év\nrelative_time_long_months={{months}} hónap\nrelative_time_long_days={{days}} nap\nrelative_time_long_hours={{hours}} óra\nrelative_time_long_minutes={{minutes}} perc\nrelative_time_long_seconds={{seconds}} másodperc\nrelative_time_short_years={{years}} é\nrelative_time_short_months={{months}} H\nrelative_time_short_days={{days}} n\nrelative_time_short_hours={{hours}} óra\nrelative_time_short_minutes={{minutes}} perc\nrelative_time_short_seconds={{seconds}} másodperc\nrelative_time_left={{time}} maradt\nrelative_time_ago={{time}} eltelt\nauto=Automatikus\nunspecified=Meghatározatlan\ncustom=Egyéni\nicon=Ikon\nauthor=Szerző\nlink=Hivatkozás\nsize=Méret\nstatus=Állapot\nparts_info_downloaded_size=Letöltött\nparts_info_total_size=Teljes\nspeed=Sebesség\ntime_left=Hátralévő idő\ndate_added=Dátum hozzáadva\ninfo=Információ\ndownload_page_downloaded_size=Letöltött\ndownload_page_download_completed=Letöltés befejeződött\nresume_support=Támogatás folytatása\nyes=Igen\nno=Nem\nparts_info=Részletek…\ndisconnected=Szétkapcsolt\nreceiving_data=Adatok fogadása\nconnecting=Csatlakozás\nwarning=Figyelmeztetés\nunsupported_resume_warning=Ez a letöltés nem támogatja a folytatást\\! Lehet, hogy később újra kell indítania a letöltési listában\nstop_anyway=Mindenképpen álljon meg\ncustomize_columns=Oszlopok testreszabása\nreset=Visszaállítás\nmonday=hétfő\ntuesday=kedd\nwednesday=szerda\nthursday=csütörtök\nfriday=péntek\nsaturday=szombat\nsunday=vasárnap\nproxy_open_system_proxy_settings=Nyissa meg a rendszer proxy beállításait\nproxy_type=Proxy típus\nproxy_do_not_use_proxy_for=Ne használjon proxyt\nproxy_do_not_use_proxy_for_description=Azoknak az url címeknek a listája, amelyeket nem lehet proxyzni\\\\nHasználhat * karaktert,\\\\ például 192.168.1.* example.com (szóközzel elválasztva)\nproxy_change_title=Változtassa meg a proxyt\nchange_proxy=Változtassa meg a proxyt\nproxy_no=Nincs proxy\nproxy_system=Rendszer proxy\nproxy_manual=Kézi proxy\nproxy_pac=Proxy automatikus konfigurálása\nproxy_pac_url=Proxy automatikus konfigurációs URL-címe\naddress=Cím\nport=Port\naddress_and_port=Cím és port\nuse_authentication=Hitelesítés használata\nwarning_you_may_have_to_restart_the_download_later=Lehet, hogy később újra kell indítania a letöltést\\!\nedit_download_title=Letöltés szerkesztése\nedit_download_update_from_download_page=Frissítés a letöltési oldalról\nedit_download_update_from_download_page_description=Ha ez az ablak meg van nyitva, akkor lépjen a Letöltés oldalra, és kattintson a letöltés gombra. Az alkalmazás rögzíti és frissíti az új letöltési hitelesítő adatokat, így elmentheti azokat.\nedit_download_saved_download_item_size_not_match=A mentett letöltési elem mérete {{currentSize}}, amely nem felel meg az új méretnek {{newSize}}.\ntranslators_page_thanks=Hálával azoknak, akik segítették a projekt lefordítását ❤️\ntranslators=Fordítók\nlanguage=Nyelv\ntranslators_contribute_title=Fordítások javítása\ntranslators_contribute_description=Szeretne segíteni a projekt fejlesztésében? Ha a te nyelved nem szerepel a listán, vagy ha szükséged van néhány javításra, akkor járulj hozzá a fordításaiddal, és tedd jobbá\\!\ncontribute=Hozzájárul\nmeet_the_translators=Ismerje meg a fordítókat\nlocalized_by_translators=Fordítók által lokalizálva\nconfirm_exit=Erősítse meg a kilépést\nconfirm_exit_description=Biztos, hogy ki akar lépni az AB Letöltéskezelőből?\\\\nAz aktív letöltések/várólisták leállnak\\!\nupdate=Frissítés\nupdate_updater=Frissítő\nupdate_available=Frissítés érhető el\nupdate_error=Frissítési hiba\nupdate_available_suggest_to_to_update=A legújabb verzióra frissítve élvezheti az új funkciókat, fejlesztéseket és teljesítményjavításokat.\nupdate_release_notes=Kiadási megjegyzések\nupdate_check_for_update=Frissítés keresése\nupdate_checking_for_update=A frissítés ellenőrzése\nupdate_no_update=A legújabb verziót használja\nupdate_check_error=Hiba a frissítés keresése közben\nupdate_app_updated_to_version_n=Az alkalmazás frissítve a(z) {{{version}} verzióra\ncreate_desktop_entry=Asztali bejegyzés létrehozása\nshutdown_alert=Leállítási riasztás\nsystem_shutdown_soon=A rendszer hamarosan leáll\\!\nsystem_shutdown_failed=A rendszer leállítása sikertelen\\!\nsystem_shutdown_soon_description=A rendszer hamarosan leáll. Ha még használja a számítógépet, kérjük, mentse el a munkáját, vagy törölje a leállítást.\nsystem_shutdown_reason_queue_completed=A várólistán lévő összes letöltés elkészült.\nsystem_shutdown_reason_queue_end_time_reached=Elérte a letöltési sor ütemezett befejezési időpontját.\nsystem_shutdown_download_finished=Letöltés befejezve.\nshutdown_now=Leállítás most\nsettings_per_host_settings_new_host=<Új Gazdagép>\nsettings_per_host_settings_not_selected=Először hozzon létre vagy válasszon ki egy új elemet\\!\nsettings_per_host_settings_host=Gazdagép\nsettings_per_host_settings_host_description=Ezek a beállítások az adott gazdagépnévvel rendelkező letöltésekre lesznek alkalmazva. A helyettesítő karakterek (*) támogatottak (így\\: példa.com, *.példa.com – csak egyet használjon).\nsettings_browser_in_launcher=Böngésző ikon az indítóban\nsettings_browser_in_launcher_description=A böngésző ikonjának megjelenítése vagy elrejtése az indítóban (alkalmazások listája).\nsort_by=Rendezés\nwelcome=Üdvözöljük\nnew_folder=Új mappa\nskip=Kihagy\nlets_go=Menjünk\nnext=Következő\nselect_all=Mind kiválaszt\nselect_inside=Belső kijelölés\nselect_invert=Kiválasztás megfordítása\nopen_settings=Beállítások megnyitása\nback=Vissza\nservice_is_running=A szolgáltatás fut\ninitial_setup_description=Állítsuk be a dolgokat\ninitial_setup_notice=Ezeket a beállításokat később bármikor módosíthatja\npermission_granted=Engedély megadva\npermission_not_granted=Engedély megtagadva\npermissions=Engedélyek\ngive_permission=Engedélyezés\ngive_storage_permission=Tárhely-hozzáférés engedélyezése\nstorage_roots=Fő tárolási helyek\npermissions_initial_title=Engedélyek beállítása\npermissions_initial_description=A megfelelő működéshez az alkalmazásnak néhány engedélyre van szüksége. A következő képernyőn látni fogja, hogy az egyes engedélyeket mire használják, és eldöntheti, hogy melyiket engedélyezi vagy hagyja ki.\npermissions_done_title=Minden készen áll\npermissions_done_description=Minden készen áll. Minden szükséges engedélyt megadtunk, és az alkalmazás készen áll.\npermissions_manage_storage_title=Tárhely-hozzáférés kezelése\npermissions_manage_storage_reason=Ez az engedély lehetővé teszi az alkalmazás számára, hogy módosítsa a letöltési mappát, pontosabban észlelje a duplikált letöltéseket, és engedélyezzen néhány extra funkciót. Nem kötelező, de a legjobb élmény érdekében ajánlott.\npermission_read_write_external_storage_title=Tárhely olvasása és írása\npermission_read_write_external_storage_reason=Ez az engedély lehetővé teszi az alkalmazás számára, hogy mentse és kezelje a letöltött fájlokat, módosítsa a letöltési helyet, és javítsa az ismétlődő letöltések észlelését.\npermissions_post_notification_title=Értesítés küldése\npermissions_post_notification_reason=Az alkalmazásnak a háttérben kell futnia a letöltések kezeléséhez. Az értesítések tájékoztatást adnak, és lehetővé teszik a háttérben történő működést.\npermissions_ignore_battery_optimization_title=Az akkumulátoroptimalizálás figyelmen kívül hagyása\npermissions_ignore_battery_optimization_reason=Egyes eszközök agresszíven korlátozzák a háttértevékenységet az akkumulátor kímélése érdekében, ami szüneteltetheti vagy leállíthatja a letöltéseket, ha az alkalmazás nincs megnyitva. Opcionálisan kizárhatja az alkalmazást az akkumulátor-optimalizálásból, hogy a letöltések megszakítás nélkül folytatódjanak\nopen_in_browser=Megnyitás böngészőben\nbrowser=Böngésző\nbrowser_new_tab=Új lap\nbrowser_close_tab=Lap bezárása\nbrowser_open_in_new_tab=Megnyitás új lapon\nbrowser_open_in_new_background_tab=Megnyitás új háttérlapon\nbrowser_no_tab_open=Nincs megnyitott lap\nbrowser_tabs=Lapok\nbrowser_paste_and_go=Beillesztés és tovább\nbrowser_bookmarks=Könyvjelzők\nbrowser_add_bookmark=Könyvjelző hozzáadása\nbrowser_edit_bookmark=Könyvjelző szerkesztése\nbrowser_add_to_bookmarks=Hozzáadás a könyvjelzőkhöz\nbrowser_remove_from_bookmarks=Eltávolítás a könyvjelzők közül\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/id_ID.properties",
    "content": "app_title=Manajer Unduhan AB\nconfirm_auto_categorize_downloads_title=Mengelompokkan unduhan secara otomatis\nconfirm_auto_categorize_downloads_description=Setiap item yang belum memiliki kategori akan otomatis dimasukkan ke kategori yang sesuai.\nconfirm_reset_to_default_categories_title=Mengembalikan ke Kategori Bawaan\nconfirm_reset_to_default_categories_description=Ini akan MENGHAPUS semua kategori dan mengembalikan kategori bawaan\\!\nconfirm_delete_download_items_title=Konfirmasi Penghapusan\nconfirm_delete_download_items_description=Apakah kamu yakin untuk menghapus {{count}} item?\nconfirm_delete_download_unfinished_items_description=Apakah Anda yakin ingin menghapus {{count}} unduhan yang belum selesai?\nconfirm_delete_download_finished_and_unfinished_items_description=Apakah Anda yakin ingin menghapus {{finishedCount}} unduhan yang telah selesai dan {{unfinishedCount}} unduhan yang belum selesai?\nalso_delete_file_from_disk=Juga menghapus berkas di dalam penyimpanan\nconfirm_delete_category_item_title=Menghapus kategori {{name}}\nconfirm_delete_category_item_description=Apakah kamu yakin untuk menghapus \"{{value}}\" Kategori?\nyour_download_will_not_be_deleted=Unduhan Anda tidak akan dihapus\ndrag_the_file_to_another_app=Seret berkas ke aplikasi lain\ndrop_link_or_file_here=Letakkan tautan atau berkas di sini.\nnothing_will_be_imported=Tidak ada yang akan diimpor\nn_links_will_be_imported={{count}} tautan akan diimpor\nn_items_selected={{count}} butir dipilih\nwindow_close=Keluar\nwindow_minimize=Meminimalkan\nwindow_maximize=Diperbesar\nwindow_restore=Mengembalikan\ndelete=Hapus\nremove=Buang\ncancel=Batal\nclose=Tutup\nmenu=Menu\nmore_options=Lebih Banyak Opsi\nok=Oke\nadd=Tambah\npaste=Tempel\nchange=Ubah\nedit=Sunting\nchange_anyway=Ganti Saja\ndownload=Unduh\nrefresh=Muat Ulang\nsettings=Setelan\non_completion=Ketika Selesai\nunknown=Tidak diketahui\nunknown_error=Kesalahan yang tidak diketahui\ndownload_item_not_found=Butir unduhan tidak ditemukan\nname=Nama\ndownload_link=Tautan unduhan\nnot_finished=Tidak selesai\nall=Semua\nfinished=Selesai\nUnfinished=Belum Selesai\ncanceled=Dibatalkan\nerror=Kesalahan\npaused=Ditunda\ndownloading=Mengunduh\nadded=Ditambahkan\nidle=Tidak Aktif\npreparing_file=Menyiapkan berkas\ncreating_file=Membuat berkas\nresuming=Melanjutkan\nretrying=Mencoba kembali\nlist_is_empty=Daftar kosong\\!\nsearch_in_the_list=Mencari dalam Daftar\nsearch=Cari\nclear=Kosongkan\ngeneral=Umum\nenabled=Diaktifkan\ndisabled=Nonaktif\ndefault=Bawaan\nfile=Berkas\ntasks=Tugas-tugas\ntools=Peralatan\nhelp=Bantuan\nsystem=Sistem\nall_missing_files=Semua Berkas Hilang\nall_finished=Semuanya selesai\nall_unfinished=Semuanya belum selesai\nentire_list=Seluruh Daftar\ndownload_browser_integration=Unduh Integrasi Browser\nexit=Keluar\nshow_downloads=Tampilkan Unduhan\nnew_download=Unduhan Baru\nstop_all=Stop Semua\nimport_from_clipboard=Impor dari Papan Klip\nbatch_download=Unduh Berkelompok\nopen=Buka\nshare=Bagikan\nopen_file=Buka Berkas\nopen_folder=Buka Folder\nresume=Lanjut\npause=Jeda\nrestart_download=Mulai Ulang Unduhan\ncopy=Salin\ncopy_link=Salin tautan\ncopy_as_curl=Salin sebagai cURL\nshow_properties=Tampilkan Properti\nmove_to_queue=Pindahkan ke Antrean\nmove_to_this_queue=Pindahkan ini ke Antrean\nmove_to_category=Pindahkan ke Kategori\nmove_to_this_category=Pindahkan ke kategori ini\ncategories=Kategori\nadd_category=Tambah Kategori\nedit_category=Sunting Kategori\ndelete_category=Hapus Kategori\ncategory_name=Nama Kategori\ncategory_download_location=Lokasi Pengunduhan Kategori\ncategory_download_location_description=Ketika kategori ini dipilih dalam “Tambahkan Unduhan”, gunakan direktori ini sebagai “Lokasi Unduhan”\ncategory_file_types=Jenis berkas kategori\ncategory_file_types_description=Secara otomatis masukkan jenis file ini ke dalam kategori ini. (saat Anda menambahkan unduhan baru)\\nPisahkan ekstensi file dengan spasi (ext1 ext2 ...)\ncategory_url_patterns=Pola URL\ncategory_url_patterns_description=Secara otomatis masukkan unduhan dari URL ini ke dalam kategori ini. (saat Anda menambahkan unduhan baru)\\nPisahkan URL dengan spasi, Anda juga dapat menggunakan * sebagai wildcard\nauto_categorize_downloads=Kategorikan Otomatis Unduhan\nrestore_defaults=Pulihkan Kondisi Bawaan\nabout=Tentang\nversion_n=Versi {{value}}\ndeveloped_with_love_for_you=Dikembangkan dengan ❤️ untukmu\ndonate=Donasi\nvisit_the_project_website=Kunjungi situs web proyek\nthis_is_a_free_and_open_source_software=Ini adalah perangkat lunak gratis dan bersifat Sumber Terbuka\nview_the_source_code=Lihat Kode Sumber\nthird_party_libraries=Perpustakaan Pihak Ketiga\npowered_by_open_source_software=Didukung oleh Perangkat Lunak Sumber Terbuka\nview_the_open_source_licenses=Lihat lisensi Sumber Terbuka\nsupport_and_community=Dukungan dan Komunitas\ntelegram=Telegram\nchannel=Saluran\ngroup=Grup\nadd_download=Tambahkan Unduh\nadd_multi_download_page_header=Pilih butir yang ingin Anda ambil untuk diunduh\nsave_to=Simpan ke\nwhere_should_each_item_saved=Setiap butir yang ada mau disimpan ke mana?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Ada beberapa butir\\! Pilihlah cara penyimpanan yang kamu mau\neach_item_on_its_own_category=Setiap butir dalam kategorinya masing-masing\neach_item_on_its_own_category_description=Setiap butir akan ditempatkan dalam kategori yang memiliki tipe berkas tersebut\nall_items_in_one_category=Semua butir dalam satu Kategori\nall_items_in_one_category_description=Semua berkas akan disimpan dalam kategori yang dipilih\nall_items_in_one_Location=Semua butir dalam satu Lokasi\nall_items_in_one_Location_description=Semua butir akan disimpan dalam kategori yang dipilih\nunselected_all_items_in_specific_location_description=Semua berkas akan disimpan di lokasi kategori yang dipilih\nno_category_selected=Tidak ada Kategori yang dipilih\nno_categories_found=Tidak ditemukan kategori\ndownload_location=Lokasi Unduhan\nlocation=Lokasi\nselect_queue=Pilih Antrean\nwithout_queue=Tanpa Antrean\nuse_category=Gunakan Kategori\ncant_write_to_this_folder=Tidak dapat menulis ke folder ini\nfile_name_already_exists=Nama yang sama sudah ada\ndownload_already_exists=Unduhan sudah ada\ninvalid_file_name=Nama berkas tidak sah\nshow_solutions=Tampilkan solusi...\nchange_solution=Ganti solusi\nselect_a_solution=Pilih solusi\nselect_download_strategy_description=Tautan yang Anda berikan sudah ada dalam daftar unduhan, silakan tentukan apa yang ingin Anda lakukan\ndownload_strategy_add_a_numbered_file=Menambahkan berkas bernomor\ndownload_strategy_add_a_numbered_file_description=Tambahkan indeks setelah akhir nama berkas unduhan\ndownload_strategy_override_existing_file=Mengganti berkas yang ada\ndownload_strategy_override_existing_file_description=Buang unduhan yang ada dan tulis ke berkas tersebut\ndownload_strategy_update_download_link=Perbarui unduhan yang ada\ndownload_strategy_update_download_link_description=Perbarui tautan unduhan yang ada beserta kredensialnya\ndownload_strategy_show_downloaded_file=Tampilkan file yang telah diunduh\ndownload_strategy_show_downloaded_file_description=Tampilkan butir unduhan yang sudah ada, sehingga Anda dapat menekan lanjutkan atau membukanya\nbatch_download_link_help=Masukkan tautan yang berisi wildcard (gunakan *)\ninvalid_url=URL tidak sah\nlist_is_too_large_maximum_n_items_allowed=Daftar terlalu besar\\! Maksimal {{count}} butir yang diizinkan\nenter_range=Masukkan rentang nilai\nrange_from=Dari\nrange_to=Sampai\nbatch_download_wildcard_length=Panjang wildcard\nfirst_link=Tautan Pertama\nlast_link=Tautan Terakhir\nopen_source_software_used_in_this_app=Perangkat Lunak Sumber Terbuka yang digunakan dalam Aplikasi ini\nlinks=Tautan\nwebsite=Situs Web\ndevelopers=Pengembang\nsource_code=Kode Sumber\nlicense=Lisensi\nno_license_found=Tidak ada lisensi yang ditemukan\norganization=Organisasi\nadd_new_queue=Tambahkan Antrean Baru\nqueue_name=Nama Antrean\nqueues=Antrean\nstop_queue=Henti Antrean\nstart_queue=Mulai Antrean\nclear_queue_items=Antrean Kosong\nconfig=Konfigurasi\nitems=Butir\nmove_down=Pindah ke bawah\nmove_up=Pindah ke atas\nremove_queue=Hapus Antrean\nqueue_name_help=Tentukan nama untuk antrean ini\nqueue_name_describe=Nama antrean adalah {{value}}\nqueue_max_concurrent_download=Jumlah Unduhan Bersamaan Maksimum\nqueue_max_concurrent_download_description=Unduhan maksimal untuk antrean ini\nqueue_automatic_stop=Penghentian otomatis\nqueue_automatic_stop_description=Antrean berhenti otomatis ketika tidak ada butir di dalamnya\nqueue_scheduler=Penjadwal\nqueue_enable_scheduler=Aktifkan Penjadwal\nqueue_active_days=Hari Aktif\nqueue_active_days_description=Pada hari apa penjadwal berfungsi?\nqueue_scheduler_enable_auto_start_time=Aktifkan waktu mulai otomatis\nqueue_scheduler_auto_start_time=Waktu Mulai Otomatis\nqueue_scheduler_enable_auto_stop_time=Aktifkan Waktu Berhenti Otomatis\nqueue_scheduler_auto_stop_time=Waktu Berhenti Otomatis\nqueue_shutdown_on_completion=Matikan Sistem Ketika Selesai\nqueue_shutdown_on_completion_description=Mematikan sistem secara otomatis saat antrean ini selesai. atau ketika waktu akhir yang dijadwalkan tercapai.\nappearance=Tampilan\ndownload_engine=Mesin Unduhan\nbrowser_integration=Integrasi Peramban\nsettings_download_max_retries_count=Jumlah Maksimum Ulangi Unduhan\nsettings_download_max_retries_count_description=Jumlah maksimum kali aplikasi akan mencoba mengunduh ulang file yang gagal sebelum menyerah\nsettings_download_max_retries_count_describe_no_retries=Unduhan yang gagal tidak akan dicoba lagi\nsettings_download_max_retries_count_describe_n_retries=Unduhan yang gagal akan dicoba ulang {{count}} kali\nsettings_download_thread_count=Jumlah Utas\nsettings_download_thread_count_description=Utas unduhan maksimum per butir unduhan\nsettings_download_thread_count_describe=Unduhan dapat memiliki hingga {{count}} utas\nsettings_download_thread_count_with_large_value_describe=Peringatan\\: Memakai jumlah unit proses yang banyak, berpotensi meningkatkan pemakaian sumber daya komputer, mengurangi performa komputer atau menyebabkan masalah koneksi pada komputer penyedia. Gunakan konfigurasi ini jika anda memahami kemungkinan masalah di atas.\nsettings_use_server_last_modified_time=Gunakan Waktu Terakhir Diubah Server\nsettings_use_server_last_modified_time_description=Saat mengunduh file, gunakan waktu modifikasi terakhir server untuk file lokal\nsettings_append_extension_to_incomplete_downloads=Tambahkan Ekstensi ke Unduhan yang Belum Selesai\nsettings_append_extension_to_incomplete_downloads_description=Tambahkan ekstensi \".part\" ke unduhan yang belum selesai. Hal ini membantu mengidentifikasi unduhan yang belum selesai dan mencegah pembukaan file yang belum selesai secara tidak sengaja.\nsettings_use_sparse_file_allocation=Alokasi Berkas Renggang\nsettings_use_sparse_file_allocation_description=Buat berkas lebih efisien, terutama pada SSD, dengan mengurangi penulisan data yang tidak perlu. Ini dapat mempercepat pengunduhan dimulai dan mengurangi penggunaan penyimpanan. Jika unduhan dimulai dengan lambat atau Anda mengalami kecepatan unduh yang tidak biasa, pertimbangkan untuk nonaktifkan opsi ini, karena mungkin tidak didukung sepenuhnya di beberapa perangkat.\nsettings_ignore_ssl_certificates=Abaikan sertifikat SSL\nsettings_ignore_ssl_certificates_description=Nonaktifkan verifikasi sertifikat SSL. Gunakan hanya jika diperlukan, karena hal ini dapat membuat koneksi Anda rentan terhadap risiko keamanan.\nsettings_global_speed_limiter=Pembatas Kecepatan Global\nsettings_global_speed_limiter_description=Batas kecepatan unduh global (0 berarti tidak terbatas)\nsettings_show_average_speed=Tampilkan Kecepatan Rata-rata\nsettings_show_average_speed_description=Kecepatan unduh rata-rata atau presisi\nsettings_use_category_by_default=Gunakan Kategori Secara Bawaan\nsettings_use_category_by_default_description=Gunakan kategori secara bawaan saat menambahkan unduhan.\nsettings_default_download_folder=Folder Unduhan Bawaan\nsettings_default_download_folder_description=Saat kamu menambahkan unduhan baru, lokasi ini digunakan secara bawaan\nsettings_default_download_folder_describe=\"{{folder}}\" akan digunakan\nsettings_use_proxy=Gunakan Proxy\nsettings_use_proxy_description=Gunakan proxy untuk mengunduh berkas\nsettings_use_proxy_describe_no_proxy=Tidak akan menggunakan proxy\nsettings_use_proxy_describe_system_proxy=Proxy Sistem akan digunakan\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" akan digunakan\nsettings_use_proxy_describe_pac_proxy=Berkas PAC \"{{value}} \" akan digunakan\nsettings_track_deleted_files_on_disk=Lacak Berkas yang Dihapus Di Penyimpanan\nsettings_track_deleted_files_on_disk_description=Hapus secara otomatis file dari daftar saat file tersebut dihapus atau dipindahkan dari direktori unduhan.\nsettings_delete_partial_file_on_download_cancellation=Hapus File Sebagian Saat Pembatalan Unduhan\nsettings_delete_partial_file_on_download_cancellation_description=Ketika unduhan dibatalkan, file yang telah diunduh sebagian akan dihapus dari cakram. Hal ini membantu menjaga folder unduhan tetap rapi dan mengurangi penggunaan ruang cakram yang tidak perlu. Namun, unduhan akan dimulai dari awal lagi saat Anda memulainya kembali.\nsettings_default_user_agent=Agen Pengguna Bawaan\nsettings_default_user_agent_description=Tentukan string User Agent Default untuk menentukan cara permintaan diidentifikasi oleh server. Hal ini dapat membantu dalam mengakses konten yang dioptimalkan untuk perangkat tertentu atau dalam menghindari batasan unduhan yang diberlakukan oleh beberapa situs web.\nsettings_download_size_unit=Satuan Ukuran Unduhan\nsettings_download_size_unit_description=Satuan yang digunakan untuk menampilkan ukuran unduhan\nsettings_download_speed_unit=Unit Kecepatan Unduhan\nsettings_download_speed_unit_description=Satuan yang digunakan untuk menampilkan kecepatan unduh\nsettings_theme=Tema\nsettings_theme_description=Pilih tema untuk Aplikasi\nsettings_default_dark_theme=Tema Gelap Bawaan\nsettings_default_dark_theme_description=Terapkan ketika aplikasi mengikuti tema sistem dan mode gelap diaktifkan\nsettings_default_light_theme=Tema Terang Bawaan\nsettings_default_light_theme_description=Terapkan ketika aplikasi mengikuti tema sistem dan mode terang diaktifkan\nsettings_font=Font\nsettings_font_description=Ubah font yang digunakan dalam antarmuka aplikasi. Beberapa font mungkin tidak ditampilkan dengan benar di aplikasi.\nsettings_ui_scale=Skala UI\nsettings_ui_scale_description=Sesuaikan ukuran elemen antarmuka aplikasi\nsettings_language=Bahasa\nsettings_compact_top_bar=Bilah Atas Ringkas\nsettings_compact_top_bar_description=Gabungkan bilah atas dengan bilah judul saat jendela utama memiliki lebar yang cukup\nsettings_use_native_menu_bar=Gunakan Bilah Menu Asli\nsettings_use_native_menu_bar_description=Gunakan gaya bilah menu sistem secara bawaan\nsettings_use_relative_date_time=Gunakan tanggal/waktu secara relatif\nsettings_use_relative_date_time_description=Gunakan format tanggal/waktu secara relatif untuk tanggal aplikasi (misalnya., \"2 hari yang lalu\" bukan tanggal/waktu)\nsettings_show_icon_labels=Tampilkan Label Ikon\nsettings_show_icon_labels_description=Tampilkan label di bawah ikon jika memungkinkan (seperti tindakan pada bilah alat beranda)\nsettings_use_system_tray=Gunakan Baki Sistem\nsettings_use_system_tray_description=Tampilkan ikon baki sistem saat aplikasi sedang berjalan\nsettings_start_on_boot=Jalankan Pada Saat Boot\nsettings_start_on_boot_description=Aplikasi mulai otomatis saat masuk pengguna\nsettings_notification_sound=Suara Notifikasi\nsettings_notification_sound_description=Putar suara pada notifikasi baru\nsettings_browser_integration=Integrasi Peramban\nsettings_browser_integration_description=Terima unduhan dari peramban\nsettings_browser_integration_server_port=Server Port\nsettings_browser_integration_server_port_description=Port untuk integrasi peramban\nsettings_browser_integration_server_port_describe=Aplikasi akan memperhatikan port {{port}}\nsettings_dynamic_part_creation=Pembuatan Bagian Dinamis\nsettings_dynamic_part_creation_description=Saat satu bagian selesai buat bagian lain dengan memisahkan bagian lain untuk meningkatkan kecepatan unduh\nsettings_show_completion_dialog=Tampilkan Dialog Penyelesaian Pengunduhan\nsettings_show_completion_dialog_description=Secara otomatis menampilkan dialog \"Unduh Selesai\" saat unduhan selesai.\nsettings_show_download_progress_dialog=Tampilkan Dialog Progres Unduhan\nsettings_show_download_progress_dialog_description=Tampilkan secara otomatis dialog \"Progres Unduhan\" saat unduhan dimulai.\nsettings_per_host_settings=Per Pengaturan Tuan Rumah\nsettings_per_host_settings_descriptions=Pengaturan ini akan diterapkan secara otomatis pada setiap unduhan baru yang sesuai dengan host yang ditentukan.\nsettings_download_max_concurrent_downloads=Jumlah Unduhan Bersamaan Maksimum\nsettings_download_max_concurrent_downloads_description=Jumlah maksimum file yang dapat diunduh secara bersamaan (unduhan yang dikelola oleh antrian tidak dihitung; atur ke 0 untuk tak terbatas)\ndownload_item_settings_speed_limit=Batas Kecepatan\ndownload_item_settings_speed_limit_description=Batasi kecepatan unduh untuk item ini\ndownload_item_settings_show_download_completion_dialog=Tampilkan dialog Penyelesaian Unduhan\ndownload_item_settings_show_download_completion_dialog_description=Secara otomatis menampilkan dialog \"Penyelesaian Unduhan\" saat pengunduhan selesai.\ndownload_item_settings_shutdown_on_completion=Sistem Matikan Setelah Selesai\ndownload_item_settings_shutdown_on_completion_description=Matikan sistem secara otomatis setelah unduhan ini selesai.\ndownload_item_settings_thread_count=Jumlah Utas\ndownload_item_settings_thread_count_description=Berapa banyak benang yang digunakan untuk mengunduh butir unduhan ini (0 untuk default)\ndownload_item_settings_thread_count_describe={{count}} utas untuk unduhan ini\ndownload_item_settings_username_description=Berikan nama pengguna jika tautan tersebut merupakan sumber daya yang dilindungi\ndownload_item_settings_password_description=Masukkan kata sandi jika tautan tersebut merupakan sumber daya yang dilindungi\ndownload_item_settings_download_page=Halaman Unduhan\ndownload_item_settings_download_page_description=Halaman web tempat unduhan ini dimulai\ndownload_item_settings_file_checksum=Berkas Integritas Data\ndownload_item_settings_file_checksum_description=String hash yang dapat digunakan untuk memeriksa apakah file telah diunduh dengan benar\ndownload_item_settings_user_agent=Agen-Pengguna\ndownload_item_settings_user_agent_description=User-Agent khusus untuk butir ini (biarkan kosong untuk menggunakan default)\nfile_checksum=Berkas pemeriksa integritas data\nfile_checksum_page=Pemeriksa Berkas Integritas Data\nfile_checksum_page_file_checksum_default_algorithm=Algoritma Default\nfile_checksum_page_file_checksum_default_algorithm_help=Algoritma default yang digunakan untuk menghitung checksum file ketika checksum tersebut tidak disediakan.\nstart=Mulai\ncalculated_checksum=Checksum yang dihitung\nsaved_checksum=Integritas Data Tersimpan\nchecksum_algorithm=Algoritma\nfile_not_found=Berkas tidak Ditemukan\ndownload_not_finished=Unduh belum selesai\ndone=Selesai\nwaiting=Menunggu\nmatches=Cocok\nnot_matches=Tidak Cocok\ncopy_to_clipboard=Salin Ke Papan Klip\nusername=Nama Pengguna\npassword=Kata Sandi\naverage_speed=Kecepatan Rata-Rata\nexact_speed=Kecepatan Akurat\nunlimited=Tidak Terbatas\nuse_global_settings=Gunakan Setelan Global\ncant_run_browser_integration=Tidak dapat menjalankan integrasi browser\ncant_open_file=Tidak Dapat Membuka Berkas\ncant_open_folder=Tidak dapat membuka folder\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} tahun\nrelative_time_long_months={{months}} bulan\nrelative_time_long_days={{days}} hari\nrelative_time_long_hours={{hours}} jam\nrelative_time_long_minutes={{minutes}} menit\nrelative_time_long_seconds={{seconds}} detik\nrelative_time_short_years={{years}} t\nrelative_time_short_months={{months}} b\nrelative_time_short_days={{days}} h\nrelative_time_short_hours={{hours}} j\nrelative_time_short_minutes={{minutes}} m\nrelative_time_short_seconds={{seconds}} d\nrelative_time_left={{time}} tersisa\nrelative_time_ago={{time}} lalu\nauto=Otomatis\nunspecified=Tidak ditentukan\ncustom=Sesuaikan\nicon=Ikon\nauthor=Pembuat\nlink=Tautan\nsize=Ukuran\nstatus=Status\nparts_info_downloaded_size=Diunduh\nparts_info_total_size=Total\nspeed=Kecepatan\ntime_left=Waktu Tersisa\ndate_added=Tanggal Ditambahkan\ninfo=Info\ndownload_page_downloaded_size=Diunduh\ndownload_page_download_completed=Unduhan Selesai\nresume_support=Dukungan Melanjutkan\nyes=Ya\nno=Tidak\nparts_info=Info Suku Cadang\ndisconnected=Terputus\nreceiving_data=Menerima Data\nconnecting=Menyambung\nwarning=Peringatan\nunsupported_resume_warning=Unduhan ini tidak mendukung melanjutkan\\! Anda mungkin harus MEMULAI ULANG nanti di Daftar Unduhan\nstop_anyway=Berhenti Pula\ncustomize_columns=Sesuaikan Kolom\nreset=Setel ulang\nmonday=Senin\ntuesday=Selasa\nwednesday=Rabu\nthursday=Kamis\nfriday=Jumat\nsaturday=Sabtu\nsunday=Minggu\nproxy_open_system_proxy_settings=Buka Setelan Proxy Sistem\nproxy_type=Tipe proxy\nproxy_do_not_use_proxy_for=Jangan Gunakan proxy untuk\nproxy_do_not_use_proxy_for_description=Daftar url yang mungkin tidak diproksi\\nAnda dapat menggunakan wildcard dengan *\\nmisalnya 192.168.1.* example.com (dipisahkan spasi)\nproxy_change_title=Ganti Proxy\nchange_proxy=Ganti Proxy\nproxy_no=Tanpa Proksi\nproxy_system=Proxy Sistem\nproxy_manual=Proxy Manual\nproxy_pac=Konfigurasi Perantara Otomatis\nproxy_pac_url=URL untuk Konfigurasi Perantara Otomatis\naddress=Alamat\nport=Port\naddress_and_port=Alamat & Port\nuse_authentication=Gunakan Autentikasi\nwarning_you_may_have_to_restart_the_download_later=Anda mungkin harus memulai ulang unduhan nanti\\!\nedit_download_title=Sunting Unduhan\nedit_download_update_from_download_page=Pembaruan dari Halaman Unduhan\nedit_download_update_from_download_page_description=Saat jendela ini terbuka, Anda dapat pergi ke Halaman Unduh dan klik tombol unduh. Aplikasi akan menangkap dan memperbarui kredensial unduh baru sehingga Anda dapat menyimpannya.\nedit_download_saved_download_item_size_not_match=Butir unduhan yang disimpan memiliki ukuran {{currentSize}}, yang tidak sesuai dengan ukuran baru {{newSize}}.\ntranslators_page_thanks=Dengan Rasa Terima Kasih Kepada Mereka Yang Membantu Menerjemahkan Proyek Ini ❤️\ntranslators=Penerjemah\nlanguage=Bahasa\ntranslators_contribute_title=Tingkatkan Terjemahan\ntranslators_contribute_description=Ingin membantu meningkatkan proyek ini? Jika bahasa Anda tidak terdaftar atau memerlukan beberapa penyesuaian, Anda dapat menyumbangkan terjemahan Anda dan membuatnya lebih baik\\!\ncontribute=Berpartisipasi\nmeet_the_translators=Sapa para penerjemah\nlocalized_by_translators=Diterjemahkan oleh Penerjemah\nconfirm_exit=Konfirmasi untuk keluar\nconfirm_exit_description=Apakah Anda yakin ingin keluar dari Manajer Unduhan AB?\\nUnduhan/Antrean yang sedang aktif akan dihentikan\\!\nupdate=Pembaruan\nupdate_updater=Pembaru\nupdate_available=Pembaruan Tersedia\nupdate_error=Kesalahan pada Pembaruan\nupdate_available_suggest_to_to_update=Anda dapat memperbarui ke versi terbaru untuk menikmati fitur baru, peningkatan, dan perbaikan kinerja.\nupdate_release_notes=Catatan Rilis\nupdate_check_for_update=Cek untuk Pembaruan\nupdate_checking_for_update=Mengecek untuk Pembaruan\nupdate_no_update=Kamu menggunakan versi terbaru\nupdate_check_error=Ada kesalahan saat mengecek pembaruan\nupdate_app_updated_to_version_n=Aplikasi diperbarui ke versi {{version}}\ncreate_desktop_entry=Buat Entri Desktop\nshutdown_alert=Matikan Peringatan\nsystem_shutdown_soon=Sistem Akan Segera Dimatikan\\!\nsystem_shutdown_failed=Sistem Dimatikan Gagal\\!\nsystem_shutdown_soon_description=Sistem akan segera dimatikan. Jika Anda masih menggunakan komputer, silakan simpan pekerjaan Anda atau batalkan proses pematian sistem.\nsystem_shutdown_reason_queue_completed=Semua unduhan dalam antrean selesai.\nsystem_shutdown_reason_queue_end_time_reached=Waktu akhir yang dijadwalkan untuk antrean unduhan tercapai.\nsystem_shutdown_download_finished=Pengunduhan selesai.\nshutdown_now=Matikan Sekarang\nsettings_per_host_settings_new_host=<Host Baru>\nsettings_per_host_settings_not_selected=Buat atau pilih butir baru terlebih dahulu\\!\nsettings_per_host_settings_host=Tuan Rumah\nsettings_per_host_settings_host_description=Pengaturan ini akan diterapkan pada unduhan yang sesuai dengan nama host ini. Karakter wildcard (*) didukung (misalnya, example.com, *.example.com — gunakan hanya salah satu).\nsettings_browser_in_launcher=Ikon Browser Di Peluncur\nsettings_browser_in_launcher_description=Tampilkan atau sembunyikan ikon browser di peluncur (daftar aplikasi).\nsort_by=Urut berdasarkan\nwelcome=Selamat Datang\nnew_folder=Folder Baru\nskip=Lewati\nlets_go=Ayo Mulai\nnext=Berikutnya\nselect_all=Pilih Semua\nselect_inside=Pilih\nselect_invert=Pilih di Dalam\nopen_settings=Buka Pengaturan\nback=Kembali\nservice_is_running=Layanan sedang berjalan\ninitial_setup_description=Mari kita atur semuanya\ninitial_setup_notice=Anda dapat mengubah pengaturan ini kapan saja nanti\npermission_granted=Izin sudah diberikan\npermission_not_granted=Izin tidak diberikan\npermissions=Izin\ngive_permission=Beri izin\ngive_storage_permission=Izinkan akses penyimpanan\nstorage_roots=Akar Penyimpanan\npermissions_initial_title=Pengaturan izin\npermissions_initial_description=Agar dapat berfungsi dengan baik, aplikasi ini memerlukan beberapa izin. Pada layar berikutnya, Anda akan melihat tujuan dari setiap izin dan dapat memutuskan izin mana yang ingin Anda izinkan atau lewati.\npermissions_done_title=Anda sudah siap\npermissions_done_description=Semua sudah siap. Semua izin yang diperlukan telah diberikan dan aplikasi siap digunakan.\npermissions_manage_storage_title=Kelola akses penyimpanan\npermissions_manage_storage_reason=Izin ini memungkinkan aplikasi untuk mengubah folder unduhan, mendeteksi unduhan ganda dengan lebih akurat, dan mengaktifkan beberapa fitur tambahan. Izin ini bersifat optional, tetapi disarankan untuk pengalaman terbaik.\npermission_read_write_external_storage_title=Baca dan tulis penyimpanan\npermission_read_write_external_storage_reason=Izin ini memungkinkan aplikasi untuk menyimpan dan mengelola berkas yang diunduh, mengubah lokasi unduhan, serta meningkatkan deteksi unduhan ganda.\npermissions_post_notification_title=Notifikasi Pos\npermissions_post_notification_reason=Aplikasi ini perlu berjalan di latar belakang untuk mengelola unduhan. Pemberitahuan digunakan untuk memberitahu Anda dan memungkinkan operasi di latar belakang.\npermissions_ignore_battery_optimization_title=Abaikan Optimalisasi Baterai\npermissions_ignore_battery_optimization_reason=Beberapa perangkat secara agresif membatasi aktivitas latar belakang untuk menghemat baterai, yang dapat menghentikan atau menunda unduhan saat aplikasi tidak terbuka. Anda dapat secara opsional mengecualikan aplikasi dari pengoptimalan baterai untuk memastikan unduhan terus berlanjut tanpa terputus.\nopen_in_browser=Buka Di Browser\nbrowser=Browser\nbrowser_new_tab=Tab Baru\nbrowser_close_tab=Tutup Tab\nbrowser_open_in_new_tab=Buka di Tab Baru\nbrowser_open_in_new_background_tab=Buka di Tab Latar Belakang Baru\nbrowser_no_tab_open=Tidak ada tab yang terbuka\nbrowser_tabs=Tab\nbrowser_paste_and_go=Tempel dan Jalankan\nbrowser_bookmarks=Markah\nbrowser_add_bookmark=Tambahkan Markah\nbrowser_edit_bookmark=Sunting Markah\nbrowser_add_to_bookmarks=Tambah Ke Penanda\nbrowser_remove_from_bookmarks=Hapus Dari Penanda\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/it_IT.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Categorizzazione automatica download\nconfirm_auto_categorize_downloads_description=Qualsiasi elemento non categorizzato sarà automaticamente aggiunto alla categoria correlata.\nconfirm_reset_to_default_categories_title=Ripristina categorie predefinite\nconfirm_reset_to_default_categories_description=Questo RIMUOVERÀ tutte le categorie e ripristinerà le categorie predefinite\\!\nconfirm_delete_download_items_title=Conferma eliminazione\nconfirm_delete_download_items_description=Vuoi eliminare {{count}} elementi?\nconfirm_delete_download_unfinished_items_description=Vuoi eliminare {{count}} download non completati?\nconfirm_delete_download_finished_and_unfinished_items_description=Vuoi eliminare {{finishedCount}} download completati e {{unfinishedCount}} download non completati?\nalso_delete_file_from_disk=Elimina anche il file dal disco\nconfirm_delete_category_item_title=Rimozione categoria {{name}}\nconfirm_delete_category_item_description=Vuoi eliminare la categoria \"{{value}}\"?\nyour_download_will_not_be_deleted=I download non verranno eliminati\ndrag_the_file_to_another_app=Trascina il file su un'altra applicazione\ndrop_link_or_file_here=Trascina il collegamento o il file qui.\nnothing_will_be_imported=Niente sarà importato\nn_links_will_be_imported=Saranno importati {{count}} collegamenti\nn_items_selected={{count}} elementi selezionati\nwindow_close=Chiudi\nwindow_minimize=Minimizza\nwindow_maximize=Ingrandisci\nwindow_restore=Ripristina\ndelete=Elimina\nremove=Rimuovi\ncancel=Annulla\nclose=Chiudi\nmenu=Menù\nmore_options=Altre opzioni\nok=Ok\nadd=Aggiungi\npaste=Incolla\nchange=Modifica\nedit=Modifica\nchange_anyway=Cambia Comunque\ndownload=Download\nrefresh=Aggiorna\nsettings=Impostazioni\non_completion=In Completamento\nunknown=Sconosciuto\nunknown_error=Errore sconosciuto\ndownload_item_not_found=Elemento download non trovato\nname=Nome\ndownload_link=Collegamento download\nnot_finished=Non completato\nall=Tutti\nfinished=Completati\nUnfinished=Non completati\ncanceled=Annullato\nerror=Errore\npaused=In pausa\ndownloading=Download\nadded=Aggiunto\nidle=NON ATTIVO\npreparing_file=Preparazione file\ncreating_file=Creazione file\nresuming=Ripresa\nretrying=Nuovo tentativo\nlist_is_empty=L'elenco è vuoto\\!\nsearch_in_the_list=Cerca nell'elenco\nsearch=Cerca\nclear=Azzera\ngeneral=Generale\nenabled=Abilitato\ndisabled=Disabilitato\ndefault=Predefinito\nfile=File\ntasks=Attività\ntools=Strumenti\nhelp=Aiuto\nsystem=Sistema\nall_missing_files=Tutti i file mancanti\nall_finished=Tutti completati\nall_unfinished=Tutti non completati\nentire_list=Intero elenco\ndownload_browser_integration=Download estensione browser\nexit=Esci\nshow_downloads=Visualizza download\nnew_download=Aggiungi nuovo download\nstop_all=Interrompi tutto\nimport_from_clipboard=Importa dagli appunti\nbatch_download=Download batch\nopen=Apri\nshare=Condividi\nopen_file=Apri file\nopen_folder=Apri cartella\nresume=Riprendi\npause=Metti in pausa\nrestart_download=Riavvia download\ncopy=Copia\ncopy_link=Copia collegamento\ncopy_as_curl=Copia come cURL\nshow_properties=Visualizza proprietà\nmove_to_queue=Sposta nella coda\nmove_to_this_queue=Sposta in questa coda\nmove_to_category=Muovi nella categoria\nmove_to_this_category=Sposta in questa categoria\ncategories=Categorie\nadd_category=Aggiungi categoria\nedit_category=Modifica categoria\ndelete_category=Elimina categoria\ncategory_name=Nome categoria\ncategory_download_location=Percorso download categoria\ncategory_download_location_description=Quando in \"Aggiungi download\" è scelta questa categoria, usa questo cartella come \"Percorso salvataggio\"\ncategory_file_types=Tipi di file categoria\ncategory_file_types_description=Metti automaticamente questi tipi di file in questa categoria (quando aggiungi un nuovo download).\\nSepara le estensioni dei file con uno spazio (ext1 ext2 ...)\ncategory_url_patterns=Modelli URL\ncategory_url_patterns_description=Metti automaticamente i download da queste URL in questa categoria (quando aggiungi un nuovo download).\\nSepara gli URL con uno spazio, puoi anche usare * come carattere jolly\nauto_categorize_downloads=Categorizza automaticamente i download\nrestore_defaults=Ripristina predefiniti\nabout=Info programma\nversion_n=Versione {{value}}\ndeveloped_with_love_for_you=Sviluppato con ❤️ per te\ndonate=Dona\nvisit_the_project_website=Visita il sito del progetto\nthis_is_a_free_and_open_source_software=Questo è un software gratuito e open source\nview_the_source_code=Guarda il codice sorgente\nthird_party_libraries=Librerie di terze parti\npowered_by_open_source_software=Supportato da Open Software Source\nview_the_open_source_licenses=Visualizza le licenze open source\nsupport_and_community=Supporto e comunità\ntelegram=Telegram\nchannel=Canale\ngroup=Gruppo\nadd_download=Aggiungi download\nadd_multi_download_page_header=Seleziona gli elementi che vuoi scaricare\nsave_to=Salva in\nwhere_should_each_item_saved=Dove vuoi salvare ciascun elemento?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Ci sono più elementi\\!\\nSeleziona un modo in cui salvarli\neach_item_on_its_own_category=Ogni elemento nella propria categoria\neach_item_on_its_own_category_description=Ogni elemento sarà collocato in una categoria corrispondente al tipo di file\nall_items_in_one_category=Tutti gli elementi in un'unica categoria\nall_items_in_one_category_description=Tutti i file saranno salvati nel percorso della categoria selezionata\nall_items_in_one_Location=Tutti gli elementi in un unico percorso\nall_items_in_one_Location_description=Tutti gli elementi saranno salvati nella cartella selezionata\nunselected_all_items_in_specific_location_description=Tutti i file verranno salvati nel percorso della categoria selezionata\nno_category_selected=Nessuna categoria selezionata\nno_categories_found=Nessuna categoria trovata\ndownload_location=Percorso download\nlocation=Percorso\nselect_queue=Seleziona coda\nwithout_queue=Nessuna coda\nuse_category=Usa categoria\ncant_write_to_this_folder=Impossibile scrivere in questa cartella\nfile_name_already_exists=Il nome del file esiste già\ndownload_already_exists=Il download esiste già\ninvalid_file_name=Nome file non valido\nshow_solutions=Visualizza soluzioni...\nchange_solution=Cambia soluzione\nselect_a_solution=Seleziona una soluzione\nselect_download_strategy_description=Il collegamento fornito è già presente nell'elenco download, specifica cosa vuoi fare\ndownload_strategy_add_a_numbered_file=Aggiungi un file numerato\ndownload_strategy_add_a_numbered_file_description=Aggiungi un indice alla fine del nome file scaricato\ndownload_strategy_override_existing_file=Sovrascrivi file esistente\ndownload_strategy_override_existing_file_description=Rimuovi il download esistente e scrivi in quel file\ndownload_strategy_update_download_link=Aggiorna il download esistente\ndownload_strategy_update_download_link_description=Aggiornare il link di download esistente e le sue credenziali\ndownload_strategy_show_downloaded_file=Visualizza file scaricato\ndownload_strategy_show_downloaded_file_description=Visualizza l'elemento download già esistente, così puoi scegliere se riprenderlo o aprirlo\nbatch_download_link_help=Inserisci un collegamento che contiene caratteri jolly (usa *)\ninvalid_url=URL non valida\nlist_is_too_large_maximum_n_items_allowed=L''elenco è troppo grande - consentiti massimo {{count}} elementi\nenter_range=Inserisci intervallo\nrange_from=Da\nrange_to=A\nbatch_download_wildcard_length=Lunghezza carattere jolly\nfirst_link=Primo collegamento\nlast_link=Ultimo collegamento\nopen_source_software_used_in_this_app=Software open source usato in questa app\nlinks=Collegamento\nwebsite=Sito web\ndevelopers=Sviluppatori\nsource_code=Codice sorgente\nlicense=Licenza\nno_license_found=Nessuna licenza trovata\norganization=Organizzazione\nadd_new_queue=Aggiungi nuova coda\nqueue_name=Nome coda\nqueues=Code\nstop_queue=Ferma coda\nstart_queue=Avvia coda\nclear_queue_items=Svuota Coda\nconfig=Configurazione\nitems=Elementi\nmove_down=Sposta giù\nmove_up=Sposta su\nremove_queue=Rimuovi coda\nqueue_name_help=Specifica un nome per questa coda\nqueue_name_describe=Il nome della coda è {{value}}\nqueue_max_concurrent_download=Download simultanei massimi\nqueue_max_concurrent_download_description=Download max per questa coda\nqueue_automatic_stop=Interruzione automatica\nqueue_automatic_stop_description=Interrompi automaticamente la coda quando non ci sono elementi\nqueue_scheduler=Pianificazione\nqueue_enable_scheduler=Abilita pianificazione\nqueue_active_days=Giorni pianificazione\nqueue_active_days_description=In quali giorni è attiva la pianificazione?\nqueue_scheduler_enable_auto_start_time=Abilita Ora Di Avvio Automatico\nqueue_scheduler_auto_start_time=Orario avvio automatico\nqueue_scheduler_enable_auto_stop_time=Abilita orario arresto automatico\nqueue_scheduler_auto_stop_time=Orario arresto automatico\nqueue_shutdown_on_completion=Arresto del sistema al completamento\nqueue_shutdown_on_completion_description=Spegni automaticamente il sistema quando questa coda è completata o quando l'ora di fine pianificata è raggiunta.\nappearance=Aspetto\ndownload_engine=Motore download\nbrowser_integration=Integrazione browser\nsettings_download_max_retries_count=N. max ritentativi\nsettings_download_max_retries_count_description=Numero massimo di volte che l'app riproverà un download non riuscito prima di arrendersi\nsettings_download_max_retries_count_describe_no_retries=Non verranno effettuati ritentativi per i download non riusciti\nsettings_download_max_retries_count_describe_n_retries=Per i download falliti verranno effettuati {{count}} ritentativi\nsettings_download_thread_count=Numero thread\nsettings_download_thread_count_description=Numero max thread per ogni elemento download\nsettings_download_thread_count_describe=Un download può avere fino a {{count}} thread\nsettings_download_thread_count_with_large_value_describe=Avviso\\: l'impostazione di un numero elevato di thread può aumentare l'uso delle risorse di sistema, ridurre le prestazioni o causare problemi di connessione con i server. \\nUsa valori più alti solo se comprendi il potenziale impatto sul sistema e sulla rete.\nsettings_use_server_last_modified_time=Usa l'ora dell'ultima modifica nel server\nsettings_use_server_last_modified_time_description=Quando scarichi un file, usa per il file locale l'orario dell'ultima modifica nel server\nsettings_append_extension_to_incomplete_downloads=Aggiungi Estensione al Download Incompleto\nsettings_append_extension_to_incomplete_downloads_description=Aggiunge l'estensione \".part\" ai download incompleti. Questo aiuta a identificare i download incompleti e impedisce l'apertura accidentale di file incompleti.\nsettings_use_sparse_file_allocation=Allocazione segmenti file\nsettings_use_sparse_file_allocation_description=Crea file in modo più efficiente, specialmente su SSD, riducendo la scrittura di dati non necessari.\\nQuesto può accelerare l'avvio dei download e ridurre l'uso del disco.\\nSe i download iniziano lentamente o riscontri velocità di download insolite, considera di disabilitare questa opzione, poiché potrebbe non essere completamente supportata in alcuni dispositivi.\nsettings_ignore_ssl_certificates=Ignora certificati SSL\nsettings_ignore_ssl_certificates_description=Disabilita la verifica del certificato SSL. \\nUsa questa opzione solo se necessario, in quanto può esporre la connessione ai rischi per la sicurezza.\nsettings_global_speed_limiter=Limitatore velocità globale\nsettings_global_speed_limiter_description=Limite la velocità di download globale (0 significa illimitata)\nsettings_show_average_speed=Visualizza velocità media\nsettings_show_average_speed_description=Velocità download  media o precisa\nsettings_use_category_by_default=Usa la categoria per impostazione predefinita\nsettings_use_category_by_default_description=Quando si aggiunge un download usa la categoria per impostazione predefinita.\nsettings_default_download_folder=Cartella download predefinita\nsettings_default_download_folder_description=Quando aggiungi un nuovo download, per impostazione predefinita è usato questa percorso\nsettings_default_download_folder_describe=Sara usata \"{{folder}}\"\nsettings_use_proxy=Usa proxy\nsettings_use_proxy_description=Per scaricare i file usa un proxy \nsettings_use_proxy_describe_no_proxy=Non verrà usato alcun proxy\nsettings_use_proxy_describe_system_proxy=Verrà usato il proxy di sistema\nsettings_use_proxy_describe_manual_proxy=Sarà usato \"{{value}}\"\nsettings_use_proxy_describe_pac_proxy=verrà usato il file pac \"{{value}}\"\nsettings_track_deleted_files_on_disk=Tieni traccia dei file eliminati dal disco\nsettings_track_deleted_files_on_disk_description=Rimuovi automaticamente i file dall'elenco quando vengono eliminati o spostati nella cartella download.\nsettings_delete_partial_file_on_download_cancellation=Elimina il file parziale all'annullamento del download\nsettings_delete_partial_file_on_download_cancellation_description=Quando un download viene annullato, il file parzialmente scaricato verrà eliminato dal disco. Questo aiuta a mantenere pulita la cartella di download e riduce l'utilizzo non necessario dello spazio su disco. Tuttavia, il download ricomincerà dall'inizio al successivo avvio.\nsettings_default_user_agent=User agent predefinito\nsettings_default_user_agent_description=Specificare la stringa dello user agent predefinito per definire come identificare le richieste ai server. \\nCiò può aiutare ad accedere ai contenuti ottimizzati per dispositivi particolari o ad eludere le limitazioni di download imposte da alcuni siti web.\nsettings_download_size_unit=Unità velocità download\nsettings_download_size_unit_description=Unità usata per visualizzare la velocità di download\nsettings_download_speed_unit=Unità velocità download\nsettings_download_speed_unit_description=Unità usata per visualizzare la velocità di download\nsettings_theme=Tema\nsettings_theme_description=Seleziona un tema per l'app\nsettings_default_dark_theme=Tema Scuro Predefinito\nsettings_default_dark_theme_description=Si applica quando l'applicazione segue il tema di sistema e la modalità scura è attiva\nsettings_default_light_theme=Tema Chiaro Predefinito\nsettings_default_light_theme_description=Si applica quando l'applicazione segue il tema di sistema e la modalità chiara è attiva\nsettings_font=Tipo di carattere\nsettings_font_description=Cambia il carattere usato nell'interfaccia dell'app, alcuni caratteri potrebbero non essere visualizzati correttamente nell'app.\nsettings_ui_scale=Scala UI\nsettings_ui_scale_description=Regola la dimensione degli elementi dell'interfaccia dell'app\nsettings_language=Lingua UI\nsettings_compact_top_bar=Barra superiore compatta\nsettings_compact_top_bar_description=Quando la finestra principale ha abbastanza larghezza unisci la barra superiore con la barra del titolo \nsettings_use_native_menu_bar=Usa la Barra dei Menu Nativa\nsettings_use_native_menu_bar_description=Utilizzare lo stile predefinito della barra dei menu del sistema\nsettings_use_relative_date_time=Usa data/ora relativa\nsettings_use_relative_date_time_description=Usa il formato relativo di data/ora per le date nell'applicazione (ad esempio, \"2 giorni fa\" invece della data/ora esatta)\nsettings_show_icon_labels=Visualizza etichette icone\nsettings_show_icon_labels_description=Visualizza etichette nelle icone quando possibile (come le azioni della barra strumenti home)\nsettings_use_system_tray=Usa icona barra sistema\nsettings_use_system_tray_description=Quando l'app è in esecuzione visualizza l'icona nella barra sistema\nsettings_start_on_boot=Esegui ad avvio sistema\nsettings_start_on_boot_description=Esegue automaticamente l'applicazione all'avvio del sistema\nsettings_notification_sound=Suono notifica\nsettings_notification_sound_description=Riproduci suono per una nuova notifica\nsettings_browser_integration=Integrazione browser\nsettings_browser_integration_description=Accetta download dai browser\nsettings_browser_integration_server_port=Porta server\nsettings_browser_integration_server_port_description=Porta integrazione browser\nsettings_browser_integration_server_port_describe=L''app rimarrà in ascolto sulla porta {{port}}\nsettings_dynamic_part_creation=Creazione dinamica blocchi\nsettings_dynamic_part_creation_description=Per aumentare la velocità in download quando un blocco è completato, crea un'altro blocco dividendo gli altri blocchi \nsettings_show_completion_dialog=Visualizza finestra \"Download completato\"\nsettings_show_completion_dialog_description=A download completato visualizza automaticamente la finestra \"Download completato\".\nsettings_show_download_progress_dialog=Visualizza finestra \"Avanzamento download\"\nsettings_show_download_progress_dialog_description=All'avvio di un download visualizza automaticamente la finestra \"Avanzamento download\".\nsettings_per_host_settings=Impostazioni Per Host\nsettings_per_host_settings_descriptions=Queste impostazioni verranno applicate automaticamente a qualsiasi nuovo download che corrisponda all'host specificato.\nsettings_download_max_concurrent_downloads=Numero massimo di Download simultanei\nsettings_download_max_concurrent_downloads_description=Il numero massimo di file che possono essere scaricati contemporaneamente (i download gestiti dalle code non sono conteggiati; imposta a 0 per illimitati)\ndownload_item_settings_speed_limit=Limite di velocità\ndownload_item_settings_speed_limit_description=Limita la velocità in download per questo elemento\ndownload_item_settings_show_download_completion_dialog=Visualizza finestra \"Download completato\"\ndownload_item_settings_show_download_completion_dialog_description=A download completato visualizza automaticamente la finestra \"Download completato\".\ndownload_item_settings_shutdown_on_completion=Arresto del sistema al completamento\ndownload_item_settings_shutdown_on_completion_description=Spegni automaticamente il sistema quando il download è terminato.\ndownload_item_settings_thread_count=Numero di thread\ndownload_item_settings_thread_count_description=N. thread usati per scaricare questo elemento (0 \\= predefinito)\ndownload_item_settings_thread_count_describe={{count}} thread per questo download\ndownload_item_settings_username_description=Se il collegamento è una risorsa protetta inserisci il nome utente \ndownload_item_settings_password_description=Se il collegamento è una risorsa protetta inserisci la password\ndownload_item_settings_download_page=Pagina web download\ndownload_item_settings_download_page_description=La pagina web da cui è stato avviato questo download\ndownload_item_settings_file_checksum=Checksum file\ndownload_item_settings_file_checksum_description=Una stringa hash che può essere usata per verificare se il file è stato scaricato correttamente\ndownload_item_settings_user_agent=User Agent\ndownload_item_settings_user_agent_description=User-Agent personalizzato per questo elemento (lasciare vuoto per usare il predefinito)\nfile_checksum=Checksum file\nfile_checksum_page=Controllo checksum file\nfile_checksum_page_file_checksum_default_algorithm=Algoritmo predefinito\nfile_checksum_page_file_checksum_default_algorithm_help=L'algoritmo predefinito usato per calcolare i checksum dei file quando non vengono forniti.\nstart=Avvia\ncalculated_checksum=Checksum calcolato\nsaved_checksum=Checksum salvato\nchecksum_algorithm=Algoritmo\nfile_not_found=File non trovato\ndownload_not_finished=Download non completato\ndone=Completato\nwaiting=Attendi\nmatches=Corrispondenze\nnot_matches=Non corrispondenze\ncopy_to_clipboard=Copia negli appunti\nusername=Nome utente\npassword=Password\naverage_speed=Velocità media\nexact_speed=Velocità esatta\nunlimited=Illimitata\nuse_global_settings=Usa impostazioni globali\ncant_run_browser_integration=Impossibile eseguire l'integrazione nel browser\ncant_open_file=Impossibile aprire il file\ncant_open_folder=Impossibile aprire la cartella\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} anni\nrelative_time_long_months={{months}} mesi\nrelative_time_long_days={{days}} giorni\nrelative_time_long_hours={{hours}} ore\nrelative_time_long_minutes={{minutes}} minuti\nrelative_time_long_seconds={{seconds}} secondi\nrelative_time_short_years={{years}} a\nrelative_time_short_months={{months}} m\nrelative_time_short_days={{days}} g\nrelative_time_short_hours={{hours}} h\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} sec\nrelative_time_left={{time}} rimanente\nrelative_time_ago={{time}} fa\nauto=Auto\nunspecified=Non specificato\ncustom=Personalizzato\nicon=Icona\nauthor=Autore\nlink=Collegamento\nsize=Dimensione\nstatus=Stato\nparts_info_downloaded_size=Scaricato\nparts_info_total_size=Totale\nspeed=Velocità\ntime_left=Tempo rimanente\ndate_added=Data aggiunta\ninfo=Info\ndownload_page_downloaded_size=Scaricato\ndownload_page_download_completed=Download completato\nresume_support=Supporta ripresa\nyes=Sì\nno=No\nparts_info=Informazioni sui blocchi\ndisconnected=Disconnesso\nreceiving_data=Ricezione dati\nconnecting=Connessione\nwarning=Avviso\nunsupported_resume_warning=Questo download non supporta la ripresa\\!\\nPotresti dover RIAVVIARLO più tardi nell'elenco download\nstop_anyway=Ferma comunque\ncustomize_columns=Personalizza colonne\nreset=Ripristina\nmonday=Lunedì\ntuesday=Martedì\nwednesday=Mercoledì\nthursday=Giovedì\nfriday=Venerdì\nsaturday=Sabato\nsunday=Domenica\nproxy_open_system_proxy_settings=Apri impostazioni proxy sistema\nproxy_type=Tipo di proxy\nproxy_do_not_use_proxy_for=Non usare proxy per\nproxy_do_not_use_proxy_for_description=Un elenco di URL che potrebbero non supportare il proxy\\nPuoi usare caratteri jolly con *\\nper esempio 192.168.1.* example.com (separati da spazi)\nproxy_change_title=Modifica proxy\nchange_proxy=Modifica proxy\nproxy_no=Nessun proxy\nproxy_system=Proxy di sistema\nproxy_manual=Proxy manuale\nproxy_pac=Configurazione automatica proxy\nproxy_pac_url=URL configurazione automatica proxy\naddress=Indirizzo\nport=Porta\naddress_and_port=Indirizzo e porta\nuse_authentication=Usa autenticazione\nwarning_you_may_have_to_restart_the_download_later=Potresti dover riavviare il download più tardi\\!\nedit_download_title=Modifica download\nedit_download_update_from_download_page=Aggiorna dalla pagina di download\nedit_download_update_from_download_page_description=Quando questa finestra è aperta, puoi andare alla pagina di download e selezionare il pulsante di download.\\nL'app catturerà e aggiornerà le nuove credenziali di download in modo da poterle salvare.\nedit_download_saved_download_item_size_not_match=L''elemento di download salvato ha una dimensione di {{currentSize}}, che non corrisponde alla nuova dimensione di {{newSize}}.\ntranslators_page_thanks=Con gratitudine a coloro che ci hanno aiutato a tradurre questo progetto ❤️\ntranslators=Traduttori\nlanguage=Lingua\ntranslators_contribute_title=Migliora le traduzioni\ntranslators_contribute_description=Vuoi aiutare a migliorare questo progetto?\\nSe una lingua non è elencata o ha bisogno di alcune modifiche, puoi contribuire e renderla migliore con le tue traduzioni\\!\ncontribute=Contribuisci\nmeet_the_translators=Incontra i traduttori\nlocalized_by_translators=Localizzato dai traduttori\nconfirm_exit=Conferma uscita\nconfirm_exit_description=Vuoi uscire da AB Download Manager?\\nI download/code attivi verranno interrotti\\!\nupdate=Aggiorna\nupdate_updater=Aggiornamento\nupdate_available=Aggiornamento disponibile\nupdate_error=Errore durante l'aggiornamento\nupdate_available_suggest_to_to_update=Puoi aggiornare alla versione più recente per usufruire di nuove funzionalità, miglioramenti e miglioramenti delle prestazioni.\nupdate_release_notes=Note sulla versione\nupdate_check_for_update=Controlla aggiornamenti\nupdate_checking_for_update=Controllo aggiornamenti\nupdate_no_update=Questa versione è aggiornata\nupdate_check_error=Errore durante il controllo degli aggiornamenti\nupdate_app_updated_to_version_n=App aggiornata lla versione {{version}}\ncreate_desktop_entry=Crea Elemento Desktop\nshutdown_alert=Avviso Di Arresto\nsystem_shutdown_soon=Il sistema si spegnerà presto\\!\nsystem_shutdown_failed=Arresto del sistema non riuscito\\!\nsystem_shutdown_soon_description=Il sistema si spegnerà presto. Se stai ancora usando il computer, per favore salva il tuo lavoro o annulla l'arresto.\nsystem_shutdown_reason_queue_completed=Tutti i download nella coda sono stati completati.\nsystem_shutdown_reason_queue_end_time_reached=Ora di fine pianificata per la coda di download raggiunta.\nsystem_shutdown_download_finished=Download completato.\nshutdown_now=Spegni Ora\nsettings_per_host_settings_new_host=<Nuovo Host>\nsettings_per_host_settings_not_selected=Crea o seleziona un nuovo elemento\\!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=Queste impostazioni verranno applicate ai download che corrispondono a questo nome host. I caratteri jolly (*) sono supportati (ad esempio, example.com, *.example.com — utilizzarne solo uno).\nsettings_browser_in_launcher=Icona del browser nel Launcher\nsettings_browser_in_launcher_description=Mostra o nasconde l'icona del browser nel launcher (elenco app).\nsort_by=Ordina per\nwelcome=Benvenuto\nnew_folder=Nuova cartella\nskip=Salta\nlets_go=Si comincia\nnext=Avanti\nselect_all=Seleziona Tutto\nselect_inside=Seleziona Dentro\nselect_invert=Seleziona Inverti\nopen_settings=Apri Impostazioni\nback=Indietro\nservice_is_running=Il servizio è in esecuzione\ninitial_setup_description=\ninitial_setup_notice=Puoi modificare queste impostazioni in qualsiasi momento\npermission_granted=Autorizzazione concessa\npermission_not_granted=Autorizzazione non concessa\npermissions=Autorizzazioni\ngive_permission=Consenti autorizzazioni\ngive_storage_permission=Consenti l'accesso allo spazio di archiviazione\nstorage_roots=Storage Roots\npermissions_initial_title=Configurazione autorizzazioni\npermissions_initial_description=Per funzionare correttamente, l'app necessita di alcuni permessi. Nella schermata successiva, vedrai per cosa viene utilizzato ogni permesso e potrai decidere quali permessi permettere o saltare.\npermissions_done_title=È tutto pronto\npermissions_done_description=Tutto è pronto. Tutti i permessi richiesti sono stati concessi e l'app è pronta per l'uso.\npermissions_manage_storage_title=Gestisci l'accesso all'archivio\npermissions_manage_storage_reason=Questa autorizzazione consente all'app di cambiare la cartella di download, rilevare i download duplicati in modo più accurato e abilitare alcune funzionalità aggiuntive. È opzionale, ma consigliata per la migliore esperienza.\npermission_read_write_external_storage_title=Lettura e scrittura memoria\npermission_read_write_external_storage_reason=Questa autorizzazione consente all'app di salvare e gestire i file scaricati, modificare la posizione di download e migliorare il rilevamento dei download duplicati.\npermissions_post_notification_title=Pubblica una notifica\npermissions_post_notification_reason=L'applicazione deve essere eseguita in background per gestire i download. Le notifiche vengono utilizzate per tenerti informato e consentire operazioni in background.\npermissions_ignore_battery_optimization_title=Ignora Ottimizzazioni Batteria\npermissions_ignore_battery_optimization_reason=Alcuni dispositivi limitano aggressivamente l'attività in background per risparmiare batteria, che può mettere in pausa o fermare i download quando l'applicazione non è aperta. È possibile escludere l'app dall'ottimizzazione della batteria per garantire che i download continuino ininterrotti\nopen_in_browser=Apri nel browser\nbrowser=Browser\nbrowser_new_tab=Nuova Scheda\nbrowser_close_tab=Chiudi scheda\nbrowser_open_in_new_tab=Apri in una nuova scheda\nbrowser_open_in_new_background_tab=Apri In Nuova Scheda In Secondo Piano\nbrowser_no_tab_open=Nessuna scheda aperta\nbrowser_tabs=Schede\nbrowser_paste_and_go=Incolla e vai\nbrowser_bookmarks=Segnalibri\nbrowser_add_bookmark=Aggiungi segnalibro\nbrowser_edit_bookmark=Modifica Segnalibro\nbrowser_add_to_bookmarks=Aggiungi ai segnalibri\nbrowser_remove_from_bookmarks=Rimuovi dai Segnalibri\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ja_JP.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=ダウンロードの自動分類\nconfirm_auto_categorize_downloads_description=カテゴリ未設定の項目は、自動的に関連するカテゴリに追加されます。\nconfirm_reset_to_default_categories_title=既定のカテゴリにリセット\nconfirm_reset_to_default_categories_description=すべてのカテゴリを削除し、既定のカテゴリを復元します\\!\nconfirm_delete_download_items_title=削除の確認\nconfirm_delete_download_items_description={{count}} 個の項目を削除しますか?\nconfirm_delete_download_unfinished_items_description={{count}} 個の未完了のダウンロードを削除してもよろしいですか?\nconfirm_delete_download_finished_and_unfinished_items_description={{finishedCount}} 個の完了と {{unfinishedCount}} 個の未完了のダウンロードを削除してもよろしいですか?\nalso_delete_file_from_disk=ディスク上のファイルも削除\nconfirm_delete_category_item_title={{name}} カテゴリを削除\nconfirm_delete_category_item_description=カテゴリ \"{{value}}\" を削除しますか?\nyour_download_will_not_be_deleted=ダウンロードは削除されません\ndrag_the_file_to_another_app=ファイルを別のアプリにドラッグ\ndrop_link_or_file_here=ここにリンクまたはファイルをドロップします。\nnothing_will_be_imported=インポートされるものはありません\nn_links_will_be_imported={{count}} 個のリンクをインポートします\nn_items_selected={{count}} 個の項目を選択\nwindow_close=閉じる\nwindow_minimize=最小化\nwindow_maximize=最大化\nwindow_restore=復元\ndelete=削除\nremove=消去\ncancel=キャンセル\nclose=閉じる\nmenu=メニュー\nmore_options=その他のオプション\nok=はい\nadd=追加\npaste=貼り付け\nchange=変更\nedit=編集\nchange_anyway=変更を適用\ndownload=ダウンロード\nrefresh=更新\nsettings=設定\non_completion=完了時\nunknown=不明\nunknown_error=不明なエラー\ndownload_item_not_found=ダウンロード項目が見つかりません\nname=名前\ndownload_link=ダウンロード リンク\nnot_finished=未完了\nall=全て\nfinished=完了\nUnfinished=未完了\ncanceled=キャンセル\nerror=エラー\npaused=一時停止中\ndownloading=ダウンロード中\nadded=追加済み\nidle=待機中\npreparing_file=ファイルの準備中\ncreating_file=ファイルの作成中\nresuming=再開中\nretrying=再試行中\nlist_is_empty=リストは空です\\!\nsearch_in_the_list=リスト内を検索\nsearch=検索\nclear=クリア\ngeneral=全般\nenabled=有効\ndisabled=無効\ndefault=既定\nfile=ファイル\ntasks=タスク\ntools=ツール\nhelp=ヘルプ\nsystem=システム\nall_missing_files=すべての不足しているファイル\nall_finished=すべて完了\nall_unfinished=すべての未完了\nentire_list=リスト全体\ndownload_browser_integration=ブラウザー統合をダウンロード\nexit=終了\nshow_downloads=ダウンロードを表示\nnew_download=新しいダウンロード\nstop_all=すべて停止\nimport_from_clipboard=クリップボードから取り込み\nbatch_download=一括ダウンロード\nopen=開く\nshare=共有\nopen_file=ファイルを開く\nopen_folder=フォルダーを開く\nresume=再開\npause=一時停止\nrestart_download=ダウンロードを最初からやり直す\ncopy=コピー\ncopy_link=リンクをコピー\ncopy_as_curl=cURL としてコピー\nshow_properties=プロパティを表示\nmove_to_queue=キューへ移動\nmove_to_this_queue=このキューへ移動\nmove_to_category=カテゴリへ移動\nmove_to_this_category=このカテゴリへ移動\ncategories=カテゴリ\nadd_category=カテゴリを追加\nedit_category=カテゴリの編集\ndelete_category=カテゴリの削除\ncategory_name=カテゴリ名\ncategory_download_location=カテゴリの保存先\ncategory_download_location_description=\"ダウンロードの追加\" でこのカテゴリを選択した場合、このディレクトリを \"保存先\" として使用します\ncategory_file_types=カテゴリのファイルの種類\ncategory_file_types_description=新しいダウンロードを追加するとき、これらのファイルの種類は自動的にこのカテゴリに入ります。\\n拡張子はスペースで区切って指定します(ext1 ext2 ...)。\ncategory_url_patterns=URL パターン\ncategory_url_patterns_description=新しいダウンロードを追加するとき、これらの URL からのダウンロードは自動的にこのカテゴリに入ります。\\nURL はスペースで区切って指定できます。ワイルドカードとして * を使用できます。\nauto_categorize_downloads=自動でカテゴリ分け\nrestore_defaults=既定に戻す\nabout=概要\nversion_n=バージョン {{value}}\ndeveloped_with_love_for_you=開発者\\: ❤️\ndonate=寄付\nvisit_the_project_website=プロジェクトのウェブサイトを開く\nthis_is_a_free_and_open_source_software=これは無料のオープン ソース ソフトウェアです\nview_the_source_code=ソース コードを見る\nthird_party_libraries=サードパーティ ライブラリ\npowered_by_open_source_software=オープン ソース ソフトウェアを利用しています\nview_the_open_source_licenses=オープン ソース ライセンスを表示\nsupport_and_community=サポートとコミュニティ\ntelegram=Telegram\nchannel=チャンネル\ngroup=グループ\nadd_download=ダウンロードを追加\nadd_multi_download_page_header=ダウンロードする項目を選択してください\nsave_to=保存先\nwhere_should_each_item_saved=各項目の保存先を指定しますか?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=複数の項目があります。保存方法を選択してください。\neach_item_on_its_own_category=項目ごとにカテゴリに分ける\neach_item_on_its_own_category_description=各項目は、そのファイルの種類に対応するカテゴリに分類されます。\nall_items_in_one_category=すべてを 1 つのカテゴリに保存\nall_items_in_one_category_description=すべてのファイルを選択したカテゴリに保存します。\nall_items_in_one_Location=すべてを 1 つの場所に保存\nall_items_in_one_Location_description=すべての項目を選択したディレクトリに保存します。\nunselected_all_items_in_specific_location_description=すべてのファイルを選択したカテゴリの保存先に保存します。\nno_category_selected=カテゴリが選択されていません\nno_categories_found=カテゴリが見つかりません\ndownload_location=保存先\nlocation=場所\nselect_queue=キューを選択\nwithout_queue=キューがありません\nuse_category=カテゴリを使用\ncant_write_to_this_folder=このフォルダーに書き込めません\nfile_name_already_exists=同じ名前のファイルが既にあります\ndownload_already_exists=同じダウンロードが既にあります\ninvalid_file_name=無効なファイル名\nshow_solutions=解決策を表示...\nchange_solution=解決策を変更\nselect_a_solution=解決策を選択\nselect_download_strategy_description=指定したリンクは既にダウンロード一覧にあります。行う操作を選択してください。\ndownload_strategy_add_a_numbered_file=番号付きファイルを追加\ndownload_strategy_add_a_numbered_file_description=ファイル名の末尾に連番を付けて追加します。\ndownload_strategy_override_existing_file=既存のファイルを上書き\ndownload_strategy_override_existing_file_description=既存のダウンロードを削除し、そのファイルに書き込みます。\ndownload_strategy_update_download_link=既存のダウンロードを更新\ndownload_strategy_update_download_link_description=既存のダウンロード リンクと資格情報を更新します。\ndownload_strategy_show_downloaded_file=既存の項目を表示\ndownload_strategy_show_downloaded_file_description=既存のダウンロード項目を表示します。再開や開く操作ができます。\nbatch_download_link_help=ワイルドカード(*)を使用したリンクを入力します。\ninvalid_url=無効な URL\nlist_is_too_large_maximum_n_items_allowed=リストが大きすぎます。最大 {{count}} 件までです\nenter_range=範囲を入力\nrange_from=開始\nrange_to=終了\nbatch_download_wildcard_length=ワイルドカードの長さ\nfirst_link=最初のリンク\nlast_link=最後のリンク\nopen_source_software_used_in_this_app=このアプリで使用されているオープン ソース ソフトウェア\nlinks=リンク\nwebsite=Web サイト\ndevelopers=開発者\nsource_code=ソース コード\nlicense=ライセンス\nno_license_found=ライセンスが見つかりません\norganization=組織\nadd_new_queue=新しいキューを追加\nqueue_name=キュー名\nqueues=キュー\nstop_queue=キューを停止\nstart_queue=キューを開始\nclear_queue_items=キューを空にする\nconfig=設定\nitems=項目\nmove_down=下へ移動\nmove_up=上へ移動\nremove_queue=キューを削除\nqueue_name_help=このキューの名前を指定します\nqueue_name_describe=キュー名\\: {{value}}\nqueue_max_concurrent_download=最大同時ダウンロード数\nqueue_max_concurrent_download_description=このキューで同時に実行できるダウンロードの最大数です。\nqueue_automatic_stop=自動停止\nqueue_automatic_stop_description=キューに項目がない場合、このキューを自動的に停止します。\nqueue_scheduler=スケジューラ\nqueue_enable_scheduler=スケジューラを有効にする\nqueue_active_days=稼働日\nqueue_active_days_description=スケジューラを実行する曜日を選択します。\nqueue_scheduler_enable_auto_start_time=自動開始時刻を有効にする\nqueue_scheduler_auto_start_time=自動開始時刻\nqueue_scheduler_enable_auto_stop_time=自動停止時刻を有効にする\nqueue_scheduler_auto_stop_time=自動停止時刻\nqueue_shutdown_on_completion=完了時にシステムをシャットダウン\nqueue_shutdown_on_completion_description=このキューが完了したとき、またはスケジュールの終了時刻に達したときにシステムを自動的にシャットダウンします。\nappearance=外観\ndownload_engine=ダウンロード エンジン\nbrowser_integration=ブラウザー統合\nsettings_download_max_retries_count=最大再試行回数\nsettings_download_max_retries_count_description=失敗したダウンロードを中止するまでに再試行する最大回数です。\nsettings_download_max_retries_count_describe_no_retries=失敗したダウンロードは再試行しません\nsettings_download_max_retries_count_describe_n_retries=失敗したダウンロードは {{count}} 回再試行します\nsettings_download_thread_count=スレッド数\nsettings_download_thread_count_description=各ダウンロード項目で使用する最大スレッド数です。\nsettings_download_thread_count_describe=ダウンロードは最大 {{count}} スレッドまで使用できます\nsettings_download_thread_count_with_large_value_describe=警告\\: スレッド数を高く設定すると、システム リソースの使用量が増え、性能が低下したり、サーバーとの接続に問題が発生したりする場合があります。システムやネットワークへの影響を理解している場合のみ、高い値を使用してください。\nsettings_use_server_last_modified_time=サーバーの最終更新日時を使用\nsettings_use_server_last_modified_time_description=ファイルをダウンロードするとき、ローカル ファイルの日時にサーバーの最終更新日時を使用します。\nsettings_append_extension_to_incomplete_downloads=未完了のダウンロードに拡張子を付ける\nsettings_append_extension_to_incomplete_downloads_description=完了のダウンロードに \".part\" 拡張子を付けます。未完了のダウンロードを識別しやすくし、誤って開いてしまうのを防ぎます。\nsettings_use_sparse_file_allocation=スパース ファイル割り当て\nsettings_use_sparse_file_allocation_description=不要な書き込みを減らしてファイルを効率的に作成します（特に SSD で効果があります）。これによりダウンロード開始が速くなり、ディスク使用量も抑えられます。ダウンロードの開始が遅い場合や、速度が不安定な場合は、このオプションを無効にしてください。一部のデバイスでは完全にサポートされない場合があります。\nsettings_ignore_ssl_certificates=SSL 証明書を無視\nsettings_ignore_ssl_certificates_description=SSL 証明書の検証を無効にします。必要な場合にのみ使用してください。接続がセキュリティ リスクにさらされる可能性があります。\nsettings_global_speed_limiter=グローバル速度制限\nsettings_global_speed_limiter_description=全体のダウンロード速度の上限(0 は無制限)\nsettings_show_average_speed=平均速度を表示\nsettings_show_average_speed_description=ダウンロード速度を平均または高精度で表示します。\nsettings_use_category_by_default=既定でカテゴリを使用\nsettings_use_category_by_default_description=ダウンロードを追加するとき、既定でカテゴリを使用します。\nsettings_default_download_folder=既定のダウンロード フォルダー\nsettings_default_download_folder_description=新しいダウンロードを追加するとき、既定でこの場所を使用します。\nsettings_default_download_folder_describe=\"{{folder}}\" を使用します\nsettings_use_proxy=プロキシを使用\nsettings_use_proxy_description=ファイルのダウンロードにプロキシを使用します。\nsettings_use_proxy_describe_no_proxy=プロキシは使用しません\nsettings_use_proxy_describe_system_proxy=システムのプロキシを使用します\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" を使用します\nsettings_use_proxy_describe_pac_proxy=PAC ファイル \"{{value}}\" を使用します\nsettings_track_deleted_files_on_disk=ディスク上の削除を追跡\nsettings_track_deleted_files_on_disk_description=ダウンロード ディレクトリからファイルが削除または移動された場合、一覧から自動的に削除します。\nsettings_delete_partial_file_on_download_cancellation=キャンセル時に部分ファイルを削除\nsettings_delete_partial_file_on_download_cancellation_description=ダウンロードをキャンセルしたとき、途中までダウンロードされたファイルをディスクから削除します。これによりダウンロード フォルダーをきれいに保ち、不要なディスク使用量を減らせます。ただし、次回開始するとダウンロードは最初からやり直されます。\nsettings_default_user_agent=既定の User-Agent\nsettings_default_user_agent_description=既定の User-Agent 文字列を指定し、要求がサーバーにどう識別されるかを決めます。特定デバイス向けのコンテンツにアクセスしたり、一部サイトの制限を回避したりするのに役立つ場合があります。\nsettings_download_size_unit=ダウンロード サイズの単位\nsettings_download_size_unit_description=ダウンロード サイズの表示に使用する単位\nsettings_download_speed_unit=ダウンロード速度の単位\nsettings_download_speed_unit_description=ダウンロード速度の表示に使用する単位\nsettings_theme=テーマ\nsettings_theme_description=アプリのテーマを選択します。\nsettings_default_dark_theme=既定のダーク テーマ\nsettings_default_dark_theme_description=アプリがシステム テーマに従い、ダーク モードが有効な場合に適用されます。\nsettings_default_light_theme=既定のライト テーマ\nsettings_default_light_theme_description=アプリがシステム テーマに従い、ライト モードが有効な場合に適用されます。\nsettings_font=フォント\nsettings_font_description=アプリのインターフェイスで使用するフォントを変更します。一部のフォントは正しく表示されない場合があります。\nsettings_ui_scale=UI スケール\nsettings_ui_scale_description=アプリのインターフェイス要素のサイズを調整します。\nsettings_language=言語\nsettings_compact_top_bar=コンパクトなトップ バー\nsettings_compact_top_bar_description=メイン ウィンドウの幅が十分にある場合、トップ バーをタイトル バーに統合します。\nsettings_use_native_menu_bar=ネイティブ メニュー バーを使用\nsettings_use_native_menu_bar_description=システム既定のメニュー バー スタイルを使用します。\nsettings_use_relative_date_time=相対的な日時を使用\nsettings_use_relative_date_time_description=アプリ内の日付を相対表示にします(例\\: \"2 日前\" など)。\nsettings_show_icon_labels=アイコンのラベルを表示\nsettings_show_icon_labels_description=可能な場合、アイコンの下にラベルを表示します(ホームのツールバー操作など)。\nsettings_use_system_tray=システム トレイを使用\nsettings_use_system_tray_description=アプリの実行中にシステム トレイ アイコンを表示します。\nsettings_start_on_boot=システム起動時に開始\nsettings_start_on_boot_description=ユーザー ログイン時にアプリを自動起動します。\nsettings_notification_sound=通知音\nsettings_notification_sound_description=新しい通知で音を再生します。\nsettings_browser_integration=ブラウザー統合\nsettings_browser_integration_description=ブラウザーからのダウンロードを受け付けます。\nsettings_browser_integration_server_port=サーバー ポート\nsettings_browser_integration_server_port_description=ブラウザー統合で使用するポートです。\nsettings_browser_integration_server_port_describe=アプリはポート {{port}} を監視します\nsettings_dynamic_part_creation=動的分割パーツ生成\nsettings_dynamic_part_creation_description=パーツが完了したら、他のパーツを分割して新しいパーツを作成し、ダウンロード速度を改善します。\nsettings_show_completion_dialog=ダウンロード完了ダイアログを表示\nsettings_show_completion_dialog_description=ダウンロードが完了時に \"ダウンロード完了\" ダイアログを自動的に表示します。\nsettings_show_download_progress_dialog=ダウンロード進行状況ダイアログを表示\nsettings_show_download_progress_dialog_description=ダウンロード開始時に \"ダウンロードの進行状況\" ダイアログを自動的に表示します。\nsettings_per_host_settings=ホスト別設定\nsettings_per_host_settings_descriptions=指定したホストに一致する新しいダウンロードに、これらの設定が自動的に適用されます。\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=速度制限\ndownload_item_settings_speed_limit_description=この項目のダウンロード速度を制限します。\ndownload_item_settings_show_download_completion_dialog=ダウンロード完了ダイアログを表示\ndownload_item_settings_show_download_completion_dialog_description=このダウンロードが完了したときに \"ダウンロード完了\" ダイアログを自動的に表示します。\ndownload_item_settings_shutdown_on_completion=完了時にシステムをシャットダウン\ndownload_item_settings_shutdown_on_completion_description=このダウンロードが完了したときにシステムを自動的にシャットダウンします。\ndownload_item_settings_thread_count=スレッド数\ndownload_item_settings_thread_count_description=この項目のダウンロードに使用するスレッド数(既定は 0)\ndownload_item_settings_thread_count_describe=このダウンロードに {{count}} スレッドを使用\ndownload_item_settings_username_description=リンクが保護されたリソースの場合はユーザー名を入力します。\ndownload_item_settings_password_description=リンクが保護されたリソースの場合はパスワードを入力します。\ndownload_item_settings_download_page=ダウンロード ページ\ndownload_item_settings_download_page_description=このダウンロードを開始した Web ページです。\ndownload_item_settings_file_checksum=ファイル チェックサム\ndownload_item_settings_file_checksum_description=ファイルが正しくダウンロードされたか確認するためのハッシュ文字列です。\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=この項目のカスタム User-Agent (空欄の場合は既定を使用)\nfile_checksum=ファイル チェックサム\nfile_checksum_page=チェックサム チェッカー\nfile_checksum_page_file_checksum_default_algorithm=既定のアルゴリズム\nfile_checksum_page_file_checksum_default_algorithm_help=チェックサムが指定されていない場合に使用する、チェックサム計算の既定アルゴリズムです。\nstart=開始\ncalculated_checksum=計算されたチェックサム\nsaved_checksum=保存されたチェックサム\nchecksum_algorithm=アルゴリズム\nfile_not_found=ファイルが見つかりません\ndownload_not_finished=ダウンロードが完了していません\ndone=完了\nwaiting=待機中\nmatches=一致\nnot_matches=不一致\ncopy_to_clipboard=クリップボードにコピー\nusername=ユーザー名\npassword=パスワード\naverage_speed=平均速度\nexact_speed=正確な速度\nunlimited=無制限\nuse_global_settings=グローバル設定を使用\ncant_run_browser_integration=ブラウザー統合を実行できません\ncant_open_file=ファイルを開けません\ncant_open_folder=フォルダーを開けません\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} 年\nrelative_time_long_months={{months}} か月\nrelative_time_long_days={{days}}日\nrelative_time_long_hours={{hours}} 時間\nrelative_time_long_minutes={{minutes}} 分\nrelative_time_long_seconds={{seconds}} 秒\nrelative_time_short_years={{years}} 年\nrelative_time_short_months={{months}} 月\nrelative_time_short_days={{days}} 日\nrelative_time_short_hours={{hours}} 時\nrelative_time_short_minutes={{minutes}} 分\nrelative_time_short_seconds={{seconds}} 秒\nrelative_time_left=残り {{time}}\nrelative_time_ago={{time}} 前\nauto=自動\nunspecified=未指定\ncustom=カスタム\nicon=アイコン\nauthor=作成者\nlink=リンク\nsize=サイズ\nstatus=状態\nparts_info_downloaded_size=ダウンロード済み\nparts_info_total_size=合計\nspeed=速度\ntime_left=残り時間\ndate_added=追加日時\ninfo=情報\ndownload_page_downloaded_size=ダウンロード済み\ndownload_page_download_completed=ダウンロード完了\nresume_support=サポートを再開\nyes=はい\nno=いいえ\nparts_info=パーツ情報\ndisconnected=切断されました\nreceiving_data=データ受信中\nconnecting=接続中\nwarning=警告\nunsupported_resume_warning=このダウンロードは再開に対応していません。後でダウンロード一覧から最初からやり直す必要がある場合があります。\nstop_anyway=このまま停止\ncustomize_columns=列をカスタマイズ\nreset=リセット\nmonday=月曜日\ntuesday=火曜日\nwednesday=水曜日\nthursday=木曜日\nfriday=金曜日\nsaturday=土曜日\nsunday=日曜日\nproxy_open_system_proxy_settings=システムのプロキシ設定を開く\nproxy_type=プロキシの種類\nproxy_do_not_use_proxy_for=次の URL ではプロキシを使用しない\nproxy_do_not_use_proxy_for_description=プロキシを使用しない URL の一覧です。\\nワイルドカード(*)を使用できます。\\n例\\: 192.168.1.* example.com (スペース区切り)\nproxy_change_title=プロキシの変更\nchange_proxy=プロキシの変更\nproxy_no=プロキシなし\nproxy_system=システム プロキシ\nproxy_manual=手動プロキシ\nproxy_pac=プロキシ自動構成\nproxy_pac_url=プロキシ自動構成URL\naddress=アドレス\nport=ポート\naddress_and_port=アドレスとポート\nuse_authentication=認証を使用\nwarning_you_may_have_to_restart_the_download_later=後でダウンロードを最初からやり直す必要がある場合があります\\!\nedit_download_title=ダウンロードを編集\nedit_download_update_from_download_page=ダウンロード ページから更新\nedit_download_update_from_download_page_description=このウィンドウを開いている間にダウンロード ページへ移動し、ダウンロード ボタンをクリックできます。アプリが新しい資格情報を取得して更新するため、保存できます。\nedit_download_saved_download_item_size_not_match=保存されたダウンロード項目のサイズは {{currentSize}} ですが、新しいサイズ {{newSize}} と一致しません。\ntranslators_page_thanks=このプロジェクトの翻訳に協力してくれた皆さんに感謝します ❤️\ntranslators=翻訳者\nlanguage=言語\ntranslators_contribute_title=翻訳の改善に協力する\ntranslators_contribute_description=このプロジェクトの改善に協力しませんか? 言語が一覧にない場合や調整が必要な場合は、翻訳を投稿してより良くできます。\ncontribute=翻訳に貢献する\nmeet_the_translators=翻訳者の紹介\nlocalized_by_translators=翻訳\\: 翻訳コミュニティ\nconfirm_exit=終了の確認\nconfirm_exit_description=AB Download Manager を終了しますか?\\n実行中のダウンロードやキューは停止します。\nupdate=アップデート\nupdate_updater=アップデーター\nupdate_available=更新があります\nupdate_error=更新エラー\nupdate_available_suggest_to_to_update=最新バージョンに更新して、新機能、改善、性能向上を利用できます。\nupdate_release_notes=リリース ノート\nupdate_check_for_update=更新を確認\nupdate_checking_for_update=更新を確認中\nupdate_no_update=最新バージョンを使用しています\nupdate_check_error=更新の確認中にエラーが発生しました\nupdate_app_updated_to_version_n=アプリをバージョン {{version}} に更新しました\ncreate_desktop_entry=デスクトップ エントリを作成\nshutdown_alert=シャットダウンの警告\nsystem_shutdown_soon=まもなくシステムがシャットダウンします\\!\nsystem_shutdown_failed=システムのシャットダウンに失敗しました\\!\nsystem_shutdown_soon_description=まもなくシステムがシャットダウンします。まだコンピューターを使用している場合は、作業を保存するか、シャットダウンをキャンセルしてください。\nsystem_shutdown_reason_queue_completed=キュー内のすべてのダウンロードが完了しました。\nsystem_shutdown_reason_queue_end_time_reached=ダウンロード キューのスケジュール終了時刻に達しました。\nsystem_shutdown_download_finished=ダウンロードが完了しました。\nshutdown_now=今すぐシャットダウン\nsettings_per_host_settings_new_host=<新しいホスト>\nsettings_per_host_settings_not_selected=最初に新しい項目を作成するか、既存の項目を選択してください。\nsettings_per_host_settings_host=ホスト\nsettings_per_host_settings_host_description=このホスト名に一致するダウンロードに、これらの設定が適用されます。ワイルドカード(*)を使用できます(例\\: example.com、*.example.com。どちらか 1 つのみ使用)。\nsettings_browser_in_launcher=ランチャーにブラウザー アイコンを表示\nsettings_browser_in_launcher_description=ランチャー（アプリ一覧）に表示されるブラウザー アイコンを表示または非表示にします。\nsort_by=並べ替え\nwelcome=ようこそ\nnew_folder=新しいフォルダー\nskip=スキップ\nlets_go=はじめる\nnext=次へ\nselect_all=すべて選択\nselect_inside=内部を選択\nselect_invert=選択を反転\nopen_settings=設定を開く\nback=戻る\nservice_is_running=サービスは実行中です\ninitial_setup_description=初期設定を行いましょう\ninitial_setup_notice=これらの設定は後からいつでも変更できます\npermission_granted=権限が許可されました\npermission_not_granted=権限が許可されていません\npermissions=権限\ngive_permission=権限を許可\ngive_storage_permission=ストレージへのアクセスを許可\nstorage_roots=Storage Roots\npermissions_initial_title=初期設定を行いましょう\npermissions_initial_description=アプリを正しく動作させるには、いくつかの権限が必要です。次の画面では、それぞれの権限の用途を確認し、許可するかスキップするかを選択できます。\npermissions_done_title=設定が完了しました\npermissions_done_description=すべての準備が整いました。必要な権限はすべて許可され、アプリを利用できます。\npermissions_manage_storage_title=ストレージ アクセスの管理\npermissions_manage_storage_reason=この権限を許可すると、ダウンロード フォルダーの変更、重複ダウンロードのより正確な検出、追加機能の利用が可能になります。必須ではありませんが、快適に使うために推奨されます。\npermission_read_write_external_storage_title=ストレージの読み取りと書き込み\npermission_read_write_external_storage_reason=この権限により、ダウンロードしたファイルの保存と管理、保存先の変更、重複ダウンロード検出の精度向上が可能になります。\npermissions_post_notification_title=通知の送信\npermissions_post_notification_reason=ダウンロードを管理するため、アプリはバックグラウンドで動作する必要があります。通知は状況をお知らせし、バックグラウンドでの動作を可能にします。\npermissions_ignore_battery_optimization_title=バッテリー最適化を無視\npermissions_ignore_battery_optimization_reason=一部の端末では、バッテリーを節約するためにバックグラウンドでの動作が厳しく制限され、アプリを開いていないとダウンロードが一時停止または停止することがあります。必要に応じて、このアプリをバッテリー最適化の対象外にすると、ダウンロードを中断せずに続行できます。\nopen_in_browser=ブラウザーで開く\nbrowser=ブラウザー\nbrowser_new_tab=新しいタブ\nbrowser_close_tab=タブを閉じる\nbrowser_open_in_new_tab=新しいタブで開く\nbrowser_open_in_new_background_tab=新しいバックグラウンド タブで開く\nbrowser_no_tab_open=開いているタブはありません\nbrowser_tabs=タブ\nbrowser_paste_and_go=貼り付けて移動\nbrowser_bookmarks=ブックマーク\nbrowser_add_bookmark=ブックマークを追加\nbrowser_edit_bookmark=ブックマークを編集\nbrowser_add_to_bookmarks=ブックマークに追加\nbrowser_remove_from_bookmarks=ブックマークから削除\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ka_GE.properties",
    "content": "app_title=AB ჩამოტვირთვების მენეჯერი\nconfirm_auto_categorize_downloads_title=ჩამოტვირთვების ავტომატური კატეგორიზება\nconfirm_auto_categorize_downloads_description=ნებისმიერი უკატეგორიო ნივთი ავტომატურად იქნება დამატებული მასთან დაკავშირებულ კადეგორიაში.\nconfirm_reset_to_default_categories_title=ქარხნული კატეგორიების დაბრუნება\nconfirm_reset_to_default_categories_description=ეს მოაშორებს ყველა კატეგორიას და დააბრუნებს ქარხნულ კატეგორიებს\\!\nconfirm_delete_download_items_title=წაშლის დადასტურება\nconfirm_delete_download_items_description=დარწმუნებული ხარ რომ გინდა წაშალო {{count}} ნივთი?\nconfirm_delete_download_unfinished_items_description=დარწმუნებული ხარ რომ გინდა წაშალო {{count}} დაუსრულებელი ჩამოტვირთვა?\nconfirm_delete_download_finished_and_unfinished_items_description=დარწმუნებული ხარ რომ გინდა წაშალო {{finishedCount}} დასრულებული და {{unfinishedCount}} დაუსრულებელი ჩამოტვირთვა?\nalso_delete_file_from_disk=ასევე წაშალე ფაილი დისკიდან\nconfirm_delete_category_item_title=ვშლი {{name}} კატეგორიას\nconfirm_delete_category_item_description=დარწმუნებული ხარ რომ გინდა წაშალო \"{{value}}\" კატეგორია?\nyour_download_will_not_be_deleted=შენი ჩამოტვირთვები არ წაიშლება\ndrag_the_file_to_another_app=Drag the file to another app\ndrop_link_or_file_here=ჩააგდე ლინკი ან ფაილი აქ.\nnothing_will_be_imported=არაფერი იქნება იმპორტირებული\nn_links_will_be_imported={{count}} ლინკი იქნება იმპორტირებული\nn_items_selected={{count}} ნივთი არჩეული\nwindow_close=Close\nwindow_minimize=Minimize\nwindow_maximize=Maximize\nwindow_restore=Restore\ndelete=წაშლა\nremove=წაშლა\ncancel=გაუქმება\nclose=დახურვა\nmenu=Menu\nmore_options=More Options\nok=კარგი\nadd=დამატება\npaste=Paste\nchange=შეცვლა\nedit=შეცვლა\nchange_anyway=მაინც შეცვლა\ndownload=ჩამოტვირთვა\nrefresh=განახლება\nsettings=პარამეტრები\non_completion=On Completion\nunknown=უცნობი\nunknown_error=უცნობი შეცდომა\ndownload_item_not_found=ჩამოტვირთვის ნივთი არაა ნაპოვნი\nname=სახელი\ndownload_link=ჩამოტვირთე ლინკი\nnot_finished=დაუსრულებელი\nall=ყველა\nfinished=დასრულებული\nUnfinished=დაუსრულებელი\ncanceled=გაუქმებული\nerror=შეცდომა\npaused=დაპაუზებული\ndownloading=იტვირთება\nadded=დამატებული\nidle=მოლოდინში\npreparing_file=მზადდება ფაილი\ncreating_file=იქმნება ფაილი\nresuming=ვაგრძელებ\nretrying=Retrying\nlist_is_empty=სია ცარიელია\\!\nsearch_in_the_list=მოძებნა სიაში\nsearch=ძიება\nclear=გასუფთავება\ngeneral=ზოგადი\nenabled=ჩართული\ndisabled=გამორთული\ndefault=Default\nfile=ფაილი\ntasks=დავალებები\ntools=ხელსაწყოები\nhelp=დახმარება\nsystem=სისტემა\nall_missing_files=ყველა დაკარგული ფაილი\nall_finished=ყველა დასრულებული\nall_unfinished=ყველა დაუსრულებელი\nentire_list=მთლიანი სია\ndownload_browser_integration=ჩამოტვირთვა ბრაუზერის ინტეგრაციის\nexit=გასვლა\nshow_downloads=ჩამოტვირთვების ჩვენება\nnew_download=ახალი ჩამოტვირთვა\nstop_all=ყველას შეჩერება\nimport_from_clipboard=იმპორტირება დაკოპირებულიდან\nbatch_download=ჯგუფური ჩამოტვირთვა\nopen=გახსნა\nshare=Share\nopen_file=ფაილის გახსნა\nopen_folder=საქაღალდის გახსნა\nresume=გაგრძელება\npause=შეჩერება\nrestart_download=ჩამოტვირთვის თავიდან დაწყება\ncopy=Copy\ncopy_link=ბმულის კოპირება\ncopy_as_curl=Copy as cURL\nshow_properties=თვისებების ჩვენება\nmove_to_queue=რიგში გადაყვანა\nmove_to_this_queue=Move to this Queue\nmove_to_category=კატეგორიაში გადაყვანა\nmove_to_this_category=Move to this category\ncategories=Categories\nadd_category=კატეგორიის დამატება\nedit_category=კატეგორიის რედაქტირება\ndelete_category=კატეგორიის წაშლა\ncategory_name=კატეგორიის სახელი\ncategory_download_location=კატეგორიის ჩამოტვირთვის ადგილი\ncategory_download_location_description=როდესაც ეს კატეგორია არჩეულია \"ჩამოტვირთვის დამატებისას\" მაშინ მოიხმარება ეს საქაღალდე როგორც \"ჩამოტვირთვის ადგილი\"\ncategory_file_types=კატეგორიის ფაილის სახეობები\ncategory_file_types_description=ავტომატურად ჩამატება ამ ფაილის სახეობების ამ კატეგორიაში (ახალი ჩამოტვირთვის დამატებისას)\\nგანასხვავე ფაილის გაფართოებები სფეისით (ext1 ext2 ...)\ncategory_url_patterns=ლინკების ნიმუშები\ncategory_url_patterns_description=ავტომატურად ჩამატება გადმოწერების ამ ლინკებიდან ამ კატეგორიაში (ახალი ჩამოტვირთვის დამატებისას)\\nგანასხვავეთ ლინკები სფეისით, ასევე შეგიძლიათ იხმაროთ * როგორც ზოგადი\nauto_categorize_downloads=ჩამოტვირთვების ავტომატური კატეგორიზება\nrestore_defaults=ნაგულისხმევის აღდგენა\nabout=შესახებ\nversion_n=ვერსია {{value}}\ndeveloped_with_love_for_you=შექმნილია ❤️-ით შენთვის\ndonate=Donate\nvisit_the_project_website=პროექტის საიტის ნახვა\nthis_is_a_free_and_open_source_software=ეს არის უფასო და ღია კოდის წყაროს მქონე პროგრამა\nview_the_source_code=ნახვა კოდის წყაროს\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=\nview_the_open_source_licenses=ნახვა ღია კოდის წყაროს ლიცენზიების\nsupport_and_community=დახმარება და საზოგადოება\ntelegram=ტელეგრამი\nchannel=არხი\ngroup=ჯგუფი\nadd_download=ჩამოტვირთვის დამატება\nadd_multi_download_page_header=აირჩიე ნივთები რისი აღებაც გსურს ჩამოსატვირთად\nsave_to=შენახვა\nwhere_should_each_item_saved=სად უნდა იყოს შენახული თითო ნივთი?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=აქ არის მრავალი ნივთი\\! გთხოვთ აირჩიოთ გზა რითიც გინდათ რომ შეინახოთ ისინი\neach_item_on_its_own_category=თითო ნივთი თავის კატეგორიაში\neach_item_on_its_own_category_description=თითო ნივთი იქნება ჩასმული იმ კატეგორიაში რომელსაც ეგ ფაილის სახეობა აქვს\nall_items_in_one_category=ყველა ნივთი ერთ კატეგორიაში\nall_items_in_one_category_description=ყველა ფაილი შეინახება არჩეული კატეგორიის ადგილას\nall_items_in_one_Location=ყველა ნივთი ერთ ადგილში\nall_items_in_one_Location_description=ყველა ნივთი შეინახება არჩეულ საქაღალდეში\nunselected_all_items_in_specific_location_description=All files will be saved in the selected category location\nno_category_selected=კატეგორია არჩეული არარის\nno_categories_found=No Categories Found\ndownload_location=ჩამოტვირთვის ადგილი\nlocation=ადგილი\nselect_queue=რიგის არჩევა\nwithout_queue=რიგის გარეშე\nuse_category=მოიხმარე კატეგორია\ncant_write_to_this_folder=აღნიშნულ საქაღალდეში ჩაწერა ვერ ხერხდება\nfile_name_already_exists=ფაილის სახელი უკვე არსებობს\ndownload_already_exists=Download already exists\ninvalid_file_name=მიუღებელი ფაილის სახელი\nshow_solutions=გამოსავლების ჩვენება...\nchange_solution=გამოსავლის შეცვლა\nselect_a_solution=გამოსავლის არჩევა\nselect_download_strategy_description=ლინკი რომელიც მომაწოდე უკვე ჩამოტვირთვების სიაშია გთხოვთ მიუთითეთ რისი გაკეთება გსურთ\ndownload_strategy_add_a_numbered_file=ნომერიანი ფაილის დამატება\ndownload_strategy_add_a_numbered_file_description=ინდექსის დამატება ჩამოტვირთვის ფაილის სახელის ბოლოში\ndownload_strategy_override_existing_file=არსებულ ფაილზე გადაწერა\ndownload_strategy_override_existing_file_description=არსებული ჩამოტვირთვის მოშორება და მაგ ფაილში ჩაწერა\ndownload_strategy_update_download_link=Update existing download\ndownload_strategy_update_download_link_description=Update the existing download link and its credentials\ndownload_strategy_show_downloaded_file=ჩამოტვირთვული ფაილის ჩვენება\ndownload_strategy_show_downloaded_file_description=არსებული ჩამოტვირთვის ჩვენება, რადგან შეცძლო გაგრძელების დაჭერაზე ან გახსნა ის\nbatch_download_link_help=შეიყვანეთ ლინკი რომელიც შეიცავს ზოგადებს (მოიხმარეთ *)\ninvalid_url=მიუღებელი ლინკი\nlist_is_too_large_maximum_n_items_allowed=სია ზედმეტად დიდია\\! დაშვებულია არაუმეტეს {{count}} ნივთი\nenter_range=შეიყვანეთ დიაპაზონი\nrange_from=დან\nrange_to=მდე\nbatch_download_wildcard_length=ზოგადის სიგრძე\nfirst_link=პირველი ლინკი\nlast_link=ბოლო ლინკი\nopen_source_software_used_in_this_app=ღია კოდის წყაროს მქონე პროგრამები ნახმარი ამ აპში\nlinks=ლინკები\nwebsite=ვებსაიტი\ndevelopers=დეველოპერები\nsource_code=კოდის წყარო\nlicense=ლიცენზია\nno_license_found=არანაირი ლიცენზია არაა ნაპოვნი\norganization=ორგანიზაცია\nadd_new_queue=ახალი რიგის დამატება\nqueue_name=რიგის სახელი\nqueues=რიგები\nstop_queue=რიგის შეჩერება\nstart_queue=რიგის დაწყება\nclear_queue_items=Empty Queue\nconfig=კონფიგურაცია\nitems=ნივთები\nmove_down=ქვემოთ ჩამოტანა\nmove_up=ზემოთ ატანა\nremove_queue=რიგის წაშლა\nqueue_name_help=მიუთითეთ სახელი ამ რიგისთვის\nqueue_name_describe=რიგის სახელია {{value}}\nqueue_max_concurrent_download=მაქსიმუმი ერთდროული ჩამოტვირთვა\nqueue_max_concurrent_download_description=მაქსიმუმი ერთდროული ჩამოტვირთვა ამ რიგისთვის\nqueue_automatic_stop=ავტომატური შეჩერება\nqueue_automatic_stop_description=რიგის ავტომატური შეჩერება როდესაც არაფერია შიგნით\nqueue_scheduler=დამგეგმი\nqueue_enable_scheduler=დამგეგმის ჩართვა\nqueue_active_days=აქტიური დღეები\nqueue_active_days_description=რომელ დღეებში მუშაობს დამგეგმი?\nqueue_scheduler_enable_auto_start_time=Enable Auto Start Time\nqueue_scheduler_auto_start_time=ავტომატური დაწყების დრო\nqueue_scheduler_enable_auto_stop_time=ავტომატური დაწყების დროის ჩართვა\nqueue_scheduler_auto_stop_time=ავტომატური შეჩერების დრო\nqueue_shutdown_on_completion=Shutdown System On Completion\nqueue_shutdown_on_completion_description=Automatically shutdown the system when this queue is completed. or when the scheduled end time is reached.\nappearance=შეხედულობა\ndownload_engine=ჩამოტვირთვების ძრავა\nbrowser_integration=ბრაუზერთან ინტეგრაცია\nsettings_download_max_retries_count=Maximum Download Retries\nsettings_download_max_retries_count_description=The maximum number of times the app will retry a failed download before giving up\nsettings_download_max_retries_count_describe_no_retries=Failed downloads won't be retried\nsettings_download_max_retries_count_describe_n_retries=Failed downloads will be retried {{count}} time(s)\nsettings_download_thread_count=ძაფების რაოდენობა\nsettings_download_thread_count_description=მაქსიმუმი ჩამოტვირთვის ძაფი თითო ჩამოტვირთვის ნივთისთვის\nsettings_download_thread_count_describe=ჩამოტვირთვას შეიძლება ჰქონდეს {{count}} ძაფები\nsettings_download_thread_count_with_large_value_describe=Warning\\: Setting a high thread count may increase system resource usage, reduce performance, or cause connection issues with servers. Use higher values only if you understand the potential impact on your system and network.\nsettings_use_server_last_modified_time=სერვერის \"ბოლოს მოდიფიცირებული\" დროის მოხმარება\nsettings_use_server_last_modified_time_description=ფაილის ჩამოტვირთვისას, მოხმარება სერვერის ბოლოს მოდიფიცირებული დროის ადიგლობრივი ფაილისთვის\nsettings_append_extension_to_incomplete_downloads=Append Extension To Incomplete Downloads\nsettings_append_extension_to_incomplete_downloads_description=Append \".part\" extension to incomplete downloads. This helps to identify unfinished downloads and prevents accidental opening of incomplete files.\nsettings_use_sparse_file_allocation=Sparse File Allocation\nsettings_use_sparse_file_allocation_description=ფაილების უფრო დამზოგველად შექმნა, განსაკუთრებით SSD-ებზე, არა აუცილებელი მონაცემთა ჩაწერის შემცირებით. ამას შეუძლია ჩამოტვირთვის დაწყების აჩქარება და დისკის მოხმარების დაკლება, გაითვალისწინეთ ამ პარამეტრის გამორთვა, რადგან შეიძლება ზოგ მოწყობილობაზე მთლიანად არ იყოს მხარდაჭერილი.\nsettings_ignore_ssl_certificates=Ignore SSL Certificates\nsettings_ignore_ssl_certificates_description=Disables SSL certificate verification. Use only if necessary, as it may expose your connection to security risks.\nsettings_global_speed_limiter=გლობალური სიჩქარის ლიმიტი\nsettings_global_speed_limiter_description=გლობალური ჩამოტვირთვის სიჩქარის ლიმიტი (0 ნიშვანს ულიმიტოს)\nsettings_show_average_speed=საშუალო სიჩქარის ჩვენება\nsettings_show_average_speed_description=ჩამოტვირთვის სიჩქარე საშუალო ან ზუსტი\nsettings_use_category_by_default=Use Category By Default\nsettings_use_category_by_default_description=Use category by default when adding a download.\nsettings_default_download_folder=ნაგულისხმევი ჩამოტვირთვების საქაღალდე\nsettings_default_download_folder_description=როდესაც დაამატებ ახალ ჩამოტვირთვას ეს საქაღალდეა მოხმარებული ნაგულისხმევად\nsettings_default_download_folder_describe=მოიხმარება {{folder}}\nsettings_use_proxy=პროქსის მოხმარება\nsettings_use_proxy_description=პროქსის მოხმარება ფაილების გადმოწერისთვის\nsettings_use_proxy_describe_no_proxy=არ მოიხმარება პროქსი\nsettings_use_proxy_describe_system_proxy=მოიხმარება სისტემის პროქსი\nsettings_use_proxy_describe_manual_proxy=მოიხმარება {{value}}\nsettings_use_proxy_describe_pac_proxy=PAC file \"{{value}}\" will be used\nsettings_track_deleted_files_on_disk=Track Deleted Files On Disk\nsettings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.\nsettings_delete_partial_file_on_download_cancellation=Delete Partial File On Download Cancellation\nsettings_delete_partial_file_on_download_cancellation_description=When a download is canceled, the partially downloaded file will be deleted from the disk. This helps keep your download folder clean and reduces unnecessary disk space usage. However, the download will restart from the beginning the next time you start it.\nsettings_default_user_agent=Default User-Agent\nsettings_default_user_agent_description=Specify the Default-User Agent string to define how requests identify to servers. This can help in accessing content optimized for particular devices or in circumventing download limitations imposed by certain websites.\nsettings_download_size_unit=Download Size Unit\nsettings_download_size_unit_description=Unit used to display the download size\nsettings_download_speed_unit=Download Speed Unit\nsettings_download_speed_unit_description=Unit used to display the download speed\nsettings_theme=თემა\nsettings_theme_description=აირჩიეთ თემა აპისთვის\nsettings_default_dark_theme=Default Dark Theme\nsettings_default_dark_theme_description=Applies when the app follows the system theme and dark mode is active\nsettings_default_light_theme=Default Light Theme\nsettings_default_light_theme_description=Applies when the app follows the system theme and light mode is active\nsettings_font=Font\nsettings_font_description=Change the font used in the app interface, Some fonts might not display correctly in the app.\nsettings_ui_scale=ინტერფეისის ზომა\nsettings_ui_scale_description=აპის ინტერფეისის ელემენტების ზომის შეცვლა\nsettings_language=ენა\nsettings_compact_top_bar=კომპაქტური ზედა ზოლი\nsettings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width\nsettings_use_native_menu_bar=Use Native Menu Bar\nsettings_use_native_menu_bar_description=Use the system's default menu bar style\nsettings_use_relative_date_time=Use relative date/time\nsettings_use_relative_date_time_description=Use relative date/time format for dates in the app (e.g., \"2 days ago\" instead of the exact date/time)\nsettings_show_icon_labels=Show Icon Labels\nsettings_show_icon_labels_description=Show labels under icons when possible ( like home toolbar actions )\nsettings_use_system_tray=Use System Tray\nsettings_use_system_tray_description=Show system tray icon when the app is running\nsettings_start_on_boot=კომპიუტერთან ერთად ჩართვა\nsettings_start_on_boot_description=ავტომატურად ჩართვა როდესაც კომპიუტერი ჩაირთვება\nsettings_notification_sound=შეტყობინების ხმა\nsettings_notification_sound_description=Play sound on new notification\nsettings_browser_integration=ბრაუზერთან ინტეგრაცია\nsettings_browser_integration_description=ჩამოტვირთვების წამოყება ბრაუზერებიდან\nsettings_browser_integration_server_port=სერვერის პორტი\nsettings_browser_integration_server_port_description=პორტი ბრაუზერთან ინტეგრაციისთვის\nsettings_browser_integration_server_port_describe=აპი მოუსმენს პორტ {{port}}-ს\nsettings_dynamic_part_creation=Dynamic Part Creation\nsettings_dynamic_part_creation_description=როდესაც ნაწილი დამთავრებულია შექმენი ახალი ნაწილი სხვა ნაწილების გაყოფით რადგან სიჩქარემ მოიმატოს\nsettings_show_completion_dialog=Show Download Completion Dialog\nsettings_show_completion_dialog_description=Automatically show \"Download Complete\" dialog when a download finished.\nsettings_show_download_progress_dialog=ჩამოტვირთვის პროგრესის დიალოგის ჩვენება\nsettings_show_download_progress_dialog_description=ავტომატური ჩვენება \"ჩამოტვირთვის პროგრესის\" დიალოგის როდესაც ეს ჩამოტვირთვა დაიწყება.\nsettings_per_host_settings=Per Host Settings\nsettings_per_host_settings_descriptions=These settings will be automatically applied to any new download that matches the specified host.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=სიჩქარის ლიმიტი\ndownload_item_settings_speed_limit_description=ჩამოტვირთვის სიჩქარის ლიმიტი ამ ნივთისთვის\ndownload_item_settings_show_download_completion_dialog=ჩამოტვირთვა დასრულებულია დიალოგის ჩვენება\ndownload_item_settings_show_download_completion_dialog_description=ავტომატური ჩვენება \"ჩამოტვირთვა დასრლებულია\" დიალოგის როდესაც ეს ჩამოტვირთვა მორჩება.\ndownload_item_settings_shutdown_on_completion=Shutdown System On Completion\ndownload_item_settings_shutdown_on_completion_description=Automatically shutdown the system when this download is finished.\ndownload_item_settings_thread_count=ძაფების რაოდენობა\ndownload_item_settings_thread_count_description=რამდენი ძაფი არის მოხმარებული ამ ნივთის გადმოსაწერად (0 რომ დარჩეს ნაგულისხმევი)\ndownload_item_settings_thread_count_describe={{count}} ძაფი ამ ჩამოტვირთვისთვის\ndownload_item_settings_username_description=მიუთითეთ მომხმარებელი თუ ლინკი არის დაცული რესურსი\ndownload_item_settings_password_description=მიუთითეთ პაროლი თუ ლინკი არის დაცული რესურსი\ndownload_item_settings_download_page=ჩამოტვირთვის გვერდი\ndownload_item_settings_download_page_description=ვებგვერდი სადაც ჩამოტვირთვა წამოიწყო\ndownload_item_settings_file_checksum=File Checksum\ndownload_item_settings_file_checksum_description=A hash string which can be used to check if file is downloaded correctly\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Custom User-Agent for this item (leave empty to use the default)\nfile_checksum=File Checksum\nfile_checksum_page=File Checksum Checker\nfile_checksum_page_file_checksum_default_algorithm=Default Algorithm\nfile_checksum_page_file_checksum_default_algorithm_help=The default algorithm used to calculate file checksums when they are not provided.\nstart=Start\ncalculated_checksum=Calculated Checksum\nsaved_checksum=Saved Checksum\nchecksum_algorithm=Algorithm\nfile_not_found=File not found\ndownload_not_finished=Download not finished\ndone=Done\nwaiting=Waiting\nmatches=Matches\nnot_matches=Not Matches\ncopy_to_clipboard=Copy To Clipboard\nusername=მომხმარებელი\npassword=პაროლი\naverage_speed=საშუალო სიჩქარე\nexact_speed=ზუსტი სიჩქარე\nunlimited=ულიმიტო\nuse_global_settings=გლობალური პარამეტრების მოხმარება\ncant_run_browser_integration=ვერ ვუშვებ ბრაუზერთან ინტეგრაციას\ncant_open_file=ფაილის გახსნა ვერ ხერხდება\ncant_open_folder=საქაღალდის გახსნა ვერ ხერხდება\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} წელი\nrelative_time_long_months={{months}} თვე\nrelative_time_long_days={{days}} დღე\nrelative_time_long_hours={{hours}} საათი\nrelative_time_long_minutes={{minutes}} წუთი\nrelative_time_long_seconds={{seconds}} წამი\nrelative_time_short_years={{years}} წ\nrelative_time_short_months={{months}} თ\nrelative_time_short_days={{days}} დ\nrelative_time_short_hours={{hours}} სთ\nrelative_time_short_minutes={{minutes}} წთ\nrelative_time_short_seconds={{seconds}} წმ\nrelative_time_left={{time}} დარჩენილი\nrelative_time_ago={{time}} წინ\nauto=ავტომატური\nunspecified=დაუზუსტებელი\ncustom=ხელით შეყვანა\nicon=ხატულა\nauthor=ავტორი\nlink=ლინკი\nsize=ზომა\nstatus=სტატუსი\nparts_info_downloaded_size=ჩამოტვირთული\nparts_info_total_size=ჯამი\nspeed=სიჩქარე\ntime_left=დარჩენილი დრო\ndate_added=დამატების თარიღი\ninfo=ინფორმაცია\ndownload_page_downloaded_size=ჩამოტვირთული\ndownload_page_download_completed=ჩამოტვირთვა დამთავრებულია\nresume_support=გაგრძელების მხარდაჭერა\nyes=დიახ\nno=არა\nparts_info=ნაწილების ინფორმაცია\ndisconnected=კავშირი გამწყდარია\nreceiving_data=მონაცემების მიღება\nconnecting=კავშირდება\nwarning=გაფრთხილება\nunsupported_resume_warning=ამ ჩამოტვირთვას არ აქვს გაგრძელების მხარდაჭერა\\! შეიძლება თავიდან დაწყება დაგჭირდეთ მოგვიანებით ჩამოტვირთვების სიიდან\nstop_anyway=მაინც შეჩერება\ncustomize_columns=Customize Columns\nreset=განულება\nmonday=ორშაბათი\ntuesday=სამშაბათი\nwednesday=ოთხშაბათი\nthursday=ხუთშაბათი\nfriday=პარასკევი\nsaturday=შაბათი\nsunday=კვირა\nproxy_open_system_proxy_settings=სისტემის პროქსის პარამეტრების გახსნა\nproxy_type=პროქსის ტიპი\nproxy_do_not_use_proxy_for=არ იხმარო პროქსი ამისთვის\nproxy_do_not_use_proxy_for_description=ლინკების სია რისთვისაც პროქსი არ მოიხმარება, შეგიძლიათ ზოგადად იხმაროთ *\\nმაგალითად 192.168.1.* example.com (სფეისით გაცალკევებული)\nproxy_change_title=პროქსის შეცვლა\nchange_proxy=პროქსის შეცვლა\nproxy_no=არანაირი პროქსი\nproxy_system=სისტემის პროქსი\nproxy_manual=პროქსის შეყვანა\nproxy_pac=Proxy Auto Configuration\nproxy_pac_url=Proxy Auto Configuration URL\naddress=მისამართი\nport=პორტი\naddress_and_port=მისამართი და პორტი\nuse_authentication=ავთენტიფიკაციის მოხმარება\nwarning_you_may_have_to_restart_the_download_later=შეიძლება მოგვიანებით ჩამოტვირთვის თავიდან დაწყება დაგჭირდეს\\!\nedit_download_title=ჩამოტვირთვის შეცვლა\nedit_download_update_from_download_page=განახლება ჩამოტვირთვის გვერდიდან\nedit_download_update_from_download_page_description=როდესაც ეს ფანჯარაა გახსნილი, შეგიძლიათ ჩამოტვირთვის გვერძე გადახვიდეთ და ჩამოტვირთვის ღილაკს დააჭიროთ. აპი დაიჭერს და განაახლებს ჩამოტვირთვის მონაცემებს რადგან შეგეძლოთ მათი შენახვა.\nedit_download_saved_download_item_size_not_match=შენახულ ჩამოტვირთვას აქვს {{currentSize}} ზომა, რომელიც არ ემთხვევა ახალ {{newSize}} ზომას.\ntranslators_page_thanks=მადლობით მათ ვინც დაგვეხმარა ამ პროექტის თარგმნაში ❤️\ntranslators=მთარგმნელები\nlanguage=ენა\ntranslators_contribute_title=თარგმნების გაუმჯობესება\ntranslators_contribute_description=გსურთ გააუმჯობესოთ ეს პროექტი? თუ თქვენი ენა არარის ჩამონათვალში ან ჩასწორებები სჭირდება, შეგიძლიათ თვენც გადათარგმნოთ და გააუმჯობესოთ\\!\ncontribute=მონაწილეობა\nmeet_the_translators=გაიცანი მთარგმნელები\nlocalized_by_translators=ლოკალიზირებული მთარგმნელების მიერ\nconfirm_exit=გამოსვლის დადასტურება\nconfirm_exit_description=დარწმუნებული ხართ რომ გინდათ გამოხვიდეთ AB ჩამოტვირთვების მენეჯერიდან? აქტიური ჩამოტვირთვები/რიგები შეჩერებული იქნება\\!\nupdate=განახლება\nupdate_updater=გამნახლებელი\nupdate_available=განახლება ხელმისაწვდომია\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=შეგიძლიათ უახლეს ვერსიაზე განახლება რათა ისიამოვნოთ ახალი ფუნქციებით, გაუმჯობესებებით და აჩქარებებით.\nupdate_release_notes=გამოშვების შენიშვნები\nupdate_check_for_update=განახლების შემოწმება\nupdate_checking_for_update=მოწმდება განახლება\nupdate_no_update=თქვენ ხმარობთ უახლეს ვერსიას\nupdate_check_error=მოხდა შეცდომა განახლების შემოწმებისას\nupdate_app_updated_to_version_n=აპი განახლდა ვერსია {{version}}ზე\ncreate_desktop_entry=Create Desktop Entry\nshutdown_alert=Shut Down Alert\nsystem_shutdown_soon=System Will Shut Down Soon\\!\nsystem_shutdown_failed=System Shut Down Failed\\!\nsystem_shutdown_soon_description=The system will shut down soon. If you're still using the computer, please save your work or cancel the shutdown.\nsystem_shutdown_reason_queue_completed=All downloads in the queue are complete.\nsystem_shutdown_reason_queue_end_time_reached=Scheduled end time for the download queue reached.\nsystem_shutdown_download_finished=Download completed.\nshutdown_now=Shut Down Now\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Create or select a new item first\\!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=These settings will be applied to downloads matching this hostname. Wildcards (*) are supported (e.g., example.com, *.example.com — use only one).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ko_KR.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=다운로드 자동 분류\nconfirm_auto_categorize_downloads_description=분류되지 않은 항목은 자동으로 관련 범주에 추가됩니다.\nconfirm_reset_to_default_categories_title=기본 범주로 재설정\nconfirm_reset_to_default_categories_description=이렇게 하면 모든 범주가 제거되고 기본값 범주가 다시 표시됩니다\\!\nconfirm_delete_download_items_title=삭제 확인\nconfirm_delete_download_items_description={{count}}개의 항목을 삭제하시겠습니까?\nconfirm_delete_download_unfinished_items_description=완료되지 않은 다운로드 {{count}}개를 삭제하시겠습니까?\nconfirm_delete_download_finished_and_unfinished_items_description=완료된 다운로드 {{finishedCount}}개와 완료되지 않은 다운로드 {{unfinishedCount}}개를 삭제하시겠습니까?\nalso_delete_file_from_disk=디스크에서도 파일 삭제\nconfirm_delete_category_item_title={{name}} 범주 제거\nconfirm_delete_category_item_description=\"{{value}}\" 범주를 삭제하시겠습니까?\nyour_download_will_not_be_deleted=다운로드한 것은 삭제되지 않습니다\ndrag_the_file_to_another_app=파일을 다른 앱으로 끌어 놓기\ndrop_link_or_file_here=링크나 파일을 여기에 놓아주세요.\nnothing_will_be_imported=아무것도 가져오지 않았습니다\nn_links_will_be_imported={{count}}개의 링크를 가져옴\nn_items_selected={{count}}개의 항목이 선택됨\nwindow_close=닫기\nwindow_minimize=최소화\nwindow_maximize=최대화\nwindow_restore=복원\ndelete=삭제\nremove=제거\ncancel=취소\nclose=닫기\nmenu=메뉴\nmore_options=추가 옵션\nok=확인\nadd=추가\npaste=붙여넣기\nchange=변경\nedit=편집\nchange_anyway=어쨌든 변경\ndownload=다운로드\nrefresh=새로 고침\nsettings=설정\non_completion=완료 시\nunknown=알 수 없음\nunknown_error=알 수 없는 오류\ndownload_item_not_found=다운로드 항목을 찾을 수 없음\nname=이름\ndownload_link=다운로드 링크\nnot_finished=완료되지 않음\nall=모두\nfinished=완료됨\nUnfinished=미완료\ncanceled=취소됨\nerror=오류\npaused=일시중지됨\ndownloading=다운로드 중\nadded=추가됨\nidle=유휴\npreparing_file=파일 준비 중\ncreating_file=파일 생성 중\nresuming=재개 중\nretrying=다시 시도 중\nlist_is_empty=목록이 비어있습니다\\!\nsearch_in_the_list=목록에서 검색\nsearch=검색\nclear=지우기\ngeneral=일반\nenabled=사용함\ndisabled=사용 안 함\ndefault=기본값\nfile=파일\ntasks=작업\ntools=도구\nhelp=도움말\nsystem=시스템\nall_missing_files=모든 누락된 파일\nall_finished=모든 완료\nall_unfinished=모든 미완료\nentire_list=전체 목록\ndownload_browser_integration=브라우저 통합 다운로드\nexit=종료\nshow_downloads=다운로드 표시\nnew_download=새 다운로드\nstop_all=모두 중지\nimport_from_clipboard=클립보드에서 가져오기\nbatch_download=일괄 다운로드\nopen=열기\nshare=공유\nopen_file=파일 열기\nopen_folder=폴더 열기\nresume=재개\npause=일시 중지\nrestart_download=다운로드 다시 시작\ncopy=복사\ncopy_link=링크 복사\ncopy_as_curl=cURL로 복사\nshow_properties=속성 표시\nmove_to_queue=대기열로 이동\nmove_to_this_queue=이 대기열로 이동\nmove_to_category=범주로 이동\nmove_to_this_category=이 범주로 이동\ncategories=범주\nadd_category=범주 추가\nedit_category=범주 편집\ndelete_category=범주 삭제\ncategory_name=범주 이름\ncategory_download_location=범주 다운로드 위치\ncategory_download_location_description=\"다운로드 추가\"에서 이 범주를 선택한 경우 이 디렉터리를 \"다운로드 위치\"로 사용합니다\ncategory_file_types=범주 파일 형식\ncategory_file_types_description=이러한 파일 유형을 이 범주에 자동으로 추가합니다. (새 다운로드를 추가할 때)\\n파일 확장자는 공백으로 구분하세요 (예\\: ext1, ext2 ...)\ncategory_url_patterns=URL 패턴\ncategory_url_patterns_description=이러한 URL에서 이 범주에 자동으로 다운로드를 추가합니다. (새 다운로드를 추가할 때)\\n공백이 있는 별도의 URL을 사용할 수 있으며, 와일드카드에는 *을 사용할 수도 있습니다\nauto_categorize_downloads=다운로드 자동 분류\nrestore_defaults=기본값으로 복원\nabout=정보\nversion_n=버전 {{value}}\ndeveloped_with_love_for_you=당신을 위해 ❤️으로 개발되었습니다\ndonate=기부\nvisit_the_project_website=프로젝트 웹사이트 방문\nthis_is_a_free_and_open_source_software=이것은 무료 및 오픈 소스 소프트웨어입니다\nview_the_source_code=소스 코드 보기\nthird_party_libraries=타사 라이브러리\npowered_by_open_source_software=오픈 소스 소프트웨어 기반\nview_the_open_source_licenses=오픈 소스 라이선스 보기\nsupport_and_community=지원 및 커뮤니티\ntelegram=Telegram\nchannel=채널\ngroup=그룹\nadd_download=다운로드 추가\nadd_multi_download_page_header=다운로드할 항목 선택\nsave_to=저장 위치\nwhere_should_each_item_saved=각 항목을 어디에 저장하시겠습니까?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=여러 항목이 있습니다\\! 저장할 방법을 선택해 주세요\neach_item_on_its_own_category=각 항목을 자체 범주에\neach_item_on_its_own_category_description=각 항목을 해당 파일 형식을 가진 범주에 배치\nall_items_in_one_category=하나의 범주에 모든 항목\nall_items_in_one_category_description=모든 파일이 선택한 범주에 저장됩니다\nall_items_in_one_Location=모든 항목을 하나의 위치에\nall_items_in_one_Location_description=모든 항목이 선택된 디렉터리에 저장됩니다\nunselected_all_items_in_specific_location_description=모든 파일은 선택한 범주 위치에 저장됩니다\nno_category_selected=선택한 범주 없음\nno_categories_found=범주를 찾을 수 없음\ndownload_location=다운로드 위치\nlocation=위치\nselect_queue=대기열 선택\nwithout_queue=대기열 없음\nuse_category=범주 사용\ncant_write_to_this_folder=이 폴더에 쓸 수 없습니다\nfile_name_already_exists=파일 이름이 이미 존재합니다\ndownload_already_exists=다운로드가 이미 존재합니다\ninvalid_file_name=잘못된 파일 이름\nshow_solutions=해결책 표시...\nchange_solution=해결책 변경\nselect_a_solution=해결책 선택\nselect_download_strategy_description=제공한 링크가 이미 다운로드 목록에 있습니다. 원하는 작업을 지정해 주세요\ndownload_strategy_add_a_numbered_file=번호가 매겨진 파일 추가\ndownload_strategy_add_a_numbered_file_description=다운로드 파일 이름이 끝난 후 인덱스 추가\ndownload_strategy_override_existing_file=기존 파일 덮어쓰기\ndownload_strategy_override_existing_file_description=기존 다운로드를 제거하고 해당 파일에 쓰기\ndownload_strategy_update_download_link=기존 다운로드 업데이트\ndownload_strategy_update_download_link_description=기존 다운로드 링크와 자격 증명 업데이트\ndownload_strategy_show_downloaded_file=다운로드된 파일 표시\ndownload_strategy_show_downloaded_file_description=이미 존재하는 다운로드 항목을 표시하여 재개하거나 열 수 있습니다\nbatch_download_link_help=와일드카드가 포함된 링크를 입력하세요 (* 사용)\ninvalid_url=잘못된 URL\nlist_is_too_large_maximum_n_items_allowed=목록이 너무 큽니다\\! 최대 {{count}}개 항목 허용\nenter_range=범위 입력\nrange_from=시작\nrange_to=끝\nbatch_download_wildcard_length=와일드카드 길이\nfirst_link=첫 번째 링크\nlast_link=마지막 링크\nopen_source_software_used_in_this_app=이 앱에서 사용되는 오픈 소스 소프트웨어\nlinks=링크\nwebsite=웹 사이트\ndevelopers=개발자\nsource_code=소스 코드\nlicense=라이선스\nno_license_found=라이선스를 찾을 수 없음\norganization=조직\nadd_new_queue=새 대기열 추가\nqueue_name=대기열 이름\nqueues=대기열\nstop_queue=대기열 중지\nstart_queue=대기열 시작\nclear_queue_items=빈 대기열\nconfig=구성\nitems=항목\nmove_down=아래로 이동\nmove_up=위로 이동\nremove_queue=대기열 제거\nqueue_name_help=이 대기열의 이름 지정\nqueue_name_describe=대기열 이름은 {{value}}\nqueue_max_concurrent_download=최대 동시 다운로드\nqueue_max_concurrent_download_description=이 대기열의 최대 다운로드 수\nqueue_automatic_stop=자동 중지\nqueue_automatic_stop_description=항목이 없을 때 자동으로 대기열 중지\nqueue_scheduler=스케줄러\nqueue_enable_scheduler=스케줄러 사용\nqueue_active_days=활동일\nqueue_active_days_description=어떤 요일에 스케줄러가 작동하나요?\nqueue_scheduler_enable_auto_start_time=자동 시작 시간 사용\nqueue_scheduler_auto_start_time=자동 시작 시간\nqueue_scheduler_enable_auto_stop_time=자동 중지 시간 사용\nqueue_scheduler_auto_stop_time=자동 중지 시간\nqueue_shutdown_on_completion=완료 시 시스템 종료\nqueue_shutdown_on_completion_description=이 대기열이 완료되거나 예정된 종료 시간에 도달하면 시스템을 자동으로 종료합니다.\nappearance=모양\ndownload_engine=다운로드 엔진\nbrowser_integration=브라우저 통합\nsettings_download_max_retries_count=최대 다운로드 재시도 횟수\nsettings_download_max_retries_count_description=앱이 실패한 다운로드를 포기하기 전에 다시 시도하는 최대 횟수\nsettings_download_max_retries_count_describe_no_retries=실패한 다운로드는 재시도되지 않습니다\nsettings_download_max_retries_count_describe_n_retries=실패한 다운로드를 {{count}}번 재시도합니다\nsettings_download_thread_count=스레드 수\nsettings_download_thread_count_description=다운로드 항목당 최대 다운로드 스레드 수\nsettings_download_thread_count_describe=다운로드에는 최대 {{count}}개의 스레드가 포함될 수 있습니다\nsettings_download_thread_count_with_large_value_describe=경고\\: 스레드 수를 높게 설정하면 시스템 리소스 사용량이 증가하거나 성능이 저하되거나 서버 연결 문제가 발생할 수 있습니다. 시스템과 네트워크에 미칠 수 있는 잠재적 영향을 이해하는 경우에만 더 높은 값을 사용하세요.\nsettings_use_server_last_modified_time=서버의 마지막 수정 시간 사용\nsettings_use_server_last_modified_time_description=파일을 다운로드할 때 로컬 파일에 대해 서버의 마지막 수정 시간을 사용합니다\nsettings_append_extension_to_incomplete_downloads=완료되지 않은 다운로드에 확장자 추가\nsettings_append_extension_to_incomplete_downloads_description=완료되지 않은 다운로드에는 \".part\" 확장자를 추가합니다. 이렇게 하면 완료되지 않은 다운로드를 식별하고 완료되지 않은 파일을 실수로 여는 것을 방지할 수 있습니다.\nsettings_use_sparse_file_allocation=희소 파일 할당\nsettings_use_sparse_file_allocation_description=불필요한 데이터 쓰기를 줄임으로써 특히 SSD에서 파일을 더 효율적으로 생성할 수 있습니다. 이렇게 하면 다운로드 시작 속도를 높이고 디스크 사용량을 줄일 수 있습니다. 다운로드가 느리게 시작되거나 비정상적인 다운로드 속도가 발생하는 경우 일부 디바이스에서 완전히 지원되지 않을 수 있으므로 이 옵션을 비활성화하는 것을 고려해 보세요.\nsettings_ignore_ssl_certificates=SSL 인증서 무시\nsettings_ignore_ssl_certificates_description=SSL 인증서 인증을 비활성화합니다. 필요한 경우에만 사용하면 연결이 보안 위험에 노출될 수 있으므로 사용하세요.\nsettings_global_speed_limiter=전역 속도 제한\nsettings_global_speed_limiter_description=전역 다운로드 속도 제한 (0은 무제한을 의미함)\nsettings_show_average_speed=평균 속도 표시\nsettings_show_average_speed_description=다운로드 속도 평균 또는 정밀도\nsettings_use_category_by_default=기본값으로 범주 사용\nsettings_use_category_by_default_description=다운로드를 추가할 때 기본값으로 범주를 사용합니다.\nsettings_default_download_folder=기본 다운로드 폴더\nsettings_default_download_folder_description=새 다운로드를 추가할 때 이 위치는 기본적으로 사용됩니다\nsettings_default_download_folder_describe=\"{{folder}}\"가 사용됩니다\nsettings_use_proxy=프록시 사용\nsettings_use_proxy_description=파일 다운로드에 프록시 사용\nsettings_use_proxy_describe_no_proxy=프록시 사용 안 함\nsettings_use_proxy_describe_system_proxy=시스템 프록시가 사용됩니다\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\"가 사용됩니다\nsettings_use_proxy_describe_pac_proxy=PAC 파일 \"{{value}}\"이 사용됩니다\nsettings_track_deleted_files_on_disk=디스크에서 삭제된 파일 추적\nsettings_track_deleted_files_on_disk_description=다운로드 디렉터리에서 파일이 삭제되거나 이동되면 파일을 목록에서 자동으로 제거합니다.\nsettings_delete_partial_file_on_download_cancellation=다운로드 취소 시 부분 파일 삭제\nsettings_delete_partial_file_on_download_cancellation_description=다운로드가 취소되면 부분적으로 다운로드된 파일이 디스크에서 삭제됩니다. 이렇게 하면 다운로드 폴더를 깨끗하게 유지하고 불필요한 디스크 공간 사용을 줄일 수 있습니다. 그러나 다음에 다운로드를 시작할 때 처음부터 다운로드가 다시 시작됩니다.\nsettings_default_user_agent=기본 사용자 에이전트\nsettings_default_user_agent_description=기본 사용자 에이전트 문자열을 지정하여 요청이 서버에 식별되는 방식을 정의합니다. 이는 특정 장치에 최적화된 콘텐츠에 액세스하거나 특정 웹사이트에서 부과하는 다운로드 제한을 우회하는 데 도움이 될 수 있습니다.\nsettings_download_size_unit=다운로드 크기 단위\nsettings_download_size_unit_description=다운로드 크기를 표시하는 데 사용되는 단위\nsettings_download_speed_unit=다운로드 속도 단위\nsettings_download_speed_unit_description=다운로드 속도를 표시하는 데 사용되는 단위\nsettings_theme=테마\nsettings_theme_description=앱의 테마 선택\nsettings_default_dark_theme=기본 어두운 테마\nsettings_default_dark_theme_description=앱이 시스템 테마를 따르고 어두운 모드가 활성화된 경우 적용됩니다\nsettings_default_light_theme=기본 밝은 테마\nsettings_default_light_theme_description=앱이 시스템 테마를 따르고 밝은 모드가 활성화된 경우 적용됩니다\nsettings_font=글꼴\nsettings_font_description=앱 인터페이스에서 사용되는 글꼴을 변경하면 일부 글꼴이 앱에 올바르게 표시되지 않을 수 있습니다.\nsettings_ui_scale=UI 크기\nsettings_ui_scale_description=앱의 인터페이스 요소 크기 조정\nsettings_language=언어\nsettings_compact_top_bar=조밀한 상단 표시줄\nsettings_compact_top_bar_description=기본 창의 너비가 충분할 때 상단 막대를 제목 막대와 병합\nsettings_use_native_menu_bar=원래 메뉴 표시줄 사용\nsettings_use_native_menu_bar_description=시스템의 기본 메뉴 표시줄 스타일을 사용합니다\nsettings_use_relative_date_time=상대적 날짜/시간 사용\nsettings_use_relative_date_time_description=앱의 날짜에 대해 상대적 날짜/시간 형식 사용 (예\\: 정확한 날짜/시간 대신 \"2일 전\")\nsettings_show_icon_labels=아이콘 레이블 표시\nsettings_show_icon_labels_description=가능한 경우 아이콘 아래에 레이블 표시 (예\\: 홈 도구모음 작업)\nsettings_use_system_tray=시스템 트레이 사용\nsettings_use_system_tray_description=앱이 실행 중일 때 시스템 트레이 아이콘 표시\nsettings_start_on_boot=부팅할 때 시작\nsettings_start_on_boot_description=사용자 로그인 시 응용 프로그램 자동 시작\nsettings_notification_sound=알림 소리\nsettings_notification_sound_description=새 알림에서 소리 재생\nsettings_browser_integration=브라우저 통합\nsettings_browser_integration_description=브라우저에서 다운로드 수락\nsettings_browser_integration_server_port=서버 포트\nsettings_browser_integration_server_port_description=브라우저 통합을 위한 포트\nsettings_browser_integration_server_port_describe=앱이 {{port}} 포트를 수신할 것입니다\nsettings_dynamic_part_creation=동적 부분 생성\nsettings_dynamic_part_creation_description=부분이 완료되면 다른 부분을 분할하여 새 부분을 생성하여 다운로드 속도를 개선합니다\nsettings_show_completion_dialog=다운로드 완료 표시 대화 상자\nsettings_show_completion_dialog_description=다운로드가 완료되면 자동으로 \"다운로드 완료\" 대화 상자를 표시합니다.\nsettings_show_download_progress_dialog=다운로드 진행 상황 표시 대화 상자\nsettings_show_download_progress_dialog_description=다운로드가 시작되면 자동으로 \"다운로드 진행 상황\" 대화 상자를 표시합니다.\nsettings_per_host_settings=호스트별 설정\nsettings_per_host_settings_descriptions=이 설정은 지정된 호스트와 일치하는 새 다운로드에 자동으로 적용됩니다.\nsettings_download_max_concurrent_downloads=최대 동시 다운로드 수\nsettings_download_max_concurrent_downloads_description=동시에 다운로드할 수 있는 최대 파일 수 (대기열로 관리되는 다운로드는 포함되지 않음, 무제한의 경우 0으로 설정됨)\ndownload_item_settings_speed_limit=속도 제한\ndownload_item_settings_speed_limit_description=이 항목의 다운로드 속도 제한\ndownload_item_settings_show_download_completion_dialog=다운로드 완료 대화 상자 표시\ndownload_item_settings_show_download_completion_dialog_description=이 다운로드가 완료되면 자동으로 \"다운로드 완료\" 대화 상자를 표시합니다.\ndownload_item_settings_shutdown_on_completion=완료 시 시스템 종료\ndownload_item_settings_shutdown_on_completion_description=이 다운로드가 완료되면 시스템을 자동으로 종료합니다.\ndownload_item_settings_thread_count=스레드 수\ndownload_item_settings_thread_count_description=이 다운로드 항목을 다운로드하는 데 사용된 스레드 수 (기본값은 0)\ndownload_item_settings_thread_count_describe=이 다운로드를 위한 {{count}}개의 스레드\ndownload_item_settings_username_description=링크가 보호된 리소스인 경우 사용자 이름 제공\ndownload_item_settings_password_description=링크가 보호된 리소스인 경우 비밀번호 제공\ndownload_item_settings_download_page=다운로드 페이지\ndownload_item_settings_download_page_description=이 다운로드가 시작된 웹페이지\ndownload_item_settings_file_checksum=파일 체크섬\ndownload_item_settings_file_checksum_description=파일이 올바르게 다운로드되었는지 확인하는 데 사용할 수 있는 해시 문자열\ndownload_item_settings_user_agent=사용자 에이전트\ndownload_item_settings_user_agent_description=이 항목에 대한 사용자 지정 사용자 에이전트 (기본값을 사용하려면 비워 두세요)\nfile_checksum=파일 체크섬\nfile_checksum_page=파일 체크섬 검사기\nfile_checksum_page_file_checksum_default_algorithm=기본 알고리즘\nfile_checksum_page_file_checksum_default_algorithm_help=파일 체크섬이 제공되지 않을 때 파일 체크섬을 계산하는 데 사용되는 기본 알고리즘입니다.\nstart=시작\ncalculated_checksum=계산된 체크섬\nsaved_checksum=저장된 체크섬\nchecksum_algorithm=알고리즘\nfile_not_found=파일을 찾을 수 없음\ndownload_not_finished=다운로드가 완료되지 않음\ndone=완료\nwaiting=대기 중\nmatches=일치\nnot_matches=일치하지 않음\ncopy_to_clipboard=클립보드에 복사\nusername=사용자 이름\npassword=비밀번호\naverage_speed=평균 속도\nexact_speed=정확한 속도\nunlimited=무제한\nuse_global_settings=전역 설정 사용\ncant_run_browser_integration=브라우저 통합을 실행할 수 없습니다\ncant_open_file=파일을 열 수 없습니다\ncant_open_folder=폴더를 열 수 없습니다\n# times for example 2 seconds ago\nrelative_time_long_years={{years}}년\nrelative_time_long_months={{months}}개월\nrelative_time_long_days={{days}}일\nrelative_time_long_hours={{hours}}시간\nrelative_time_long_minutes={{minutes}}분\nrelative_time_long_seconds={{seconds}}초\nrelative_time_short_years={{years}}년\nrelative_time_short_months={{months}}개월\nrelative_time_short_days={{days}}일\nrelative_time_short_hours={{hours}}시간\nrelative_time_short_minutes={{minutes}}분\nrelative_time_short_seconds={{seconds}}초\nrelative_time_left={{time}} 남음\nrelative_time_ago={{time}} 전\nauto=자동\nunspecified=지정되지 않음\ncustom=사용자 지정\nicon=아이콘\nauthor=작성자\nlink=링크\nsize=크기\nstatus=상태\nparts_info_downloaded_size=다운로드\nparts_info_total_size=전체\nspeed=속도\ntime_left=남은 시간\ndate_added=추가된 날짜\ninfo=정보\ndownload_page_downloaded_size=다운로드\ndownload_page_download_completed=다운로드 완료\nresume_support=재개 지원\nyes=예\nno=아니요\nparts_info=부분 정보\ndisconnected=연결 끊김\nreceiving_data=데이터 수신 중\nconnecting=연결 중\nwarning=경고\nunsupported_resume_warning=이 다운로드는 재개를 지원하지 않습니다\\! 나중에 다운로드 목록에서 다시 시작해야 할 수도 있습니다\nstop_anyway=그래도 중지\ncustomize_columns=열 사용자 지정\nreset=재설정\nmonday=월요일\ntuesday=화요일\nwednesday=수요일\nthursday=목요일\nfriday=금요일\nsaturday=토요일\nsunday=일요일\nproxy_open_system_proxy_settings=시스템 프록시 설정 열기\nproxy_type=프록시 유형\nproxy_do_not_use_proxy_for=프록시 사용하지 않을 대상\nproxy_do_not_use_proxy_for_description=프록시로 사용할 수 없는 URL 목록\\n*로 와일드카드를 사용할 수 있음\\n예\\: 192.168.1.* example.com (공백으로 구분)\nproxy_change_title=프록시 변경\nchange_proxy=프록시 변경\nproxy_no=프록시 없음\nproxy_system=시스템 프록시\nproxy_manual=수동 프록시\nproxy_pac=프록시 자동 구성\nproxy_pac_url=프록시 자동 구성 URL\naddress=주소\nport=포트\naddress_and_port=주소 및 포트\nuse_authentication=인증 사용\nwarning_you_may_have_to_restart_the_download_later=나중에 다운로드를 다시 시작해야 할 수도 있습니다\\!\nedit_download_title=다운로드 편집\nedit_download_update_from_download_page=다운로드 페이지에서 업데이트\nedit_download_update_from_download_page_description=이 창이 열리면 다운로드 페이지로 이동하여 다운로드 버튼을 클릭할 수 있습니다. 앱에서 새 다운로드 자격 증명을 캡처하고 업데이트하여 저장할 수 있습니다.\nedit_download_saved_download_item_size_not_match=저장된 다운로드 항목의 크기가 {{currentSize}}인데, 이는 새 크기인 {{newSize}}와 일치하지 않습니다.\ntranslators_page_thanks=이 프로젝트 번역에 도움을 주신 분들께 감사드립니다 ❤️\ntranslators=번역가\nlanguage=언어\ntranslators_contribute_title=번역 개선\ntranslators_contribute_description=이 프로젝트를 개선하는 데 도움을 주고 싶으신가요? 귀하의 언어가 목록에 없거나 일부 수정이 필요하다면, 귀하의 번역을 기여하여 더 나은 프로젝트로 만들 수 있습니다\\!\ncontribute=기여하기\nmeet_the_translators=한국어 번역\\: 비너스걸 \nlocalized_by_translators=번역가들에 의해 현지화\nconfirm_exit=종료 확인\nconfirm_exit_description=AB Download Manager를 종료하시겠습니까?\\n활성 다운로드/대기열이 중지됩니다\\!\nupdate=업데이트\nupdate_updater=업데이터\nupdate_available=업데이트 사용 가능\nupdate_error=업데이트 오류\nupdate_available_suggest_to_to_update=최신 버전으로 업데이트하여 새로운 기능, 향상된 기능 및 성능 향상을 즐길 수 있습니다.\nupdate_release_notes=릴리스 노트\nupdate_check_for_update=업데이트 확인\nupdate_checking_for_update=업데이트 확인 중\nupdate_no_update=최신 버전을 사용하고 있습니다\nupdate_check_error=업데이트 확인 중 오류 발생\nupdate_app_updated_to_version_n=앱이 {{version}} 버전으로 업데이트되었습니다\ncreate_desktop_entry=바탕 화면 바로 가기 만들기\nshutdown_alert=종료 알림\nsystem_shutdown_soon=시스템이 곧 종료됩니다\\!\nsystem_shutdown_failed=시스템 종료 실패\\!\nsystem_shutdown_soon_description=시스템이 곧 종료됩니다. 컴퓨터를 계속 사용 중이라면 작업을 저장하거나 종료를 취소해 주세요.\nsystem_shutdown_reason_queue_completed=대기열의 모든 다운로드가 완료되었습니다.\nsystem_shutdown_reason_queue_end_time_reached=다운로드 대기열의 예정된 종료 시간에 도달했습니다.\nsystem_shutdown_download_finished=다운로드가 완료되었습니다.\nshutdown_now=지금 종료\nsettings_per_host_settings_new_host=<새 호스트>\nsettings_per_host_settings_not_selected=먼저 새 항목을 만들거나 선택하세요\\!\nsettings_per_host_settings_host=호스트\nsettings_per_host_settings_host_description=이 설정은 이 호스트 이름과 일치하는 다운로드에 적용됩니다. 와일드카드 (*)가 지원됩니다 (예\\: example.com, *.example.com — 하나만 사용).\nsettings_browser_in_launcher=런처의 브라우저 아이콘\nsettings_browser_in_launcher_description=런처 (앱 목록)에서 브라우저 아이콘을 표시하거나 숨깁니다.\nsort_by=정렬 기준\nwelcome=환영합니다\nnew_folder=새 폴더\nskip=건너뛰기\nlets_go=시작하기\nnext=다음\nselect_all=모두 선택\nselect_inside=내부 선택\nselect_invert=반전 선택\nopen_settings=설정 열기\nback=뒤로\nservice_is_running=서비스가 실행 중입니다\ninitial_setup_description=설정해 보겠습니다\ninitial_setup_notice=이 설정은 나중에 언제든지 변경할 수 있습니다\npermission_granted=권한이 부여됨\npermission_not_granted=권한이 부여되지 않음\npermissions=권한\ngive_permission=권한 허용\ngive_storage_permission=저장소 액세스 허용\nstorage_roots=저장소 루트\npermissions_initial_title=설정해 보겠습니다\npermissions_initial_description=제대로 작동하려면 앱에 몇 가지 권한이 필요합니다. 다음 화면에서 각 권한이 무엇에 사용되는지 확인하고 어떤 권한을 허용할지 건너뛸지 결정할 수 있습니다.\npermissions_done_title=모두 준비되었습니다\npermissions_done_description=모든 준비가 완료되었습니다. 필요한 모든 권한이 부여되었으며 앱은 바로 사용할 수 있습니다.\npermissions_manage_storage_title=저장소 액세스 관리\npermissions_manage_storage_reason=이 권한을 통해 앱은 다운로드 폴더를 변경하고 중복 다운로드를 더 정확하게 감지하며 몇 가지 추가 기능을 활성화할 수 있습니다. 선택 사항이지만 최상의 경험을 위해 권장됩니다.\npermission_read_write_external_storage_title=저장소 읽기 및 쓰기\npermission_read_write_external_storage_reason=이 권한을 통해 앱은 다운로드된 파일을 저장하고 관리하며, 다운로드 위치를 변경하고 중복 다운로드 감지를 개선할 수 있습니다.\npermissions_post_notification_title=알림 액세스\npermissions_post_notification_reason=다운로드를 관리하려면 앱이 백그라운드에서 실행되어야 합니다. 알림은 사용자에게 정보를 제공하고 백그라운드 작업을 허용하는 데 사용됩니다.\npermissions_ignore_battery_optimization_title=배터리 최적화 무시\npermissions_ignore_battery_optimization_reason=일부 기기는 배터리 소모를 줄이기 위해 백그라운드 활동을 적극적으로 제한하는데, 이로 인해 앱이 실행 중이 아닐 때 다운로드가 일시 중단되거나 중단될 수 있습니다. 다운로드가 중단 없이 계속되도록 하려면 앱을 배터리 최적화 대상에서 제외할 수 있습니다\nopen_in_browser=브라우저에서 열기\nbrowser=브라우저\nbrowser_new_tab=새 탭\nbrowser_close_tab=탭 닫기\nbrowser_open_in_new_tab=새 탭에서 열기\nbrowser_open_in_new_background_tab=새 백그라운드 탭에서 열기\nbrowser_no_tab_open=열려 있는 탭이 없습니다\nbrowser_tabs=탭\nbrowser_paste_and_go=붙여넣고 이동\nbrowser_bookmarks=북마크\nbrowser_add_bookmark=북마크 추가\nbrowser_edit_bookmark=북마크 편집\nbrowser_add_to_bookmarks=북마크에 추가\nbrowser_remove_from_bookmarks=북마크에서 제거\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/lt_LT.properties",
    "content": "app_title=AB Atsisiuntimų Tvarkyklė\nconfirm_auto_categorize_downloads_title=Automatiškai suskirstyti atsisiuntimus į kategorijas\nconfirm_auto_categorize_downloads_description=Bet koks nekategorizuotas elementas bus automatiškai pridėtas prie susijusios kategorijos.\nconfirm_reset_to_default_categories_title=Atkurti numatytąsias kategorijas\nconfirm_reset_to_default_categories_description=Tai PAŠALINS visas kategorijas ir atkurs numatytąsias kategorijas\\!\nconfirm_delete_download_items_title=Patvirtinti ištrynimą\nconfirm_delete_download_items_description=Ar tikrai norite ištrinti {{count}} elementus?\nconfirm_delete_download_unfinished_items_description=Ar tikrai norite ištrinti {{count}} nebaigtus atsisiuntimus?\nconfirm_delete_download_finished_and_unfinished_items_description=Ar tikrai norite ištrinti {{finishedCount}} baigtus ir {{unfinishedCount}} nebaigtus atsisiuntimus?\nalso_delete_file_from_disk=Taip pat ištrinti failą iš disko\nconfirm_delete_category_item_title=Šalinama kategorija {{name}}\nconfirm_delete_category_item_description=Ar tikrai norite ištrinti kategoriją \"{{value}}\"?\nyour_download_will_not_be_deleted=Jūsų atsisiuntimai nebus ištrinti\ndrag_the_file_to_another_app=Nuvilkite failą į kitą programėlę\ndrop_link_or_file_here=Įkelkite nuorodą arba failą čia.\nnothing_will_be_imported=Niekas nebus importuojama\nn_links_will_be_imported=Bus importuota {{count}} nuorodų\nn_items_selected=Pasirinkta {{count}} elementų\nwindow_close=Uždaryti\nwindow_minimize=Sumažinti\nwindow_maximize=Maksimizuoti\nwindow_restore=Atkurti\ndelete=Ištrinti\nremove=Pašalinti\ncancel=Atšaukti\nclose=Uždaryti\nmenu=Menu\nmore_options=More Options\nok=Gerai\nadd=Pridėti\npaste=Paste\nchange=Keisti failą\nedit=Redaguoti\nchange_anyway=Keisti vis tiek\ndownload=Atsisiųsti\nrefresh=Atnaujinti\nsettings=Nustatymai\non_completion=Užbaigus\nunknown=Nežinomas\nunknown_error=Nežinoma klaida\ndownload_item_not_found=Atsisiuntimo elementas nerastas\nname=Pavadinimas\ndownload_link=Atsisiuntimo nuoroda\nnot_finished=Neužbaigta\nall=Visi\nfinished=Baigta\nUnfinished=Nebaigtas\ncanceled=Atšauktas\nerror=Klaida\npaused=Pristabdyta\ndownloading=Siunčiama\\:\nadded=Pridėta\nidle=Neaktyvus\npreparing_file=Ruošiamas failas\ncreating_file=Kuriamas failas\nresuming=Tęsiama\nretrying=Bandoma iš naujo\nlist_is_empty=Sąrašas yra tuščias.\nsearch_in_the_list=Ieškoti sąraše\nsearch=Ieškoti\nclear=Išvalyti\ngeneral=Bendrieji nustatymai\nenabled=Įjungta\ndisabled=Išjungta\ndefault=Numatytasis\nfile=Failas\ntasks=Užduotys\ntools=Įrankiai\nhelp=Pagalba\nsystem=Sistema\nall_missing_files=Visi trūkstami failai\nall_finished=Visi baigti\nall_unfinished=Visi nebaigti\nentire_list=Visas sąrašas\ndownload_browser_integration=Atsisiuntimo naršyklės integracija\nexit=Išeiti\nshow_downloads=Rodyti atsisiuntimus\nnew_download=Naujas atsisiuntimas\nstop_all=Stabdyti visus\nimport_from_clipboard=Importuoti iš iškarpinės\nbatch_download=Grupinis atsisiuntimas\nopen=Atidaryti\nshare=Share\nopen_file=Atidaryti failą\nopen_folder=Atidaryti aplanką\nresume=Pratęsti\npause=Pristabdyti\nrestart_download=Paleisti atsisiuntimą iš naujo\ncopy=Kopijuoti\ncopy_link=Kopijuoti nuorodą\ncopy_as_curl=Kopijuoti kaip cURL\nshow_properties=Rodyti savybes\nmove_to_queue=Perkelti į eilę\nmove_to_this_queue=Perkelti į šią eilę\nmove_to_category=Perkelti į kategoriją\nmove_to_this_category=Perkelti į šią kategoriją\ncategories=Kategorijos\nadd_category=Pridėti kategoriją\nedit_category=Redaguoti kategoriją\ndelete_category=Ištrinti kategoriją\ncategory_name=Kategorijos pavadinimas\ncategory_download_location=Kategorijos atsisiuntimo vieta\ncategory_download_location_description=Kai ši kategorija pasirenkama \"Pridėti atsisiuntimą\", naudoti šį aplanką kaip \"Atsisiuntimo vietą\"\ncategory_file_types=Kategorijos failų tipai\ncategory_file_types_description=Automatiškai įdėti šiuos failų tipus į šią kategoriją. (kai pridedate naują atsisiuntimą)\\nAtskirinkite failų plėtines tarpais (ext1 ext2 ...)\ncategory_url_patterns=URL adresų modeliai\ncategory_url_patterns_description=Automatiškai įdėti atsisiuntimus iš šių URL adresų į šią kategoriją. (kai pridedate naują atsisiuntimą)\\nAtskirinkite URL adresus tarpais, galite taip pat naudoti * kaip pakaitos simbolį\nauto_categorize_downloads=Automatiškai suskirstyti atsisiuntimus į kategorijas\nrestore_defaults=Atkurti numatytuosius\nabout=Apie\nversion_n=Versija {{value}}\ndeveloped_with_love_for_you=Sukurta su ❤️ jums\ndonate=Paremti\nvisit_the_project_website=Aplankyti projekto svetainę\nthis_is_a_free_and_open_source_software=Tai yra nemokama ir atvirojo kodo programėlė\nview_the_source_code=Rodyti pirminį kodą\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=Sukurta naudojant atvirojo kodo programinę įrangą\nview_the_open_source_licenses=Rodyti atvirojo kodo licencijas\nsupport_and_community=Pagalba ir bendruomenė\ntelegram=Telegram\nchannel=Kanalas\ngroup=Grupė\nadd_download=Pridėti atsisiuntimą\nadd_multi_download_page_header=Pasirinkite elementus, kuriuos norite atsisiųsti\nsave_to=Išsaugoti į\nwhere_should_each_item_saved=Kur turėtų būti išsaugotas kiekvienas elementas?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Yra daug elementų\\! Pasirinkite, kaip norite juos išsaugoti\neach_item_on_its_own_category=Kiekvienas elementas savoje kategorijoje\neach_item_on_its_own_category_description=Kiekvienas elementas bus įdėtas į kategoriją, kuri turi šį failų tipą\nall_items_in_one_category=Visi elementai vienoje kategorijoje\nall_items_in_one_category_description=Visi failai bus išsaugoti pasirinktoje kategorijoje\nall_items_in_one_Location=Visi elementai vienoje vietoje\nall_items_in_one_Location_description=Visi elementai bus išsaugoti pasirinktoje direktorijoje\nunselected_all_items_in_specific_location_description=Visi failai bus išsaugoti pasirinktos kategorijos vietoje\nno_category_selected=Kategorija nepasirinkta\nno_categories_found=Nerasta jokių kategorijų\ndownload_location=Atsisiuntimo vieta\nlocation=Vieta\nselect_queue=Pasirinkite eilę\nwithout_queue=Be eilės\nuse_category=Naudoti kategoriją\ncant_write_to_this_folder=Negalima rašyti į šį aplanką\nfile_name_already_exists=Failo pavadinimas jau egzistuoja\ndownload_already_exists=Atsisiuntimas jau egzistuoja\ninvalid_file_name=Neteisingas failo pavadinimas\nshow_solutions=Rodyti sprendimus...\nchange_solution=Keisti sprendimą\nselect_a_solution=Pasirinkite sprendimą\nselect_download_strategy_description=Pateikta nuoroda jau yra atsisiuntimų sąraše, nurodykite, ką norite padaryti\ndownload_strategy_add_a_numbered_file=Pridėti sunumeruotą failą\ndownload_strategy_add_a_numbered_file_description=Pridėti indeksą prie atsisiuntimo failo pavadinimo pabaigos\ndownload_strategy_override_existing_file=Pakeisti esamą failą\ndownload_strategy_override_existing_file_description=Pašalinti esamą atsisiuntimą ir rašyti į tą failą\ndownload_strategy_update_download_link=Atnaujinti esamą atsisiuntimą\ndownload_strategy_update_download_link_description=Atnaujinti esamą atsisiuntimo nuorodą ir jos prisijungimo duomenis\ndownload_strategy_show_downloaded_file=Rodyti atsisiųstą failą\ndownload_strategy_show_downloaded_file_description=Rodyti jau esamą atsisiuntimo elementą, kad galėtumėte spustelėti tęsti arba atidaryti\nbatch_download_link_help=Įveskite nuorodą su pakaitos simboliais (naudokite *)\ninvalid_url=Neteisingas URL\nlist_is_too_large_maximum_n_items_allowed=Sąrašas per didelis\\! leidžiama daugiausia {{count}} elementų\nenter_range=Įveskite diapazoną\nrange_from=Nuo\nrange_to=Iki\nbatch_download_wildcard_length=Pakaitos simbolių ilgis\nfirst_link=Pirmoji nuoroda\nlast_link=Paskutinė nuoroda\nopen_source_software_used_in_this_app=Šioje programoje naudojama atvirojo kodo programinė įranga\nlinks=Nuorodos\nwebsite=Svetainė\ndevelopers=Kūrėjai\nsource_code=Pirminis kodas\nlicense=Licencija\nno_license_found=Licencija nerasta\norganization=Organizacija\nadd_new_queue=Pridėti naują eilę\nqueue_name=Eilės pavadinimas\nqueues=Eilės\nstop_queue=Stabdyti eilę\nstart_queue=Pradėti eilę\nclear_queue_items=Išvalyti eilės elementus\nconfig=Konfigūracija\nitems=Elementai\nmove_down=Perkelti žemyn\nmove_up=Perkelti aukštyn\nremove_queue=Pašalinti eilę\nqueue_name_help=Nurodykite šios eilės pavadinimą\nqueue_name_describe=Eilės pavadinimas yra {{value}}\nqueue_max_concurrent_download=Maksimalus vienu metu atsisiuntimų skaičius\nqueue_max_concurrent_download_description=Maksimalus atsisiuntimų skaičius šioje eilėje\nqueue_automatic_stop=Automatinis sustabdymas\nqueue_automatic_stop_description=Automatiškai sustabdyti eilę, kai joje nėra elementų\nqueue_scheduler=Tvarkaraštis\nqueue_enable_scheduler=Įjungti tvarkaraštį\nqueue_active_days=Aktyvios dienos\nqueue_active_days_description=Kuriomis dienomis veikia tvarkaraštis?\nqueue_scheduler_enable_auto_start_time=Įjungti automatinį pradžios laiką\nqueue_scheduler_auto_start_time=Automatinis pradžios laikas\nqueue_scheduler_enable_auto_stop_time=Įjungti automatinį sustabdymo laiką\nqueue_scheduler_auto_stop_time=Automatinis sustabdymo laikas\nqueue_shutdown_on_completion=Išjungti sistemą užbaigus\nqueue_shutdown_on_completion_description=Automatiškai išjungti sistemą, kai ši eilė bus užbaigta arba pasieks suplanuotą pabaigos laiką.\nappearance=Išvaizda\ndownload_engine=Atsisiuntimo variklis\nbrowser_integration=Naršyklės integracija\nsettings_download_max_retries_count=Maksimalus atsisiuntimo bandymų skaičius\nsettings_download_max_retries_count_description=Didžiausias kartų skaičius, kiek programa bandys iš naujo atsisiųsti nepavykusį atsisiuntimą prieš pasiduodant\nsettings_download_max_retries_count_describe_no_retries=Nepavykę atsisiuntimai nebus kartojami\nsettings_download_max_retries_count_describe_n_retries=Nepavykę atsisiuntimai bus kartojami {{count}} kartą(-us)\nsettings_download_thread_count=Gijų skaičius\nsettings_download_thread_count_description=Maksimalus atsisiuntimo gijų skaičius vienam atsisiuntimui\nsettings_download_thread_count_describe=Atsisiuntimui galima naudoti iki {{count}} gijų\nsettings_download_thread_count_with_large_value_describe=Įspėjimas\\: Nustačius didelį gijų skaičių, gali padidėti sistemos išteklių naudojimas, sumažėti našumas arba kilti ryšio problemų su serveriais. Naudokite didesnes reikšmes tik suprasdami galimą poveikį sistemai ir tinklui.\nsettings_use_server_last_modified_time=Naudoti serverio paskutinio keitimo laiką\nsettings_use_server_last_modified_time_description=Atsisiunčiant failą, naudoti serverio paskutinio keitimo laiką vietiniam failui\nsettings_append_extension_to_incomplete_downloads=Pridėti plėtinį nebaigtiems atsisiuntimams\nsettings_append_extension_to_incomplete_downloads_description=Pridėti „.part“ plėtinį nebaigtiems atsisiuntimams. Tai padeda atpažinti nebaigtus atsisiuntimus ir apsaugo nuo netyčinio jų atidarymo.\nsettings_use_sparse_file_allocation=Naudoti retą failų paskirstymą\nsettings_use_sparse_file_allocation_description=Kurti failus efektyviau, ypač SSD diskuose, sumažinant nereikalingą duomenų rašymą. Tai gali pagreitinti atsisiuntimo pradžią ir sumažinti disko naudojimą. Jei atsisiuntimai prasideda lėtai arba pastebite neįprastą greitį, išjunkite šią parinktį, nes ji gali būti nepalaikoma kai kuriuose įrenginiuose.\nsettings_ignore_ssl_certificates=Nepaisyti SSL sertifikatų\nsettings_ignore_ssl_certificates_description=Išjungia SSL sertifikatų tikrinimą. Naudokite tik esant būtinybei, nes tai gali kelti saugumo riziką.\nsettings_global_speed_limiter=Visuotinis greičio ribotuvas\nsettings_global_speed_limiter_description=Bendras atsisiuntimo greičio limitas (0 reiškia neribojama)\nsettings_show_average_speed=Rodyti vidutinį greitį\nsettings_show_average_speed_description=Atsisiuntimo greitis – vidutinis arba tikslus\nsettings_use_category_by_default=Pagal nutylėjimą naudoti kategoriją\nsettings_use_category_by_default_description=Pagal nutylėjimą naudoti kategoriją pridedant atsisiuntimą.\nsettings_default_download_folder=Numatytasis atsisiuntimų aplankas\nsettings_default_download_folder_description=Pridedant naują atsisiuntimą, ši vieta bus naudojama pagal nutylėjimą\nsettings_default_download_folder_describe=\"{}\" bus naudojama\nsettings_use_proxy=Naudoti tarpinį serverį\nsettings_use_proxy_description=Naudoti tarpinį serverį failų atsisiuntimui\nsettings_use_proxy_describe_no_proxy=Tarpių serverių nebus naudojama\nsettings_use_proxy_describe_system_proxy=Bus naudojamas sistemos tarpinis serveris\nsettings_use_proxy_describe_manual_proxy=Bus naudojamas „{{value}}“\nsettings_use_proxy_describe_pac_proxy=Bus naudojamas PAC failas „{{value}}“\nsettings_track_deleted_files_on_disk=Stebėti ištrintus failus diske\nsettings_track_deleted_files_on_disk_description=Automatiškai pašalinti failus iš sąrašo, kai jie ištrinami arba perkeliami iš atsisiuntimų katalogo.\nsettings_delete_partial_file_on_download_cancellation=Ištrinti dalinį failą atšaukus atsisiuntimą\nsettings_delete_partial_file_on_download_cancellation_description=Kai atsisiuntimas atšaukiamas, dalinai atsisiųstas failas bus ištrintas iš disko. Tai padeda išlaikyti tvarką atsisiuntimų aplanke ir sumažina nereikalingą disko vietos naudojimą. Tačiau kitą kartą pradėjus, atsisiuntimas prasidės nuo pradžių.\nsettings_default_user_agent=Numatytasis naudotojo agentas\nsettings_default_user_agent_description=Nurodykite numatytąją naudotojo agento eilutę, kad apibrėžtumėte, kaip užklausos bus atpažįstamos serveriuose. Tai gali padėti pasiekti turinį, optimizuotą tam tikriems įrenginiams, arba apeiti kai kurių svetainių atsisiuntimo apribojimus.\nsettings_download_size_unit=Download Size Unit\nsettings_download_size_unit_description=Unit used to display the download size\nsettings_download_speed_unit=Atsisiuntimo greičio vienetas\nsettings_download_speed_unit_description=Vienetas, kuriuo rodomas atsisiuntimo greitis\nsettings_theme=Tema\nsettings_theme_description=Pasirinkite programos temą\nsettings_default_dark_theme=Numatytoji tamsi tema\nsettings_default_dark_theme_description=Taikoma, kai programa seka sistemos temą ir įjungtas tamsus režimas\nsettings_default_light_theme=Numatytoji šviesi tema\nsettings_default_light_theme_description=Taikoma, kai programa seka sistemos temą ir įjungtas šviesus režimas\nsettings_font=Šriftas\nsettings_font_description=Keisti programos sąsajos šriftą. Kai kurie šriftai gali būti netinkamai rodomi programoje.\nsettings_ui_scale=Vartotojo sąsajos mastelis\nsettings_ui_scale_description=Reguliuoti programos sąsajos elementų dydį\nsettings_language=Kalba\nsettings_compact_top_bar=Kompaktiška viršutinė juosta\nsettings_compact_top_bar_description=Sujungti viršutinę juostą su antrašte, kai pagrindinis langas pakankamai platus\nsettings_use_native_menu_bar=Naudoti natūralią meniu juostą\nsettings_use_native_menu_bar_description=Naudoti sistemos numatytą meniu juostos stilių\nsettings_use_relative_date_time=Naudoti santykinį datą/laiką\nsettings_use_relative_date_time_description=Naudoti santykinį datą/laiką programoje (pvz., „prieš 2 dienas“ vietoj tikslios datos/laiko)\nsettings_show_icon_labels=Rodyti piktogramų etiketes\nsettings_show_icon_labels_description=Rodyti etiketes po piktogramomis, kai įmanoma (pvz., pagrindinės juostos veiksmai)\nsettings_use_system_tray=Naudoti sistemos dėklą\nsettings_use_system_tray_description=Rodyti programos piktogramą sistemos dėkle, kai programa veikia\nsettings_start_on_boot=Paleisti paleidžiant sistemą\nsettings_start_on_boot_description=Automatiškai paleisti programą prisijungus vartotojui\nsettings_notification_sound=Pranešimo garsas\nsettings_notification_sound_description=Grojamas garsas gavus naują pranešimą\nsettings_browser_integration=Naršyklės integracija\nsettings_browser_integration_description=Priimti atsisiuntimus iš naršyklių\nsettings_browser_integration_server_port=Serverio prievadas\nsettings_browser_integration_server_port_description=Prievadas naršyklės integracijai\nsettings_browser_integration_server_port_describe=Programa klausysis {{port}} prievado\nsettings_dynamic_part_creation=Dinaminis dalių kūrimas\nsettings_dynamic_part_creation_description=Kai dalis baigiama, sukurti kitą dalį padalijant kitas dalis, kad pagerėtų atsisiuntimo greitis\nsettings_show_completion_dialog=Rodyti atsisiuntimo pabaigos langą\nsettings_show_completion_dialog_description=Automatiškai rodyti „Atsisiuntimas baigtas“ langą, kai atsisiuntimas baigtas.\nsettings_show_download_progress_dialog=Rodyti atsisiuntimo eigos langą\nsettings_show_download_progress_dialog_description=Automatiškai rodyti „Atsisiuntimo eiga“ langą, kai prasideda atsisiuntimas.\nsettings_per_host_settings=Per Host Settings\nsettings_per_host_settings_descriptions=These settings will be automatically applied to any new download that matches the specified host.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Greičio riba\ndownload_item_settings_speed_limit_description=Greičio riba šiam elementui\ndownload_item_settings_show_download_completion_dialog=Rodyti atsisiuntimo užbaigimo dialogą\ndownload_item_settings_show_download_completion_dialog_description=Automatiškai rodyti \"Atsisiuntimas užbaigtas\" dialogą, kai šis atsisiuntimas baigtas.\ndownload_item_settings_shutdown_on_completion=Išjungti sistemą užbaigus\ndownload_item_settings_shutdown_on_completion_description=Automatiškai išjungti sistemą, kai šis atsisiuntimas bus baigtas.\ndownload_item_settings_thread_count=Gijų skaičius\ndownload_item_settings_thread_count_description=Kiek gijų naudojama šiam atsisiuntimo elementui (0 – numatytajam)\ndownload_item_settings_thread_count_describe={{count}} gijų šiam atsisiuntimui\ndownload_item_settings_username_description=Pateikite naudotojo vardą, jei nuoroda yra apsaugotas išteklius\ndownload_item_settings_password_description=Pateikite slaptažodį, jei nuoroda yra apsaugotas išteklius\ndownload_item_settings_download_page=Atsisiuntimo puslapis\ndownload_item_settings_download_page_description=Interneto puslapis, kuriame buvo pradėtas šis atsisiuntimas\ndownload_item_settings_file_checksum=Failo kontrolinė suma\ndownload_item_settings_file_checksum_description=Maišos eilutė, kuri gali būti naudojama patikrinti, ar failas atsisiųstas teisingai\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Custom User-Agent for this item (leave empty to use the default)\nfile_checksum=Failo kontrolinė suma\nfile_checksum_page=Failo kontrolinės sumos tikrintuvas\nfile_checksum_page_file_checksum_default_algorithm=Numatytasis algoritmas\nfile_checksum_page_file_checksum_default_algorithm_help=Numatytasis algoritmas, naudojamas apskaičiuoti failo kontrolines sumas, kai jos nepateikiamos.\nstart=Pradėti\ncalculated_checksum=Apskaičiuota kontrolinė suma\nsaved_checksum=Išsaugota kontrolinė suma\nchecksum_algorithm=Algoritmas\nfile_not_found=Failas nerastas\ndownload_not_finished=Atsisiuntimas nebaigtas\ndone=Atlikta\nwaiting=Laukiama\nmatches=Sutampa\nnot_matches=Nesutampa\ncopy_to_clipboard=Kopijuoti į iškarpinę\nusername=Naudotojo vardas\npassword=Slaptažodis\naverage_speed=Vidutinis greitis\nexact_speed=Tikslus greitis\nunlimited=Neribotas\nuse_global_settings=Naudoti bendruosius nustatymus\ncant_run_browser_integration=Nepavyksta paleisti naršyklės integracijos\ncant_open_file=Nepavyksta atidaryti failo\ncant_open_folder=Nepavyksta atidaryti katalogo\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} metų\nrelative_time_long_months={{months}} mėnesių\nrelative_time_long_days={{days}} dienų\nrelative_time_long_hours={{hours}} valandų\nrelative_time_long_minutes={{minutes}} minučių\nrelative_time_long_seconds={{seconds}} sekundžių\nrelative_time_short_years={{years}} m\nrelative_time_short_months={{months}} mėn\nrelative_time_short_days={{days}} d\nrelative_time_short_hours={{hours}} val\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} s\nrelative_time_left={{time}} liko\nrelative_time_ago={{time}} atgal\nauto=Automatinis\nunspecified=Nenurodyta\ncustom=Pasirinktinis\nicon=Piktograma\nauthor=Autorius\nlink=Nuoroda\nsize=Dydis\nstatus=Būsena\nparts_info_downloaded_size=Atsisiųsta\nparts_info_total_size=Iš viso\nspeed=Greitis\ntime_left=Liko laiko\ndate_added=Pridėta data\ninfo=Informacija\ndownload_page_downloaded_size=Atsisiųsta\ndownload_page_download_completed=Atsisiuntimas baigtas\nresume_support=Tęsimo palaikymas\nyes=Taip\nno=Ne\nparts_info=Dalių informacija\ndisconnected=Atsijungta\nreceiving_data=Gaunami duomenys\nconnecting=Jungiamasi\nwarning=Įspėjimas\nunsupported_resume_warning=Šis atsisiuntimas nepalaiko tęsimo\\! Gali tekti jį PERKRAUTI vėliau atsisiuntimų sąraše\nstop_anyway=Stabdyti vis tiek\ncustomize_columns=Pritaikyti stulpelius\nreset=Atkurti\nmonday=Pirmadienis\ntuesday=Antradienis\nwednesday=Trečiadienis\nthursday=Ketvirtadienis\nfriday=Penktadienis\nsaturday=Šeštadienis\nsunday=Sekmadienis\nproxy_open_system_proxy_settings=Atidaryti sistemos tarpinio serverio nustatymus\nproxy_type=Tarpinio serverio tipas\nproxy_do_not_use_proxy_for=Nenaudoti tarpinio serverio\nproxy_do_not_use_proxy_for_description=URL sąrašas, kuriam neturėtų būti naudojamas tarpinis serveris\\nGalite naudoti pakaitos simbolius su *\\npvz., 192.168.1.* example.com (atskirti tarpais)\nproxy_change_title=Keisti tarpinį serverį\nchange_proxy=Keisti tarpinį serverį\nproxy_no=Be tarpinio serverio\nproxy_system=Sistemos tarpinis serveris\nproxy_manual=Rankinis tarpinis serveris\nproxy_pac=Tarpinio serverio automatinė konfigūracija\nproxy_pac_url=Tarpinio serverio automatinės konfigūracijos URL\naddress=Adresas\nport=Prievadas\naddress_and_port=Adresas ir prievadas\nuse_authentication=Naudoti autentifikaciją\nwarning_you_may_have_to_restart_the_download_later=Gali tekti perkrauti atsisiuntimą vėliau\\!\nedit_download_title=Redaguoti atsisiuntimą\nedit_download_update_from_download_page=Atnaujinti iš atsisiuntimo puslapio\nedit_download_update_from_download_page_description=Kai šis langas atidarytas, galite eiti į atsisiuntimo puslapį ir spustelėti atsisiuntimo mygtuką. Programėlė perims ir atnaujins naujus atsisiuntimo duomenis, kad galėtumėte juos išsaugoti.\nedit_download_saved_download_item_size_not_match=Išsaugotas atsisiuntimo elementas turi dydį {{currentSize}}, kuris nesutampa su nauju dydžiu {{newSize}}.\ntranslators_page_thanks=Su dėkingumu tiems, kurie padėjo išversti šį projektą ❤️\ntranslators=Vertėjai\nlanguage=Kalba\ntranslators_contribute_title=Pagerinti vertimus\ntranslators_contribute_description=Norite padėti pagerinti šį projektą? Jei jūsų kalba nėra sąraše arba reikia patobulinimų, galite prisidėti savo vertimais ir padaryti jį geresnį\\!\ncontribute=Prisidėti\nmeet_the_translators=Susipažinkite su vertėjais\nlocalized_by_translators=Lokalizuota vertėjų\nconfirm_exit=Patvirtinti išėjimą\nconfirm_exit_description=Ar tikrai norite išeiti iš AB Atsisiuntimų Tvarkyklės?\\nAktyvūs atsisiuntimai/eilės bus sustabdyti\\!\nupdate=Atnaujinti\nupdate_updater=Atnaujinimo programa\nupdate_available=Galimas atnaujinimas\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=Galite atnaujinti į naujausią versiją, kad mėgautumėtės naujomis funkcijomis, patobulinimais ir našumo gerinimais.\nupdate_release_notes=Leidimo pastabos\nupdate_check_for_update=Patikrinti atnaujinimus\nupdate_checking_for_update=Tikrinami atnaujinimai\nupdate_no_update=Naudojate naujausią versiją\nupdate_check_error=Klaida, bandant patikrinti atnaujinimus\nupdate_app_updated_to_version_n=Programėlė atnaujinta į versiją {{version}}\ncreate_desktop_entry=Sukurti darbalaukio elementą\nshutdown_alert=Išjungimo įspėjimas\nsystem_shutdown_soon=Sistema netrukus bus išjungta\\!\nsystem_shutdown_failed=Nepavyko išjungti sistemos\\!\nsystem_shutdown_soon_description=Sistema netrukus bus išjungta. Jei vis dar naudojate kompiuterį, išsaugokite savo darbą arba atšaukite išjungimą.\nsystem_shutdown_reason_queue_completed=Visi eilėje esantys atsisiuntimai baigti.\nsystem_shutdown_reason_queue_end_time_reached=Pasiektas suplanuotas atsisiuntimų eilės pabaigos laikas.\nsystem_shutdown_download_finished=Atsisiuntimas baigtas.\nshutdown_now=Išjungti dabar\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Sukurkite arba pasirinkite naują elementą pirmiausia\\!\nsettings_per_host_settings_host=Šeimininkas\nsettings_per_host_settings_host_description=Šie nustatymai bus taikomi atsisiuntimams, atitinkantiems šį šeimininko vardą. Palaikomi pakaitos simboliai (*) (pvz., example.com, *.example.com — naudokite tik vieną).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/pl_PL.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Automatycznie kategoryzuj pobrane pliki\nconfirm_auto_categorize_downloads_description=Każdy element bez kategorii zostanie automatycznie dodany do kategorii z nim powiązanej.\nconfirm_reset_to_default_categories_title=Przywróć domyślne kategorie\nconfirm_reset_to_default_categories_description=Spowoduje to USUNIĘCIE wszystkich kategorii i przywrócenie kategorii domyślnych\\!\nconfirm_delete_download_items_title=Potwierdź usunięcie\nconfirm_delete_download_items_description=Czy na pewno chcesz usunąć {{count}} plików?\nconfirm_delete_download_unfinished_items_description=Czy na pewno chcesz usunąć {{count}} niedokończonych pobrań?\nconfirm_delete_download_finished_and_unfinished_items_description=Czy na pewno chcesz usunąć {{finishedCount}} dokończonych i {{unfinishedCount}} niedokończonych pobrań?\nalso_delete_file_from_disk=Usuń również plik z dysku\nconfirm_delete_category_item_title=Usuwanie kategorii {{name}}\nconfirm_delete_category_item_description=Czy na pewno chcesz usunąć kategorię\"{{value}}\"?\nyour_download_will_not_be_deleted=Pobrane pliki nie zostaną usunięte\ndrag_the_file_to_another_app=Przenieś plik do innej aplikacji\ndrop_link_or_file_here=Upuść tutaj link lub plik.\nnothing_will_be_imported=Nic nie zostanie zaimportowane\nn_links_will_be_imported={{count}} linków zostanie zaimportowanych\nn_items_selected={{count}} wybranych plików\nwindow_close=Zamknij\nwindow_minimize=Zminimalizuj\nwindow_maximize=Zmaksymalizuj\nwindow_restore=Przywróć\ndelete=Skasuj\nremove=Usuń\ncancel=Anuluj\nclose=Zamknij\nmenu=Menu\nmore_options=Więcej opcji\nok=Ok\nadd=Dodaj\npaste=Wklej\nchange=Zmień\nedit=Edytuj\nchange_anyway=Mimo wszystko zmień\ndownload=Pobierz\nrefresh=Odśwież\nsettings=Ustawienia\non_completion=Po ukończeniu\nunknown=Nieznane\nunknown_error=Nieznany błąd\ndownload_item_not_found=Nie znaleziono pliku do pobrania\nname=Nazwa\ndownload_link=Link pobierania\nnot_finished=Nie ukończone\nall=Wszystkie\nfinished=Ukończone\nUnfinished=Niedokończone\ncanceled=Anulowane\nerror=Błąd\npaused=Wstrzymane\ndownloading=Pobieranie\nadded=Dodane\nidle=BEZCZYNNOŚĆ\npreparing_file=Przygotowywanie pliku\ncreating_file=Tworzenie pliku\nresuming=Wznawianie\nretrying=Ponawianie\nlist_is_empty=Lista jest pusta\\!\nsearch_in_the_list=Wyszukuj na liście\nsearch=Szukaj\nclear=Wyczyść\ngeneral=Ogólne\nenabled=Włączone\ndisabled=Wyłączone\ndefault=Domyślny\nfile=Plik\ntasks=Zadania\ntools=Narzędzia\nhelp=Pomoc\nsystem=System\nall_missing_files=Wszystkie brakujące pliki\nall_finished=Wszystkie zakończone\nall_unfinished=Wszystkie niedokończone\nentire_list=Cała lista\ndownload_browser_integration=Pobierz integrację z przeglądarką\nexit=Wyjdź\nshow_downloads=Pokaż pobrania\nnew_download=Nowe pobieranie\nstop_all=Zatrzymaj wszystkie\nimport_from_clipboard=Importuj ze schowka\nbatch_download=Pobieranie zbiorcze\nopen=Otwórz\nshare=Udostępnij\nopen_file=Otwórz Plik\nopen_folder=Otwórz folder\nresume=Wznów\npause=Wstrzymaj\nrestart_download=Zrestartuj pobieranie\ncopy=Kopiuj\ncopy_link=Kopiuj link\ncopy_as_curl=Kopiuj jako cURL\nshow_properties=Pokaż właściwości\nmove_to_queue=Przenieś do kolejki\nmove_to_this_queue=Przenieś do tej kolejki\nmove_to_category=Przenieś do kategorii\nmove_to_this_category=Przenieś do tej kategorii\ncategories=Kategorie\nadd_category=Dodaj kategorię\nedit_category=Edytuj kategorię\ndelete_category=Usuń kategorię\ncategory_name=Nazwa kategorii\ncategory_download_location=Lokalizacja pobierania kategorii\ncategory_download_location_description=Po wybraniu tej kategorii w „Nowe zadanie pobierania” użyj tego katalogu jako „Lokalizacja pobierania”\ncategory_file_types=Rodzaje plików kategorii\ncategory_file_types_description=Automatycznie umieszczaj te typy plików w tej kategorii. (po dodaniu nowego pobierania)\\nOddziel rozszerzenia plików spacją (rozszerzenie1 rozszerzenie2...)\ncategory_url_patterns=Wzory adresów URL\ncategory_url_patterns_description=Automatycznie umieszczaj pobieranie z tych adresów URL w tej kategorii. (po dodaniu nowego pobierania)\\nOddziel adresy URL spacją, możesz również użyć * dla symboli wieloznacznych\nauto_categorize_downloads=Automatycznie kategoryzuj pobrania\nrestore_defaults=Przywróć domyślne\nabout=O programie\nversion_n=Wersja {{value}}\ndeveloped_with_love_for_you=Stworzone z ❤️ dla Ciebie\ndonate=Wspomóż\nvisit_the_project_website=Odwiedź stronę projektu\nthis_is_a_free_and_open_source_software=To jest darmowe i otwarte oprogramowanie\nview_the_source_code=Zobacz kod źródłowy\nthird_party_libraries=Biblioteki zewnętrzne\npowered_by_open_source_software=Zasilane przez otwarte oprogramowanie\nview_the_open_source_licenses=Zobacz licencje \"Open Source\"\nsupport_and_community=Wsparcie i społeczność\ntelegram=Telegram\nchannel=Kanał\ngroup=Grupa\nadd_download=Nowe zadanie pobierania\nadd_multi_download_page_header=Wybierz pliki, które chcesz pobrać\nsave_to=Zapisz do\nwhere_should_each_item_saved=Gdzie powinien być zapisany każdy plik?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Istnieje wiele plików\\! Wybierz sposób, w jaki chcesz je zapisać\neach_item_on_its_own_category=Każdy plik we własnej kategorii\neach_item_on_its_own_category_description=Każdy plik zostanie umieszczony w kategorii, która posiada ten typ pliku\nall_items_in_one_category=Każdy plik w jednej kategorii\nall_items_in_one_category_description=Wszystkie pliki zostaną zapisane w wybranej kategorii\nall_items_in_one_Location=Każdy plik w jednej lokalizacji\nall_items_in_one_Location_description=Wszystkie pliki zostaną zapisane w wybranej lokalizacji\nunselected_all_items_in_specific_location_description=Wszystkie pliki zostaną zapisane w wybranej lokalizacji kategorii\nno_category_selected=Nie wybrano żadnej kategorii\nno_categories_found=Nie znaleziono żadnych kategorii\ndownload_location=Lokalizacja pobierania\nlocation=Lokalizacja\nselect_queue=Wybierz kolejkę\nwithout_queue=Bez kolejki\nuse_category=Użyj kategorii\ncant_write_to_this_folder=Nie można zapisać do tego folderu\nfile_name_already_exists=Taki plik już istnieje\ndownload_already_exists=Takie pobieranie już istnieje\ninvalid_file_name=Nieprawidłowa nazwa pliku\nshow_solutions=Pokaż rozwiązania...\nchange_solution=Zmień rozwiązanie\nselect_a_solution=Wybierz rozwiązanie\nselect_download_strategy_description=Podany link znajduje się już na liście pobrań, określ co chcesz w tym wypadku zrobić\ndownload_strategy_add_a_numbered_file=Numeruj plik\ndownload_strategy_add_a_numbered_file_description=Dodaj indeks na koniec nazwy pobieranego pliku\ndownload_strategy_override_existing_file=Nadpisz istniejący plik\ndownload_strategy_override_existing_file_description=Usuń istniejące pobieranie i zapisz do tego pliku\ndownload_strategy_update_download_link=Zaktualizuj istniejące pobieranie\ndownload_strategy_update_download_link_description=Zaktualizuj istniejący link pobierania i jego dane uwierzytelniające\ndownload_strategy_show_downloaded_file=Pokaż pobrany plik\ndownload_strategy_show_downloaded_file_description=Pokaż już istniejący pobrany plik, abyś mógł nacisnąć przycisk wznowienia lub go otworzyć\nbatch_download_link_help=Wprowadź link, który zawiera symbole wieloznaczne (użyj *)\ninvalid_url=Nieprawidłowy adres URL\nlist_is_too_large_maximum_n_items_allowed=Lista jest zbyt duża\\! Maksymalna dozwolona liczba plików to {{count}}\nenter_range=Wprowadź zakres\nrange_from=Od\nrange_to=Do\nbatch_download_wildcard_length=Długość wieloznacznika\nfirst_link=Pierwszy link\nlast_link=Ostatni link\nopen_source_software_used_in_this_app=Otwarte oprogramowanie używane w tej aplikacji\nlinks=Linki\nwebsite=Strona internetowa\ndevelopers=Programiści\nsource_code=Kod źródłowy\nlicense=Licencja\nno_license_found=Nie znaleziono licencji\norganization=Organizacja\nadd_new_queue=Dodaj nową kolejkę\nqueue_name=Nazwa kolejki\nqueues=Kolejki\nstop_queue=Zatrzymaj kolejkę\nstart_queue=Uruchom kolejkę\nclear_queue_items=Opróżnij kolejkę\nconfig=Konfiguracja\nitems=Pliki\nmove_down=Przesuń w dół\nmove_up=Przesuń w górę\nremove_queue=Usuń kolejkę\nqueue_name_help=Określ nazwę dla tej kolejki\nqueue_name_describe=Nazwa kolejki to {{value}}\nqueue_max_concurrent_download=Maksymalna ilość równoczesnych pobrań\nqueue_max_concurrent_download_description=Maksymalna ilość pobrań dla tej kolejki\nqueue_automatic_stop=Automatyczne zatrzymanie\nqueue_automatic_stop_description=Automatycznie zatrzymaj kolejkę, gdy nie ma w niej plików\nqueue_scheduler=Harmonogram\nqueue_enable_scheduler=Włącz harmonogram\nqueue_active_days=Aktywne dni\nqueue_active_days_description=W jakie dni mają działać harmonogramy?\nqueue_scheduler_enable_auto_start_time=Włącz czas automatycznego startu\nqueue_scheduler_auto_start_time=Czas automatycznego startu\nqueue_scheduler_enable_auto_stop_time=Włącz czas automatycznego startu\nqueue_scheduler_auto_stop_time=Czas automatycznego zatrzymania\nqueue_shutdown_on_completion=Wyłącz system po ukończeniu\nqueue_shutdown_on_completion_description=Automatycznie wyłącz system po zakończeniu kolejki, lub po zaplanowanym czasie.\nappearance=Wygląd\ndownload_engine=Silnik pobierania\nbrowser_integration=Integracja z przeglądarką\nsettings_download_max_retries_count=Maksymalna ilość prób pobierania\nsettings_download_max_retries_count_description=Maksymalna liczba ponawianych przez aplikację prób nieudanego pobrania przed poddaniem się\nsettings_download_max_retries_count_describe_no_retries=Nieudane pobrania nie będą ponawiane\nsettings_download_max_retries_count_describe_n_retries=Nieudane pobrania zostaną ponowione {{count}} raz(y)\nsettings_download_thread_count=Liczba wątków\nsettings_download_thread_count_description=Maksymalna liczba wątków pobierania na plik\nsettings_download_thread_count_describe=Pobieranie może mieć do {{count}} wątków\nsettings_download_thread_count_with_large_value_describe=Ostrzeżenie\\: Ustawienie dużej liczby wątków może zwiększyć wykorzystanie zasobów systemowych, zmniejszyć wydajność lub spowodować problemy z połączeniem z serwerami. Używaj wyższych wartości tylko wtedy, gdy rozumiesz ich potencjalny wpływ na system i sieć.\nsettings_use_server_last_modified_time=Użyj czasu ostatniej modyfikacji serwera\nsettings_use_server_last_modified_time_description=Podczas pobierania pliku używany jest czas ostatniej modyfikacji pliku lokalnego na serwerze\nsettings_append_extension_to_incomplete_downloads=Dodaj rozszerzenie do niedokończonych pobrań\nsettings_append_extension_to_incomplete_downloads_description=Dodaj rozszerzenie \".part\" do niedokończonych pobrań. Pomaga to zidentyfikować niedokończone pobrania i zapobiec przypadkowemu otwarciu/uruchomieniu niekompletnych plików/programów.\nsettings_use_sparse_file_allocation=Niewielka alokacja plików\nsettings_use_sparse_file_allocation_description=Bardziej wydajne tworzenie plików, zwłaszcza na dyskach SSD, poprzez ograniczenie niepotrzebnego zapisu danych. Może to przyspieszyć rozpoczęcie pobierania i zmniejszyć zużycie dysku. Jeśli pobieranie rozpoczyna się powoli lub występuje nietypowa prędkość pobierania, należy rozważyć wyłączenie tej opcji, ponieważ może ona nie być w pełni obsługiwana na niektórych urządzeniach.\nsettings_ignore_ssl_certificates=Ignoruj certyfikaty SSL\nsettings_ignore_ssl_certificates_description=Wyłącza weryfikację certyfikatów SSL. Używaj tylko wtedy, gdy jest to konieczne, ponieważ może to narazić połączenie na zagrożenia bezpieczeństwa.\nsettings_global_speed_limiter=Globalny ogranicznik prędkości\nsettings_global_speed_limiter_description=Globalny limit prędkości pobierania (0 oznacza nieograniczony)\nsettings_show_average_speed=Pokaż średnią prędkość\nsettings_show_average_speed_description=Średnia lub dokładna prędkość pobierania\nsettings_use_category_by_default=Domyślnie używaj kategorii\nsettings_use_category_by_default_description=Domyślnie użyj kategorii podczas dodawania pobierania.\nsettings_default_download_folder=Domyślny folder pobierania\nsettings_default_download_folder_description=Ta lokalizacja będzie używana jako domyślna podczas dodawania nowego pobierania\nsettings_default_download_folder_describe=\"{{folder}}\" zostanie użyty\nsettings_use_proxy=Użyj proxy\nsettings_use_proxy_description=Użyj proxy do pobierania plików\nsettings_use_proxy_describe_no_proxy=Nie używaj proxy\nsettings_use_proxy_describe_system_proxy=Użyj domyślnego proxy systemowego\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" zostanie użyte\nsettings_use_proxy_describe_pac_proxy=Plik pac \"{{value}}\" będzie używany\nsettings_track_deleted_files_on_disk=Śledź usunięte pliki na dysku\nsettings_track_deleted_files_on_disk_description=Automatyczne usuwanie plików z listy po ich usunięciu lub przeniesieniu z folderu pobierania.\nsettings_delete_partial_file_on_download_cancellation=Usuń plik częściowy przy anulowaniu pobierania\nsettings_delete_partial_file_on_download_cancellation_description=Gdy pobieranie zostanie anulowane, częściowo pobrany plik zostanie usunięty z dysku, zmniejszając niepotrzebne użycie miejsca na dysku. Pobieranie zostanie jednak zrestartowane do początku przy następnym uruchomieniu.\nsettings_default_user_agent=Domyślny User Agent\nsettings_default_user_agent_description=Określ domyślny ciąg \"User Agent\", aby zdefiniować sposób, w jaki żądania identyfikują się z serwerami. Może to pomóc w uzyskaniu dostępu do treści zoptymalizowanych dla określonych urządzeń lub w obejściu ograniczeń pobierania nałożonych przez niektóre witryny internetowe.\nsettings_download_size_unit=Jednostka prędkości pobierania\nsettings_download_size_unit_description=Jednostka używana do wyświetlania prędkości pobierania\nsettings_download_speed_unit=Jednostka prędkości pobierania\nsettings_download_speed_unit_description=Jednostka używana do wyświetlania prędkości pobierania\nsettings_theme=Motyw\nsettings_theme_description=Wybierz motyw aplikacji\nsettings_default_dark_theme=Domyślny ciemny motyw\nsettings_default_dark_theme_description=Stosuje się, gdy aplikacja śledzi motyw systemowy, a ciemny motyw jest aktywny\nsettings_default_light_theme=Domyślny jasny motyw\nsettings_default_light_theme_description=Stosuje się, gdy aplikacja śledzi motyw systemowy, a jasny motyw jest aktywny\nsettings_font=Czcionka\nsettings_font_description=Zmień czcionkę używaną w interfejsie aplikacji, niektóre czcionki mogą nie wyświetlać się poprawnie w aplikacji.\nsettings_ui_scale=Skala interfejsu\nsettings_ui_scale_description=Dostosuj rozmiar elementów interfejsu aplikacji\nsettings_language=Język\nsettings_compact_top_bar=Kompaktowy pasek górny\nsettings_compact_top_bar_description=Scal górny pasek z paskiem tytułowym, gdy główne okno ma wystarczającą szerokość\nsettings_use_native_menu_bar=Użyj natywnego paska menu\nsettings_use_native_menu_bar_description=Użyj domyślnego stylu paska menu systemu\nsettings_use_relative_date_time=Użyj względnej daty/czasu\nsettings_use_relative_date_time_description=Użyj względnego formatu daty/czasu dla dat w aplikacji (np. \"2 dni temu\" zamiast dokładnej daty/czasu)\nsettings_show_icon_labels=Pokaż etykiety ikon\nsettings_show_icon_labels_description=Pokaż etykiety pod ikonami, jeśli to możliwe (np. akcje paska narzędzi głównych)\nsettings_use_system_tray=Użyj zasobnika systemowego\nsettings_use_system_tray_description=Pokaż ikonę w zasobniku systemowym, gdy aplikacja jest uruchomiona\nsettings_start_on_boot=Uruchom przy starcie systemu\nsettings_start_on_boot_description=Automatycznie uruchom aplikację przy logowaniu użytkownika\nsettings_notification_sound=Dźwięk powiadomienia\nsettings_notification_sound_description=Odtwórz dźwięk przy nowym powiadomieniu\nsettings_browser_integration=Integracja z przeglądarką\nsettings_browser_integration_description=Akceptuj pobrania z przeglądarek\nsettings_browser_integration_server_port=Port serwera\nsettings_browser_integration_server_port_description=Port do integracji z przeglądarką\nsettings_browser_integration_server_port_describe=Aplikacja będzie słuchać portu {{port}}\nsettings_dynamic_part_creation=Dynamiczne tworzenie części\nsettings_dynamic_part_creation_description=Gdy część zostanie ukończona, utwórz kolejną część, dzieląc inne części, aby poprawić prędkość pobierania\nsettings_show_completion_dialog=Pokaż okno dialogowe zakończenia pobierania\nsettings_show_completion_dialog_description=Automatycznie pokaż okno dialogowe \"Pobieranie zakończone\" po zakończeniu pobierania.\nsettings_show_download_progress_dialog=Pokaż okno postępu pobierania\nsettings_show_download_progress_dialog_description=Automatycznie pokaż okno dialogowe \"Postęp pobierania\" po rozpoczęciu pobierania.\nsettings_per_host_settings=Ustawienia dla serwera\nsettings_per_host_settings_descriptions=Te ustawienia zostaną automatycznie zastosowane do każdego nowego pobierania, które pasuje do określonego serwera.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Limit prędkości\ndownload_item_settings_speed_limit_description=Ogranicz prędkość pobierania dla tego pliku\ndownload_item_settings_show_download_completion_dialog=Pokaż okno dialogowe zakończenia pobierania\ndownload_item_settings_show_download_completion_dialog_description=Automatycznie pokaż okno dialogowe \"Pobieranie zakończone\" po zakończeniu tego pobierania.\ndownload_item_settings_shutdown_on_completion=Wyłącz system po zakończeniu\ndownload_item_settings_shutdown_on_completion_description=Automatycznie wyłącz system po zakończeniu pobierania.\ndownload_item_settings_thread_count=Liczba wątków\ndownload_item_settings_thread_count_description=Ile wątków ma zostać użytych do pobrania tego pliku (domyślnie\\: 0)\ndownload_item_settings_thread_count_describe={{count}} wątków dla tego pobrania\ndownload_item_settings_username_description=Podaj nazwę użytkownika, jeśli link jest chronionym zasobem\ndownload_item_settings_password_description=Podaj hasło, jeśli link jest chronionym zasobem\ndownload_item_settings_download_page=Strona pobierania\ndownload_item_settings_download_page_description=Strona internetowa, na której rozpoczęto pobieranie\ndownload_item_settings_file_checksum=Suma kontrolna pliku\ndownload_item_settings_file_checksum_description=Ciąg \"hash\", który może być użyty do sprawdzenia, czy plik został poprawnie pobrany\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Niestandardowy User-Agent dla tego elementu (zostaw puste, aby użyć domyślnego)\nfile_checksum=Suma kontrolna pliku\nfile_checksum_page=Sprawdzanie sumy kontrolnej pliku\nfile_checksum_page_file_checksum_default_algorithm=Domyślny algorytm\nfile_checksum_page_file_checksum_default_algorithm_help=Domyślny algorytm używany do obliczania sum kontrolnych plików, gdy nie zostały one podane.\nstart=Rozpocznij\ncalculated_checksum=Obliczona suma kontrolna\nsaved_checksum=Zapisana suma kontrolna\nchecksum_algorithm=Algorytm\nfile_not_found=Nie znaleziono pliku\ndownload_not_finished=Pobieranie nieukończone\ndone=Gotowe\nwaiting=Oczekiwanie\nmatches=Dopasowania\nnot_matches=Niepasujące\ncopy_to_clipboard=Kopiuj do schowka\nusername=Nazwa użytkownika\npassword=Hasło\naverage_speed=Średnia prędkość\nexact_speed=Dokładna prędkość\nunlimited=Nieograniczona\nuse_global_settings=Użyj ustawień globalnych\ncant_run_browser_integration=Nie można uruchomić integracji z przeglądarką\ncant_open_file=Nie można otworzyć pliku\ncant_open_folder=Nie można otworzyć folderu\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} lat\nrelative_time_long_months={{months}} miesięcy\nrelative_time_long_days={{days}} dni\nrelative_time_long_hours={{hours}} godzin\nrelative_time_long_minutes={{minutes}} minut\nrelative_time_long_seconds={{seconds}} sekund\nrelative_time_short_years={{years}} lat\nrelative_time_short_months={{months}} mies.\nrelative_time_short_days={{days}} dni\nrelative_time_short_hours={{hours}} godz.\nrelative_time_short_minutes={{minutes}} min.\nrelative_time_short_seconds={{seconds}} sek.\nrelative_time_left={{time}}\nrelative_time_ago={{time}} temu\nauto=Auto\nunspecified=Nieokreślona\ncustom=Niestandardowa\nicon=Ikona\nauthor=Autor\nlink=Link\nsize=Rozmiar\nstatus=Status\nparts_info_downloaded_size=Pobrano\nparts_info_total_size=Ogółem\nspeed=Prędkość\ntime_left=Pozostały czas\ndate_added=Data dodania\ninfo=Info\ndownload_page_downloaded_size=Pobrano\ndownload_page_download_completed=Pobieranie zakończone\nresume_support=Wsparcie ponawiania\nyes=Tak\nno=Nie\nparts_info=Informacje o częściach\ndisconnected=Rozłączono\nreceiving_data=Pobieranie danych\nconnecting=Łączenie\nwarning=Ostrzeżenie\nunsupported_resume_warning=To pobieranie nie obsługuje wznawiania\\! Być może będziesz musiał ZRESETOWAĆ je później z poziomu listy pobierania\nstop_anyway=Zatrzymaj mimo to\ncustomize_columns=Dostosuj kolumny\nreset=Zresetuj\nmonday=Poniedziałek\ntuesday=Wtorek\nwednesday=Środa\nthursday=Czwartek\nfriday=Piątek\nsaturday=Sobota\nsunday=Niedziela\nproxy_open_system_proxy_settings=Otwórz systemowe ustawienia proxy\nproxy_type=Typ proxy\nproxy_do_not_use_proxy_for=Nie używaj proxy dla\nproxy_do_not_use_proxy_for_description=Lista adresów URL, które nie mogą korzystać z proxy\\nMożesz użyć symboli wieloznacznych z *\\nna przykład 192.168.1.* example.com (oddzielone spacjami)\nproxy_change_title=Zmień serwer proxy\nchange_proxy=Zmień serwer proxy\nproxy_no=Nie używaj proxy\nproxy_system=Systemowy serwer proxy\nproxy_manual=Ręczne ustawienia proxy\nproxy_pac=Automatyczna konfiguracja serwera proxy\nproxy_pac_url=Automatyczna konfiguracja URL serwera proxy\naddress=Adres\nport=Port\naddress_and_port=Adres i port\nuse_authentication=Użyj uwierzytelniania\nwarning_you_may_have_to_restart_the_download_later=Konieczne może być późniejsze ponowne uruchomienie pobierania\\!\nedit_download_title=Edytuj zadanie pobierania\nedit_download_update_from_download_page=Aktualizuj ze strony pobierania\nedit_download_update_from_download_page_description=Po otwarciu tego okna można przejść do strony pobierania i kliknąć przycisk pobierania. Aplikacja przechwyci i zaktualizuje nowe dane uwierzytelniające pobierania, aby można je było zapisać.\nedit_download_saved_download_item_size_not_match=Pobrany plik ma rozmiar {{currentSize}}, który nie pasuje do nowego rozmiaru {{newSize}}.\ntranslators_page_thanks=Z wdzięcznością dla tych, którzy pomogli przetłumaczyć ten projekt ❤️\ntranslators=Tłumacze\nlanguage=Język\ntranslators_contribute_title=Popraw tłumaczenie\ntranslators_contribute_description=Chcesz pomóc ulepszyć ten projekt? Jeśli Twojego języka nie ma na liście lub wymaga on pewnych poprawek, możesz pomóc przy tłumaczeniu\\!\ncontribute=Udziel się\nmeet_the_translators=Poznaj tłumaczy\nlocalized_by_translators=Zlokalizowane przez tłumaczy\nconfirm_exit=Potwierdź wyjście\nconfirm_exit_description=Czy na pewno chcesz wyjść z AB Download Manager?\\nAktywne pobrania/kolejki zostaną zatrzymane\\!\nupdate=Aktualizacje\nupdate_updater=Aktualizator\nupdate_available=Dostępna aktualizacja\nupdate_error=Błąd aktualizacji\nupdate_available_suggest_to_to_update=Możesz zaktualizować do najnowszej wersji, aby cieszyć się nowymi funkcjami, ulepszeniami i poprawkami wydajności.\nupdate_release_notes=Informacje o wydaniu\nupdate_check_for_update=Sprawdź aktualizacje\nupdate_checking_for_update=Sprawdzanie dostępności aktualizacji\nupdate_no_update=Używasz najnowszej wersji\nupdate_check_error=Błąd podczas sprawdzania aktualizacji\nupdate_app_updated_to_version_n=Aplikacja została zaktualizowana do wersji {{version}}\ncreate_desktop_entry=Utwórz wpis na pulpicie\nshutdown_alert=Alarm Wyłączania\nsystem_shutdown_soon=System zostanie wkrótce wyłączony\\!\nsystem_shutdown_failed=Wyłączanie systemu nie powiodło się\\!\nsystem_shutdown_soon_description=System zostanie wkrótce wyłączony. Jeśli nadal korzystasz z komputera, zapisz swoją pracę lub anuluj wyłączenie.\nsystem_shutdown_reason_queue_completed=Wszystkie pobierania w kolejce są zakończone.\nsystem_shutdown_reason_queue_end_time_reached=Osiągnięto zaplanowany czas zakończenia kolejki pobierania.\nsystem_shutdown_download_finished=Pobieranie zakończone.\nshutdown_now=Wyłącz teraz\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Najpierw utwórz lub wybierz nowy element\\!\nsettings_per_host_settings_host=Serwer\nsettings_per_host_settings_host_description=Te ustawienia zostaną przypisane do pobrań, których nazwa hosta pasuje do podanego wzorca. Obsługiwane są proste symbole (np. *), jak w wyrażeniach regularnych (przykładowo\\: example.com, *.example.com — użyj tylko jednego).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sortuj według\nwelcome=Witaj\nnew_folder=Nowy folder\nskip=Pomiń\nlets_go=Zacznijmy\nnext=Dalej\nselect_all=Zaznacz wszystko\nselect_inside=Zaznaczenie wewnątrz\nselect_invert=Zaznaczenie odwrócone\nopen_settings=Otwórz ustawienia\nback=Wstecz\nservice_is_running=Usługa jest uruchomiona\ninitial_setup_description=Przygotujmy wszystko\ninitial_setup_notice=Możesz zmienić te ustawienia w dowolnym momencie\npermission_granted=Uprawnienia przyznane\npermission_not_granted=Nie przyznano uprawnień\npermissions=Uprawnienia\ngive_permission=Przyznaj uprawnienia\ngive_storage_permission=Zezwól na dostęp do pamięci\nstorage_roots=Storage Roots\npermissions_initial_title=Konfiguracja uprawnień\npermissions_initial_description=Aby działać prawidłowo, aplikacja potrzebuje kilku uprawnień. Na następnym ekranie zobaczysz, do czego każde uprawnienie jest używane i możesz zdecydować, na które z nich chcesz zezwolić i które pominąć.\npermissions_done_title=Wszystko gotowe\npermissions_done_description=Wszystkie wymagane uprawnienia zostały przyznane i aplikacja jest gotowa do działania.\npermissions_manage_storage_title=Zarządzaj dostępem do pamięci\npermissions_manage_storage_reason=To uprawnienie pozwala aplikacji na zmianę folderu pobierania, dokładniejsze wykrywanie duplikatów pobranych plików i włączanie dodatkowych funkcji. Jest ono opcjonalne, ale zalecane.\npermission_read_write_external_storage_title=Odczyt i zapis pamięci\npermission_read_write_external_storage_reason=To uprawnienie pozwala aplikacji na zapisywanie i zarządzanie pobranymi plikami, zmianę lokacji pobierania i lepsze wykrywanie duplikatów.\npermissions_post_notification_title=Wysyłanie powiadomień\npermissions_post_notification_reason=Aplikacja musi działać w tle, aby zarządzać pobieraniem. Powiadomienia są używane do informowania Cię o stanie pobierania i zezwalania na operację w tle.\npermissions_ignore_battery_optimization_title=Ignoruj optymalizacje baterii\npermissions_ignore_battery_optimization_reason=Niektóre urządzenia agresywnie ograniczają aktywność aplikacji w tle, by zredukować zużycie baterii, co może zatrzymać pobieranie, gdy aplikacja nie jest otwarta. Opcjonalnie możesz wykluczyć tę aplikację z optymalizacji baterii, aby upewnić się, że pobieranie będzie kontynuowane po jej zamknięciu\nopen_in_browser=Otwórz w przeglądarce\nbrowser=Przeglądarka\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Otwórz w nowej karcie\nbrowser_open_in_new_background_tab=Otwórz w nowej karcie w tle\nbrowser_no_tab_open=Żadne karty nie zostały otworzone\nbrowser_tabs=Karty\nbrowser_paste_and_go=Wklej i przejdź\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/pt_BR.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Categorizar automaticamente os downloads\nconfirm_auto_categorize_downloads_description=Qualquer item não categorizado será automaticamente adicionado à sua categoria relacionada.\nconfirm_reset_to_default_categories_title=Redefinir para as categorias padrão\nconfirm_reset_to_default_categories_description=Isso REMOVERÁ todas as categorias e trará de volta as categorias padrão\\!\nconfirm_delete_download_items_title=Confirmar exclusão\nconfirm_delete_download_items_description=Tem certeza de que deseja excluir {{count}} itens?\nconfirm_delete_download_unfinished_items_description=Tem certeza que deseja excluir {{count}} downloads não concluídos?\nconfirm_delete_download_finished_and_unfinished_items_description=Tem certeza que deseja excluir {{finishedCount}} downloads concluídos e {{unfinishedCount}} não concluídos?\nalso_delete_file_from_disk=Excluir também o arquivo do disco\nconfirm_delete_category_item_title=Removendo a categoria {{name}}\nconfirm_delete_category_item_description=Tem certeza que deseja excluir a categoria \"{{value}}\"?\nyour_download_will_not_be_deleted=Seus downloads não serão deletados\ndrag_the_file_to_another_app=Arraste o arquivo para outro aplicativo\ndrop_link_or_file_here=Solte o link ou arquivo aqui.\nnothing_will_be_imported=Nada será importado\nn_links_will_be_imported={count}} links serão importados\nn_items_selected={{count}} itens selecionados\nwindow_close=Fechar\nwindow_minimize=Minimizar\nwindow_maximize=Maximizar\nwindow_restore=Restaurar\ndelete=Excluir\nremove=Remover\ncancel=Cancelar\nclose=Fechar\nmenu=Menu\nmore_options=Mais opções\nok=Ok\nadd=Adicionar\npaste=Colar\nchange=Mudança\nedit=Editar\nchange_anyway=Mudar mesmo assim\ndownload=Download\nrefresh=Atualizar\nsettings=Configurações\non_completion=Ao concluir\nunknown=Desconhecido\nunknown_error=Erro desconhecido\ndownload_item_not_found=Download do item não encontrado\nname=Nome\ndownload_link=Link de download\nnot_finished=Não finalizado\nall=Categorias\nfinished=Concluído\nUnfinished=Inacabado\ncanceled=Cancelado\nerror=Erro\npaused=Pausado\ndownloading=Baixando\nadded=Adicionado\nidle=INATIVO\npreparing_file=Preparando Arquivo\ncreating_file=Criando Arquivo\nresuming=Continuar\nretrying=Repetindo\nlist_is_empty=A lista está vazia\\!\nsearch_in_the_list=Pesquisar na Lista\nsearch=Pesquisar\nclear=Limpar\ngeneral=Geral\nenabled=Ativado\ndisabled=Desativado\ndefault=Padrão\nfile=Arquivo\ntasks=Tarefas\ntools=Ferramentas\nhelp=Ajuda\nsystem=Sistema\nall_missing_files=Todos os arquivos ausentes\nall_finished=Todos concluídos\nall_unfinished=Todos não concluídos\nentire_list=Lista Completa\ndownload_browser_integration=Baixar integração do navegador\nexit=Sair\nshow_downloads=Mostrar downloads\nnew_download=Novo download\nstop_all=Parar Tudo\nimport_from_clipboard=Importar da área de transferência\nbatch_download=Download em lote\nopen=Abrir\nshare=Compartilhar\nopen_file=Abrir Arquivo\nopen_folder=Abrir Pasta\nresume=Retomar\npause=Pausar\nrestart_download=Reiniciar download\ncopy=Copiar\ncopy_link=Copiar link\ncopy_as_curl=Copiar como cURL\nshow_properties=Mostrar Propriedades\nmove_to_queue=Mover para a fila\nmove_to_this_queue=Mover para esta fila\nmove_to_category=Mover Para a Categoria\nmove_to_this_category=Mover para esta categoria\ncategories=Categorias\nadd_category=Adicionar Categoria\nedit_category=Editar Categoria\ndelete_category=Excluir Categoria\ncategory_name=Nome da Categoria\ncategory_download_location=Local de download da categoria\ncategory_download_location_description=Quando esta categoria for escolhida em \"Adicionar download\", use este diretório como \"Local de download\"\ncategory_file_types=Tipos de arquivo de categoria\ncategory_file_types_description=Coloque automaticamente esses tipos de arquivo nesta categoria. (quando você adicionar um novo download)\\nSepare as extensões de arquivo com espaço (ext1 ext2 ...)\ncategory_url_patterns=Padrões de URL\ncategory_url_patterns_description=Coloque automaticamente o download dessas URLs nesta categoria. (quando você adicionar um novo download)\\nSepare as URLs com espaço; você também pode usar * como caractere curinga.\nauto_categorize_downloads=Categorizar automaticamente os downloads\nrestore_defaults=Restaurar Padrões\nabout=Sobre\nversion_n=Versão {{value}}\ndeveloped_with_love_for_you=Desenvolvido com ❤️ para você\ndonate=Doar\nvisit_the_project_website=Visite o site do projeto\nthis_is_a_free_and_open_source_software=Este é um software livre e de código aberto\nview_the_source_code=Veja o código-fonte\nthird_party_libraries=Bibliotecas de Terceiros\npowered_by_open_source_software=Desenvolvido por Open Source Software\nview_the_open_source_licenses=Ver as licenças da Open-Source\nsupport_and_community=Suporte e comunidade\ntelegram=Telegram\nchannel=Canal\ngroup=Grupo\nadd_download=Adicionar download\nadd_multi_download_page_header=Selecione os itens que você deseja selecionar para baixar\nsave_to=Salvar para\nwhere_should_each_item_saved=Onde cada item deve ser salvo?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Existem vários itens\\! Por favor, selecione uma maneira que queira salvá-los\neach_item_on_its_own_category=Cada item em sua própria categoria\neach_item_on_its_own_category_description=Cada item será colocado em uma categoria que tem esse tipo de arquivo\nall_items_in_one_category=Todos os itens em uma Categoria\nall_items_in_one_category_description=Todos os arquivos serão salvos no local da categoria selecionada\nall_items_in_one_Location=Todos os itens em um só Local\nall_items_in_one_Location_description=Todos os itens serão salvos no diretório selecionado\nunselected_all_items_in_specific_location_description=Todos os arquivos serão salvos no local da categoria selecionada\nno_category_selected=Nenhuma categoria selecionada\nno_categories_found=Nenhuma categoria encontrada\ndownload_location=Local de download\nlocation=Localização\nselect_queue=Selecionar fila\nwithout_queue=Sem fila\nuse_category=Usar Categoria\ncant_write_to_this_folder=Não foi possível gravar nesta pasta\nfile_name_already_exists=Nome de arquivo já existe\ndownload_already_exists=O download já existe\ninvalid_file_name=Nome de arquivo inválido\nshow_solutions=Mostrar soluções...\nchange_solution=Alterar solução\nselect_a_solution=Selecione uma solução\nselect_download_strategy_description=O link que você forneceu já está na lista de downloads, por favor, especifique o que você quer fazer\ndownload_strategy_add_a_numbered_file=Adicionar um arquivo numerado\ndownload_strategy_add_a_numbered_file_description=Adicionar um índice após o final do nome do arquivo de download\ndownload_strategy_override_existing_file=Sobrescrever o arquivo existente\ndownload_strategy_override_existing_file_description=Remover o download existente e escrever nesse arquivo\ndownload_strategy_update_download_link=Atualizar download existente\ndownload_strategy_update_download_link_description=Atualizar o link de download existente e suas credenciais\ndownload_strategy_show_downloaded_file=Mostrar arquivo baixado\ndownload_strategy_show_downloaded_file_description=Mostrar o item de download já existente, assim você pode pressionar em continuar ou abri-lo\nbatch_download_link_help=Digite um link que contém caracteres curinga (use *)\ninvalid_url=URL Inválida\nlist_is_too_large_maximum_n_items_allowed=A lista é muito grande\\! Máximo de {{count}} itens permitidos\nenter_range=Insira o intervalo\nrange_from=De\nrange_to=Para\nbatch_download_wildcard_length=Comprimento do curinga\nfirst_link=Primeiro Link\nlast_link=Último link\nopen_source_software_used_in_this_app=Software de código aberto usado neste aplicativo\nlinks=Links\nwebsite=Site\ndevelopers=Desenvolvedores\nsource_code=Código Fonte\nlicense=Licença\nno_license_found=Nenhuma licença encontrada\norganization=Organização\nadd_new_queue=Adicionar nova fila\nqueue_name=Nome da fila\nqueues=Filas\nstop_queue=Parar fila\nstart_queue=Iniciar fila\nclear_queue_items=Fila vazia\nconfig=Configuração\nitems=Itens\nmove_down=Mover para baixo\nmove_up=Mover para cima\nremove_queue=Remover fila\nqueue_name_help=Especifique um nome para esta fila\nqueue_name_describe=Nome da fila é {{value}}\nqueue_max_concurrent_download=Download máximo simultâneo\nqueue_max_concurrent_download_description=Download máximo para esta fila\nqueue_automatic_stop=Parada automática\nqueue_automatic_stop_description=Parar automaticamente a fila quando não houver nenhum item nela\nqueue_scheduler=Agendador\nqueue_enable_scheduler=Ativar o agendador\nqueue_active_days=Dias ativos\nqueue_active_days_description=Em quais dias a funcionalidade dos agendadores funcionará?\nqueue_scheduler_enable_auto_start_time=Habilitar hora de início automático\nqueue_scheduler_auto_start_time=Hora de início automático\nqueue_scheduler_enable_auto_stop_time=Ativar tempo de parada automática\nqueue_scheduler_auto_stop_time=Tempo para parada automática\nqueue_shutdown_on_completion=Desligar o sistema ao concluir\nqueue_shutdown_on_completion_description=Automaticamente desliga o sistema quando esta fila é concluída. ou quando o horário de término programado é atingido.\nappearance=Aparência\ndownload_engine=Mecanismo de download\nbrowser_integration=Integração com Navegadores\nsettings_download_max_retries_count=Máximo de tentativas de download\nsettings_download_max_retries_count_description=Número máximo de vezes que o aplicativo tentará baixar novamente antes de desistir\nsettings_download_max_retries_count_describe_no_retries=Downloads com falha não serão repetidos\nsettings_download_max_retries_count_describe_n_retries=Os downloads com falha serão repetidos {{count}} vez(es)\nsettings_download_thread_count=Contagem de threads\nsettings_download_thread_count_description=Máximo de threads de download por item de download\nsettings_download_thread_count_describe=Um download pode ter até {{count}} threads\nsettings_download_thread_count_with_large_value_describe=Aviso\\: Definir uma contagem de threads alta pode aumentar o uso de recursos do sistema, reduzir o desempenho ou causar problemas de conexão com servidores. Use valores mais altos somente se você entender o impacto potencial em seu sistema e rede.\nsettings_use_server_last_modified_time=Usar o horário de última modificação do servidor\nsettings_use_server_last_modified_time_description=Ao baixar um arquivo, use o horário de última modificação do servidor para o arquivo local\nsettings_append_extension_to_incomplete_downloads=Acrescentar extensão aos downloads incompletos\nsettings_append_extension_to_incomplete_downloads_description=Acrescentar a extensão \".part\" para downloads incompletos. Isso ajuda a identificar downloads incompletos e impede a abertura acidental deles.\nsettings_use_sparse_file_allocation=Alocação de arquivo esparso\nsettings_use_sparse_file_allocation_description=Crie arquivos de forma mais eficiente, especialmente em SSDs, reduzindo gravações de dados desnecessárias. Isso pode acelerar o início dos downloads e reduzir o uso do disco. Se os downloads começarem lentamente ou você experimentar velocidades de download incomuns, considere desativar esta opção, pois pode não ser totalmente suportada em alguns dispositivos.\nsettings_ignore_ssl_certificates=Ignorar certificados SSL\nsettings_ignore_ssl_certificates_description=Desabilita a verificação de certificado SSL. Use somente se necessário, pois pode expor a sua conexão a riscos de segurança.\nsettings_global_speed_limiter=Limitador de Velocidade Global\nsettings_global_speed_limiter_description=Limite da velocidade de download global (0 significa ilimitado)\nsettings_show_average_speed=Mostrar Velocidade Média\nsettings_show_average_speed_description=Velocidade de download em média ou precisão\nsettings_use_category_by_default=Usar categoria como padrão\nsettings_use_category_by_default_description=Usar categoria por padrão ao adicionar um download.\nsettings_default_download_folder=Pasta de Download Padrão\nsettings_default_download_folder_description=Quando você adicionar um novo download, este local é usado por padrão\nsettings_default_download_folder_describe=\"{{folder}}\" será usada\nsettings_use_proxy=Usar Proxy\nsettings_use_proxy_description=Usar proxy para baixar arquivos\nsettings_use_proxy_describe_no_proxy=Nenhum Proxy será utilizado\nsettings_use_proxy_describe_system_proxy=Sistema de Proxy será usado\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" será utilizado\nsettings_use_proxy_describe_pac_proxy=arquivo pac \"{{value}}\" será usado\nsettings_track_deleted_files_on_disk=Rastrear arquivos excluídos no disco\nsettings_track_deleted_files_on_disk_description=Remover automaticamente os arquivos da lista quando eles são excluídos ou movidos da pasta de download.\nsettings_delete_partial_file_on_download_cancellation=Excluir o arquivo parcial ao cancelar download\nsettings_delete_partial_file_on_download_cancellation_description=Quando um download for cancelado, o arquivo parcialmente baixado será excluído do disco. Isso ajuda a manter sua pasta de download limpa e reduz o uso de espaço em disco desnecessário. No entanto, o download reiniciará do início da próxima vez que você iniciá-lo.\nsettings_default_user_agent=User Agent Padrão\nsettings_default_user_agent_description=Especifique a string padrão do User Agent para definir como as solicitações se identificam aos servidores. Isso pode ajudar a acessar conteúdo otimizado para determinados dispositivos ou a contornar limitações de download impostas por certos sites.\nsettings_download_size_unit=Unidade de tamanho de download\nsettings_download_size_unit_description=Unidade usada para exibir o tamanho do download\nsettings_download_speed_unit=Unidade de Velocidade de Download\nsettings_download_speed_unit_description=Unidade usada para exibir a velocidade de download\nsettings_theme=Tema\nsettings_theme_description=Selecione um tema para o App\nsettings_default_dark_theme=Tema escuro padrão\nsettings_default_dark_theme_description=Aplica-se quando o aplicativo segue o tema do sistema e o modo escuro está ativo\nsettings_default_light_theme=Tema claro padrão\nsettings_default_light_theme_description=Aplica-se quando o aplicativo segue o tema do sistema e o modo claro está ativo\nsettings_font=Fonte\nsettings_font_description=Alterar a fonte utilizada na interface do aplicativo. Algumas fontes podem não ser exibidas corretamente no aplicativo.\nsettings_ui_scale=Escala da Interface\nsettings_ui_scale_description=Ajustar o tamanho dos elementos da interface do aplicativo\nsettings_language=Idioma\nsettings_compact_top_bar=Barra Compacta Superior\nsettings_compact_top_bar_description=Mesclar a barra superior com a barra de título quando a janela principal tiver largura suficiente\nsettings_use_native_menu_bar=Usar barra de menu nativa\nsettings_use_native_menu_bar_description=Usar estilo da barra de menu padrão do sistema\nsettings_use_relative_date_time=Usar data/hora relativa\nsettings_use_relative_date_time_description=Usar formato de data/hora relativa para datas no aplicativo (por exemplo, \"2 dias atrás\" em vez da data/hora exata)\nsettings_show_icon_labels=Mostrar Marcadores de Ícones\nsettings_show_icon_labels_description=Mostrar rótulos abaixo dos ícones quando possível (como ações na barra de ferramentas iniciais)\nsettings_use_system_tray=Usar bandeja do sistema\nsettings_use_system_tray_description=Mostrar ícone da bandeja do sistema quando o aplicativo estiver em execução\nsettings_start_on_boot=Executar ao iniciar\nsettings_start_on_boot_description=Iniciar automaticamente aplicativo em logins de usuário\nsettings_notification_sound=Som da Notificação\nsettings_notification_sound_description=Tocar som em uma nova notificação\nsettings_browser_integration=Integração com Navegadores\nsettings_browser_integration_description=Aceitar downloads dos navegadores\nsettings_browser_integration_server_port=Porta do Servidor\nsettings_browser_integration_server_port_description=Porta para integração do navegador\nsettings_browser_integration_server_port_describe=O app ouvirá a porta {{port}}\nsettings_dynamic_part_creation=Criação de parte dinâmica\nsettings_dynamic_part_creation_description=Quando uma parte terminar crie outra parte dividindo outras partes para melhorar a velocidade de download\nsettings_show_completion_dialog=Mostrar janela de conclusão do Download\nsettings_show_completion_dialog_description=Mostrar automaticamente o diálogo \"Download Completo\" quando um download terminar.\nsettings_show_download_progress_dialog=Mostrar janela de conclusão do Download\nsettings_show_download_progress_dialog_description=Mostrar automaticamente o diálogo \"Download Completo\" quando um download terminar.\nsettings_per_host_settings=Configurações Por Host\nsettings_per_host_settings_descriptions=Essas configurações serão aplicadas automaticamente a qualquer novo download que corresponda ao host especificado.\nsettings_download_max_concurrent_downloads=Máximo de downloads simultâneos\nsettings_download_max_concurrent_downloads_description=O número máximo de arquivos que podem ser baixados ao mesmo tempo (downloads gerenciados por filas não são contados; defina como 0 para ilimitado)\ndownload_item_settings_speed_limit=Limite de Velocidade\ndownload_item_settings_speed_limit_description=Limitar velocidade de download deste item\ndownload_item_settings_show_download_completion_dialog=Mostrar janela de conclusão do Download\ndownload_item_settings_show_download_completion_dialog_description=Mostrar automaticamente o diálogo \"Download Completo\" quando um download terminar.\ndownload_item_settings_shutdown_on_completion=Desligar o sistema ao concluir\ndownload_item_settings_shutdown_on_completion_description=Automaticamente desliga o sistema quando este download terminar.\ndownload_item_settings_thread_count=Contagem de Thread\ndownload_item_settings_thread_count_description=Quantas threads foram usadas para baixar este item de download (0 para padrão)?\ndownload_item_settings_thread_count_describe={{count}} threads para este download\ndownload_item_settings_username_description=Forneça um nome de usuário se o link for um recurso protegido\ndownload_item_settings_password_description=Forneça uma senha se o link for um recurso protegido\ndownload_item_settings_download_page=Página de Download\ndownload_item_settings_download_page_description=A página da web onde este download foi iniciado\ndownload_item_settings_file_checksum=Checksum do arquivo\ndownload_item_settings_file_checksum_description=Uma string hash que pode ser usada para verificar se o arquivo foi baixado corretamente\ndownload_item_settings_user_agent=Agente do usuário\ndownload_item_settings_user_agent_description=Agente do usuário personalizado para este item (deixe vazio para usar o padrão)\nfile_checksum=Checksum do arquivo\nfile_checksum_page=Verificador de checksum do arquivo\nfile_checksum_page_file_checksum_default_algorithm=Algoritmo Padrão\nfile_checksum_page_file_checksum_default_algorithm_help=O algoritmo padrão usado para calcular as somas de verificação de arquivo quando elas não são fornecidas.\nstart=Iniciar\ncalculated_checksum=Checksum Calculado\nsaved_checksum=Checksum salvo\nchecksum_algorithm=Algoritmo\nfile_not_found=Arquivo não encontrado\ndownload_not_finished=Download não concluído\ndone=Concluído\nwaiting=Aguardando\nmatches=Corresponde\nnot_matches=Não corresponde\ncopy_to_clipboard=Copiar Para Área de Transferência\nusername=Nome de Usuário\npassword=Senha\naverage_speed=Velocidade Média\nexact_speed=Velocidade Exata\nunlimited=Ilimitado\nuse_global_settings=Utilizar Definições Globais\ncant_run_browser_integration=Não é possível executar a integração do navegador\ncant_open_file=Impossível Abrir o Arquivo\ncant_open_folder=Impossível Abrir Pasta\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} anos\nrelative_time_long_months={{months}} meses\nrelative_time_long_days={{days}} dias\nrelative_time_long_hours={{hours}} horas\nrelative_time_long_minutes={{minutes}} minutos\nrelative_time_long_seconds={{seconds}} segundos\nrelative_time_short_years={{years}} a\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} d\nrelative_time_short_hours={{hours}} hr\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} seg\nrelative_time_left={{time}} restantes\nrelative_time_ago={{time}} atrás\nauto=Auto\nunspecified=Não especificado\ncustom=Personalizado\nicon=Ícone\nauthor=Autor\nlink=Link\nsize=Tamanho\nstatus=Status\nparts_info_downloaded_size=Baixado\nparts_info_total_size=Total\nspeed=Velocidade\ntime_left=Tempo Restante\ndate_added=Adicionado na Data\ninfo=Info\ndownload_page_downloaded_size=Baixado\ndownload_page_download_completed=Download concluído\nresume_support=Suporte para continuação\nyes=Sim\nno=Não\nparts_info=Informações das partes\ndisconnected=Desconectado\nreceiving_data=Recebendo Dados\nconnecting=Enviar GET\nwarning=Aviso\nunsupported_resume_warning=Este download não oferece suporte a continuação\\! Você pode ter que reiniciá-lo mais tarde na lista de downloads\nstop_anyway=Parar Assim Mesmo\ncustomize_columns=Personalizar Colunas\nreset=Redefinir\nmonday=Segunda-feira\ntuesday=Terça-feira\nwednesday=Quarta-feira\nthursday=Quinta-feira\nfriday=Sexta-feira\nsaturday=Sábado\nsunday=Domingo\nproxy_open_system_proxy_settings=Abrir Configurações do Proxy do Sistema\nproxy_type=Tipo de proxy\nproxy_do_not_use_proxy_for=Não usar proxy para\nproxy_do_not_use_proxy_for_description=Uma lista de urls que não podem ser proxy\\nVocê pode usar curinga com *\\npor exemplo 192.168.1.* exemplo.com (separado por espaço)\nproxy_change_title=Mudar Proxy\nchange_proxy=Mudar Proxy\nproxy_no=Sem Proxy\nproxy_system=Proxy do sistema\nproxy_manual=Proxy Manual\nproxy_pac=Configuração automática de proxy\nproxy_pac_url=URL de configuração automática de proxy\naddress=Endereço de IP\nport=Porta\naddress_and_port=Endereço de IP e Porta\nuse_authentication=Usar Autenticação\nwarning_you_may_have_to_restart_the_download_later=Você talvez precise reiniciar o download mais tarde\\!\nedit_download_title=Editar Download\nedit_download_update_from_download_page=Atualizar a partir da página de download\nedit_download_update_from_download_page_description=Quando esta janela estiver aberta, você pode acessar a página de download e clicar no botão de download. O aplicativo capturará e atualizará as novas credenciais de download para que você possa salvá-las.\nedit_download_saved_download_item_size_not_match=O item de download salvo possui um tamanho de {{currentSize}}, que não corresponde ao novo tamanho de {{newSize}}.\ntranslators_page_thanks=Com gratidão a todos que ajudaram a traduzir este projeto ❤️\ntranslators=Tradutores\nlanguage=Idioma\ntranslators_contribute_title=Melhorar Traduções\ntranslators_contribute_description=Quer ajudar a melhorar este projeto? Se o seu idioma não estiver listado ou precisar de ajustes, você pode contribuir com suas traduções e torná-lo ainda melhor\\!\ncontribute=Contribuir\nmeet_the_translators=Conheça os Tradutores\nlocalized_by_translators=Localizado por Tradutores\nconfirm_exit=Deseja Sair?\nconfirm_exit_description=Tem certeza que deseja sair do AB Download Manager?\\nDownloads ativos/filas serão interrompidos\\!\nupdate=Atualizar\nupdate_updater=Atualizador\nupdate_available=Atualização disponível\nupdate_error=Erro na atualização\nupdate_available_suggest_to_to_update=Você pode atualizar para a versão mais recente para desfrutar de novos recursos, aprimoramentos e melhorias de desempenho.\nupdate_release_notes=Notas de lançamento\nupdate_check_for_update=Verificar Atualizações\nupdate_checking_for_update=Verificando por atualizações\nupdate_no_update=Você está na última versão\nupdate_check_error=Erro durante a verificação de atualizações\nupdate_app_updated_to_version_n=App atualizado para a versão {{version}}\ncreate_desktop_entry=Criar entrada na Área de Trabalho\nshutdown_alert=Alerta de desligamento\nsystem_shutdown_soon=O sistema irá desligar em breve\\!\nsystem_shutdown_failed=Erro ao desligar o sistema\\!\nsystem_shutdown_soon_description=O sistema será desligado em breve. Se você ainda estiver usando o computador, salve o trabalho ou cancele o desligamento.\nsystem_shutdown_reason_queue_completed=Todos os downloads na fila estão completos.\nsystem_shutdown_reason_queue_end_time_reached=Hora de término programada para a fila de download atingida.\nsystem_shutdown_download_finished=Download concluído.\nshutdown_now=Desligar agora\nsettings_per_host_settings_new_host=<Nova Hospedagem>\nsettings_per_host_settings_not_selected=Crie ou selecione um novo item primeiro\\!\nsettings_per_host_settings_host=Hospedar\nsettings_per_host_settings_host_description=Essas configurações serão aplicadas a downloads correspondentes a este nome do host. Wildcards ( *) são suportados (por exemplo, exemplo.com, *.example.com - use apenas um).\nsettings_browser_in_launcher=Ícone do navegador na Launcher\nsettings_browser_in_launcher_description=Exibir ou ocultar o ícone do navegador no launcher (lista de aplicativos).\nsort_by=Ordenar por\nwelcome=Bem-vindo\nnew_folder=Nova pasta\nskip=Pular\nlets_go=Vamos lá\nnext=Próximo\nselect_all=Selecionar todos\nselect_inside=Selecionar dentro\nselect_invert=Inverter seleção\nopen_settings=Abrir Configurações\nback=Voltar\nservice_is_running=O serviço está em execução\ninitial_setup_description=Vamos configurar as coisas\ninitial_setup_notice=Você pode alterar essas configurações a qualquer momento depois\npermission_granted=Permissão concedida\npermission_not_granted=Permissão não concedida\npermissions=Permissões\ngive_permission=Permitir permissão\ngive_storage_permission=Permitir acesso ao armazenamento\nstorage_roots=Raízes de armazenamento\npermissions_initial_title=Vamos configurar as coisas\npermissions_initial_description=Para funcionar corretamente, o aplicativo precisa de algumas permissões. Na próxima tela, você verá para que cada permissão é usada e você poderá decidir quais permitir ou ignorar.\npermissions_done_title=Tudo pronto\npermissions_done_description=Está tudo pronto. Todas as permissões necessárias foram concedidas e o aplicativo está pronto para ser usado.\npermissions_manage_storage_title=Gerenciar acesso ao armazenamento\npermissions_manage_storage_reason=Esta permissão permite ao aplicativo alterar a pasta de download, detectar downloads duplicados com mais precisão e habilitar alguns recursos extras. Ele é opcional, mas recomendado para a melhor experiência.\npermission_read_write_external_storage_title=Leitura e escrita no armazenamento\npermission_read_write_external_storage_reason=Esta permissão permite que o aplicativo salve e gerencie arquivos baixados, altere o local de download e melhore a detecção de download duplicados.\npermissions_post_notification_title=Acesso a notificações\npermissions_post_notification_reason=O aplicativo precisa ser executado em segundo plano para gerenciar downloads. As notificações são usadas para mantê-lo informado e permitir operações em segundo plano.\npermissions_ignore_battery_optimization_title=Ignorar otimizações da bateria\npermissions_ignore_battery_optimization_reason=Alguns dispositivos limitam agressivamente a atividade em segundo plano para economizar bateria, que pode pausar ou interromper os downloads quando o aplicativo não estiver aberto. Uma opção é excluir o aplicativo da otimização de bateria para garantir que os downloads não sejam interrompidos\nopen_in_browser=Abrir no navegador\nbrowser=Navegador\nbrowser_new_tab=Nova aba\nbrowser_close_tab=Fechar aba\nbrowser_open_in_new_tab=Abrir em nova aba\nbrowser_open_in_new_background_tab=Abrir em nova aba em segundo plano\nbrowser_no_tab_open=Nenhuma aba está aberta\nbrowser_tabs=Abas\nbrowser_paste_and_go=Colar e ir\nbrowser_bookmarks=Marcadores\nbrowser_add_bookmark=Adicionar marcador\nbrowser_edit_bookmark=Editar marcador\nbrowser_add_to_bookmarks=Adicionar aos marcadores\nbrowser_remove_from_bookmarks=Remover dos marcadores\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/ru_RU.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Автоматическая сортировка загрузок по категориям\nconfirm_auto_categorize_downloads_description=Любой неотсортированный элемент будет автоматически добавлен в соответствующую категорию.\nconfirm_reset_to_default_categories_title=Сброс к Категориям По умолчанию\nconfirm_reset_to_default_categories_description=Это приведет к удалению всех категорий и вернёт категории по умолчанию\\!\nconfirm_delete_download_items_title=Подтвердить удаление\nconfirm_delete_download_items_description=Вы уверены, что хотите удалить {{count}} элементов?\nconfirm_delete_download_unfinished_items_description=Вы уверены, что хотите удалить {{count}} незаконченных загрузок?\nconfirm_delete_download_finished_and_unfinished_items_description=Вы уверены, что хотите удалить {{finishedCount}} завершённых и {{unfinishedCount}} незаконченных загрузок?\nalso_delete_file_from_disk=Также удалить файл с диска\nconfirm_delete_category_item_title=Удаление {{name}} категории\nconfirm_delete_category_item_description=Вы уверены, что хотите удалить \"{{value}}\" Категорию?\nyour_download_will_not_be_deleted=Ваши загрузки не будут удалены\ndrag_the_file_to_another_app=Перетащить файл в другое приложение\ndrop_link_or_file_here=Перетащите ссылку или файл сюда.\nnothing_will_be_imported=Ничего не будет импортировано\nn_links_will_be_imported={{count}} ссылок будет импортировано\nn_items_selected={{count}} элементов выбрано\nwindow_close=Закрыть\nwindow_minimize=Свернуть\nwindow_maximize=Развернуть\nwindow_restore=Восстановить\ndelete=Удалить\nremove=Удалить\ncancel=Отмена\nclose=Закрыть\nmenu=Меню\nmore_options=Больше Опций\nok=ОK\nadd=Добавить\npaste=Вставить\nchange=Изменить\nedit=Редактировать\nchange_anyway=Всё равно Изменить\ndownload=Загрузить\nrefresh=Обновить\nsettings=Настройки\non_completion=По Завершении\nunknown=Неизвестно\nunknown_error=Неизвестная Ошибка\ndownload_item_not_found=Элемент для загрузки не найден\nname=Имя\ndownload_link=Ссылка для загрузки\nnot_finished=Не завершено\nall=Все\nfinished=Завершено\nUnfinished=Незаконченно\ncanceled=Отменено\nerror=Ошибка\npaused=Приостановлено\ndownloading=Загружается\nadded=Добавлено\nidle=НЕАКТИВНО\npreparing_file=Подготовка файла\ncreating_file=Создание файла\nresuming=Возобновление\nretrying=Повтор\nlist_is_empty=Список пуст\\!\nsearch_in_the_list=Поиск по списку\nsearch=Поиск\nclear=Очистить\ngeneral=Основные\nenabled=Включено\ndisabled=Отключено\ndefault=По умолчанию\nfile=Файл\ntasks=Задания\ntools=Инструменты\nhelp=Справка\nsystem=Системный\nall_missing_files=Все Отсутствующие\nall_finished=Все Завершённые\nall_unfinished=Все Незаконченные\nentire_list=Весь Список\ndownload_browser_integration=Установить Расширение для Браузера\nexit=Выход\nshow_downloads=Показать Загрузки\nnew_download=Новая Загрузка\nstop_all=Остановить Все\nimport_from_clipboard=Вставить из Буфера обмена\nbatch_download=Пакетная Загрузка\nopen=Открыть\nshare=Поделиться\nopen_file=Открыть Файл\nopen_folder=Открыть Папку\nresume=Продолжить\npause=Приостановить\nrestart_download=Перезапустить Загрузку\ncopy=Скопировать\ncopy_link=Копировать ссылку\ncopy_as_curl=Скопировать для cURL\nshow_properties=Показать Свойства\nmove_to_queue=Переместить в Очередь\nmove_to_this_queue=Переместить в эту Очередь\nmove_to_category=Переместить в Категорию\nmove_to_this_category=Переместить в эту категорию\ncategories=Категории\nadd_category=Добавить Категорию\nedit_category=Редактировать Категорию\ndelete_category=Удалить Категорию\ncategory_name=Имя Категории\ncategory_download_location=Расположение Категории Загрузки\ncategory_download_location_description=Если эта категория выбрана в разделе \"Добавить Загрузку\", используйте этот каталог как \"Папка для Загрузки\"\ncategory_file_types=Типы файлов категории\ncategory_file_types_description=Автоматически помещайте эти типы файлов в эту категорию. (при добавлении новой загрузки)\\nРазделяйте расширения файлов пробелом (ext1 ext2 ...)\ncategory_url_patterns=Шаблоны URL\ncategory_url_patterns_description=Автоматически помещайте загрузки из этих URLs в эту категорию. (при добавлении новой загрузки)\\nРазделяйте URLs пробелом, вы также можете использовать * в качестве подстановочного знака\nauto_categorize_downloads=Автоматическая сортировка Загрузок по Категориям\nrestore_defaults=Восстановить Настройки По умолчанию\nabout=О программе\nversion_n=Версия {{value}}\ndeveloped_with_love_for_you=Разработано с ❤️ для вас\ndonate=Пожертвовать\nvisit_the_project_website=Посетить веб-сайт проекта\nthis_is_a_free_and_open_source_software=Это бесплатное программное обеспечение с Открытым Исходным кодом\nview_the_source_code=Посмотреть Исходный Код\nthird_party_libraries=Сторонние Библиотеки\npowered_by_open_source_software=На основе Open Source Software\nview_the_open_source_licenses=Просмотр лицензий с Открытым Исходным кодом\nsupport_and_community=Поддержка и Сообщество\ntelegram=Telegram\nchannel=Канал\ngroup=Группа\nadd_download=Добавить Загрузку\nadd_multi_download_page_header=Выберите элементы, которые вы хотите загрузить\nsave_to=Сохранить в\nwhere_should_each_item_saved=Где следует сохранять каждый элемент?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Здесь несколько элементов\\! пожалуйста, выберите способ их сохранения\neach_item_on_its_own_category=Каждый элемент в своей категории\neach_item_on_its_own_category_description=Каждый элемент будет помещен в категорию, которая имеет этот тип файла\nall_items_in_one_category=Все элементы в одной Категории\nall_items_in_one_category_description=Все файлы будут сохранены в выбранной категории\nall_items_in_one_Location=Все элементы в одной Папке\nall_items_in_one_Location_description=Все элементы будут сохранены в выбранном каталоге\nunselected_all_items_in_specific_location_description=Все файлы будут сохранены в папке выбранной категории\nno_category_selected=Категория не Выбрана\nno_categories_found=Категории Отсутствуют\ndownload_location=Папка для Загрузки\nlocation=Расположение\nselect_queue=Выбрать Очередь\nwithout_queue=Без Очереди\nuse_category=Использовать Категорию\ncant_write_to_this_folder=Невозможно записать в эту папку\nfile_name_already_exists=Имя файла уже существует\ndownload_already_exists=Загрузка уже существует\ninvalid_file_name=Неверное имя файла\nshow_solutions=Показать решения...\nchange_solution=Изменить\nselect_a_solution=Выбрать решение\nselect_download_strategy_description=Указанная вами ссылка уже есть в списках загрузок, пожалуйста, уточните, что вы хотите сделать\ndownload_strategy_add_a_numbered_file=Добавить пронумерованный файл\ndownload_strategy_add_a_numbered_file_description=Добавить индекс после окончания загрузки имени файла\ndownload_strategy_override_existing_file=Перезаписать существующий файл\ndownload_strategy_override_existing_file_description=Удалить существующую загрузку и запись об этом файле\ndownload_strategy_update_download_link=Обновить существующую загрузку\ndownload_strategy_update_download_link_description=Обновить существующую ссылку для загрузки и её учётные данные\ndownload_strategy_show_downloaded_file=Показать загруженный файл\ndownload_strategy_show_downloaded_file_description=Показать уже существующий элемент загрузки, чтобы вы могли нажать продолжить или открыть его\nbatch_download_link_help=Введите ссылку, содержащую подстановочные знаки (используйте *)\ninvalid_url=Неверный URL\nlist_is_too_large_maximum_n_items_allowed=Список слишком большой\\! Максимум {{count}} элементов разрешено\nenter_range=Введите диапазон\nrange_from=От\nrange_to=К\nbatch_download_wildcard_length=Длина подстановочного знака\nfirst_link=Первая Ссылка\nlast_link=Последняя Ссылка\nopen_source_software_used_in_this_app=В этом Приложении используется программное обеспечение с Открытым Исходным кодом\nlinks=Ссылки\nwebsite=Веб-сайт\ndevelopers=Разработчики\nsource_code=Исходный Код\nlicense=Лицензия\nno_license_found=Лицензия не найдена\norganization=Организация\nadd_new_queue=Добавить Новую Очередь\nqueue_name=Название Очереди\nqueues=Очереди\nstop_queue=Остановить Очередь\nstart_queue=Запустить Очередь\nclear_queue_items=Пустая Очередь\nconfig=Конфигурация\nitems=Элементы\nmove_down=Переместить вниз\nmove_up=Переместить вверх\nremove_queue=Удалить Очередь\nqueue_name_help=Укажите название для этой очереди\nqueue_name_describe=Название очереди {{value}}\nqueue_max_concurrent_download=Максимальная одновременная загрузка\nqueue_max_concurrent_download_description=Максимальная загрузка для этой очереди\nqueue_automatic_stop=Автоматическая остановка\nqueue_automatic_stop_description=Автоматическая остановка очереди, когда в ней нет элементов\nqueue_scheduler=Планировщик\nqueue_enable_scheduler=Включить Планировщик\nqueue_active_days=Активные Дни\nqueue_active_days_description=В какие дни работают планировщики?\nqueue_scheduler_enable_auto_start_time=Включить Таймер Авто Запуска\nqueue_scheduler_auto_start_time=Время Авто Запуска\nqueue_scheduler_enable_auto_stop_time=Включить Таймер Автоматической Остановки\nqueue_scheduler_auto_stop_time=Время Автоматической Остановки\nqueue_shutdown_on_completion=Выключить Систему По Завершении\nqueue_shutdown_on_completion_description=Автоматически выключить систему по завершении этой очереди или по достижении запланированного времени окончания.\nappearance=Вид\ndownload_engine=Модуль Загрузки\nbrowser_integration=Интеграция с Браузером\nsettings_download_max_retries_count=Максимальная Повторная Загрузка\nsettings_download_max_retries_count_description=Максимальное количество повторных попыток приложения завершить неудачную загрузку\nsettings_download_max_retries_count_describe_no_retries=Неудачные загрузки не будут повторены\nsettings_download_max_retries_count_describe_n_retries=Неудачные загрузки будут повторены {{count}} раз(а)\nsettings_download_thread_count=Количество Потоков\nsettings_download_thread_count_description=Максимальное количество потоков загрузки для каждого элемента загрузки\nsettings_download_thread_count_describe=Загрузка может содержать до {{count}} потоков\nsettings_download_thread_count_with_large_value_describe=Предупреждение\\: Установка большого количества потоков может увеличить использование системных ресурсов, снизить производительность или вызвать проблемы с подключением к серверам. Используйте более высокие значения только в том случае, если вы понимаете потенциальное влияние на вашу систему и сеть.\nsettings_use_server_last_modified_time=Использовать Время Последнего Изменения на Сервере\nsettings_use_server_last_modified_time_description=При загрузке файла используйте время последнего изменения локального файла на сервере\nsettings_append_extension_to_incomplete_downloads=Добавлять расширение к Незавершённым Загрузкам\nsettings_append_extension_to_incomplete_downloads_description=Добавлять расширение \".part\" к незавершённым загрузкам. Это помогает идентифицировать незаконченные загрузки и предотвращает случайное открытие неполных файлов.\nsettings_use_sparse_file_allocation=Разреженное Распределение Файлов\nsettings_use_sparse_file_allocation_description=Создавайте файлы более эффективно, особенно на SSDs, сокращая объем ненужной записи данных. Это может ускорить начало загрузки и снизить нагрузку на диск. Если загрузка начинается медленно или у вас наблюдается необычная скорость загрузки, подумайте о том, чтобы отключить эту опцию, поскольку на некоторых устройствах она может поддерживаться не полностью.\nsettings_ignore_ssl_certificates=Игнорировать SSL-сертификаты\nsettings_ignore_ssl_certificates_description=Отключает проверку SSL-сертификата. Используйте только при необходимости, так как это может привести к угрозе безопасности вашего соединения.\nsettings_global_speed_limiter=Глобальный Ограничитель Скорости\nsettings_global_speed_limiter_description=Глобальное ограничение скорости загрузки (0 означает неограниченную)\nsettings_show_average_speed=Показывать Среднюю Скорость\nsettings_show_average_speed_description=Скорость загрузки в среднем или точном значении\nsettings_use_category_by_default=Использовать категорию по умолчанию\nsettings_use_category_by_default_description=Использовать категорию по умолчанию при добавлении загрузки.\nsettings_default_download_folder=Папка Загрузки По умолчанию\nsettings_default_download_folder_description=При добавлении новой загрузки это расположение используется по умолчанию\nsettings_default_download_folder_describe=\"{{folder}}\" будет использоваться\nsettings_use_proxy=Использовать Прокси\nsettings_use_proxy_description=Использовать прокси для загрузки файлов\nsettings_use_proxy_describe_no_proxy=Прокси не будет использоваться\nsettings_use_proxy_describe_system_proxy=Будет использоваться системный Прокси\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" будет использоваться\nsettings_use_proxy_describe_pac_proxy=PAC-файл \"{{value}}\" будет использоваться\nsettings_track_deleted_files_on_disk=Отслеживать Удалённые Файлы На Диске\nsettings_track_deleted_files_on_disk_description=Автоматически очищать список файлов при их удалении или перемещении из каталога загрузки.\nsettings_delete_partial_file_on_download_cancellation=Удалить Частичный Файл при отмене Загрузки\nsettings_delete_partial_file_on_download_cancellation_description=Когда загрузка отменена, частично загруженный файл будет удален с диска. Это поможет очистить вашу папку для загрузки и сократить ненужное использование дискового пространства. Однако, при следующем запуске загрузка начнется с самого начала.\nsettings_default_user_agent=User-Agent по умолчанию\nsettings_default_user_agent_description=Укажите строку User-Agent по умолчанию, чтобы определить способ идентификации запросов к серверам. Это может помочь в получении доступа к контенту, оптимизированному для конкретных устройств, или в обходе ограничений на загрузку, установленных определёнными веб-сайтами.\nsettings_download_size_unit=Единица измерения Размера\nsettings_download_size_unit_description=Единица измерения, используемая для отображения размера загрузки\nsettings_download_speed_unit=Единица измерения Скорости\nsettings_download_speed_unit_description=Единица измерения, используемая для отображения скорости загрузки\nsettings_theme=Тема\nsettings_theme_description=Выберите тему для Приложения\nsettings_default_dark_theme=Тёмная Тема По умолчанию\nsettings_default_dark_theme_description=Применяется, когда приложение соответствует системной теме и активен тёмный режим\nsettings_default_light_theme=Светлая Тема По умолчанию\nsettings_default_light_theme_description=Применяется, когда приложение соответствует системной теме и активен светлый режим\nsettings_font=Шрифт\nsettings_font_description=Изменить шрифт, используемый в интерфейсе приложения. Некоторые шрифты могут отображаться некорректно.\nsettings_ui_scale=Масштаб Интерфейса\nsettings_ui_scale_description=Отрегулируйте размер элементов интерфейса приложения\nsettings_language=Язык\nsettings_compact_top_bar=Компактная Верхняя Панель\nsettings_compact_top_bar_description=Объединить верхнюю панель со строкой заголовка, когда главное окно имеет достаточную ширину\nsettings_use_native_menu_bar=Использовать Стандартную Панель Меню\nsettings_use_native_menu_bar_description=Использовать системный стиль строки меню по умолчанию\nsettings_use_relative_date_time=Использовать относительную дату/время\nsettings_use_relative_date_time_description=Использовать относительный формат даты/времени (например, \"2 дня назад\" вместо точной даты/времени)\nsettings_show_icon_labels=Отображать Надписи Значков\nsettings_show_icon_labels_description=По возможности отображать надписи под значками ( например, действия на главной панели инструментов )\nsettings_use_system_tray=Использовать System Tray\nsettings_use_system_tray_description=Отображать значок системного лотка при запуске приложения\nsettings_start_on_boot=Запуск при Загрузке\nsettings_start_on_boot_description=Автоматический запуск приложения при входе пользователя в систему\nsettings_notification_sound=Звук Уведомления\nsettings_notification_sound_description=Воспроизводить звук при новом уведомлении\nsettings_browser_integration=Интеграция с Браузером\nsettings_browser_integration_description=Принимать загрузки из браузеров\nsettings_browser_integration_server_port=Порт Сервера\nsettings_browser_integration_server_port_description=Порт для интеграции с браузером\nsettings_browser_integration_server_port_describe=Приложение будет прослушивать порт {{port}}\nsettings_dynamic_part_creation=Динамическая сегментация\nsettings_dynamic_part_creation_description=Когда одна часть завершена, формируется другая, разделяющая оставшиеся сегменты для увеличения скорости загрузки\nsettings_show_completion_dialog=Показать диалоговое окно Завершения Загрузки\nsettings_show_completion_dialog_description=Автоматически показывать диалоговое окно «Завершение Загрузки» при завершении загрузки.\nsettings_show_download_progress_dialog=Показывать диалоговое окно Прогресса Загрузки\nsettings_show_download_progress_dialog_description=Автоматически показывать диалоговое окно «Прогресс Загрузки» при запуске загрузки.\nsettings_per_host_settings=Настройки хоста\nsettings_per_host_settings_descriptions=Эти настройки будут автоматически применены к любой новой загрузке, соответствующей указанному хосту.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Ограничение Скорости\ndownload_item_settings_speed_limit_description=Ограничить скорость загрузки для этого элемента\ndownload_item_settings_show_download_completion_dialog=Показать диалоговое окно Завершения\ndownload_item_settings_show_download_completion_dialog_description=Автоматически показывать диалоговое окно «Завершение Загрузки» при завершении этой загрузки.\ndownload_item_settings_shutdown_on_completion=Выключить Систему По Завершении\ndownload_item_settings_shutdown_on_completion_description=Автоматически выключить систему по завершении загрузки.\ndownload_item_settings_thread_count=Количество потоков\ndownload_item_settings_thread_count_description=Сколько потоков использовать для загрузки этого элемента (0 по умолчанию)\ndownload_item_settings_thread_count_describe={{count}} потоков для этой загрузки\ndownload_item_settings_username_description=Введите имя пользователя, если ссылка защищена\ndownload_item_settings_password_description=Введите пароль, если ссылка защищена\ndownload_item_settings_download_page=Страница Загрузки\ndownload_item_settings_download_page_description=Веб-страница, на которой была запущена загрузка\ndownload_item_settings_file_checksum=Контрольная сумма\ndownload_item_settings_file_checksum_description=Хеш-строка, которая может использоваться для проверки правильности загрузки файла\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Пользовательский User-Agent для этого элемента (оставьте пустым, чтобы использовать значение по умолчанию)\nfile_checksum=Контрольная сумма Файла\nfile_checksum_page=Проверка Контрольной суммы Файла\nfile_checksum_page_file_checksum_default_algorithm=Алгоритм по умолчанию\nfile_checksum_page_file_checksum_default_algorithm_help=Алгоритм по умолчанию, используемый для расчёта контрольных сумм файлов, когда они не указаны.\nstart=Начать\ncalculated_checksum=Рассчитанная Контрольная сумма\nsaved_checksum=Сохранённая Контрольная сумма\nchecksum_algorithm=Алгоритм\nfile_not_found=Файл не найден\ndownload_not_finished=Загрузка не завершена\ndone=Готово\nwaiting=Ожидание\nmatches=Совпадает\nnot_matches=Не Совпадает\ncopy_to_clipboard=Копировать в Буфер обмена\nusername=Имя пользователя\npassword=Пароль\naverage_speed=Средняя Скорость\nexact_speed=Точная скорость\nunlimited=Неограниченно\nuse_global_settings=Использовать Глобальные Настройки\ncant_run_browser_integration=Невозможно запустить интеграцию с браузером\ncant_open_file=Невозможно Открыть Файл\ncant_open_folder=Невозможно Открыть Папку\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} годы\nrelative_time_long_months={{months}} месяцев\nrelative_time_long_days={{days}} дней\nrelative_time_long_hours={{hours}} часов\nrelative_time_long_minutes={{minutes}} минут\nrelative_time_long_seconds={{seconds}} секунд\nrelative_time_short_years={{years}} г\nrelative_time_short_months={{months}} M\nrelative_time_short_days={{days}} д\nrelative_time_short_hours={{hours}} ч\nrelative_time_short_minutes={{minutes}} мин\nrelative_time_short_seconds={{seconds}} сек\nrelative_time_left={{time}} осталось\nrelative_time_ago={{time}} назад\nauto=Авто\nunspecified=Не указано\ncustom=Пользовательский\nicon=Иконка\nauthor=Автор\nlink=Ссылка\nsize=Размер\nstatus=Статус\nparts_info_downloaded_size=Загружено\nparts_info_total_size=Всего\nspeed=Скорость\ntime_left=Оставшееся время\ndate_added=Дата добавления\ninfo=Информация\ndownload_page_downloaded_size=Загружено\ndownload_page_download_completed=Загрузка Завершена\nresume_support=Возобновить Поддержку\nyes=Да\nno=Нет\nparts_info=Информация о Частях\ndisconnected=Нет подключения\nreceiving_data=Получение Данных\nconnecting=Подключение\nwarning=Предупреждение\nunsupported_resume_warning=Эта загрузка не поддерживает возобновление\\! Возможно, вам придется ПЕРЕЗАПУСТИТЬ ее позже в Списке Загрузок\nstop_anyway=Остановить в Любом случае\ncustomize_columns=Настроить столбцы\nreset=Сбросить\nmonday=Понедельник\ntuesday=Вторник\nwednesday=Среда\nthursday=Четверг\nfriday=Пятница\nsaturday=Суббота\nsunday=Воскресенье\nproxy_open_system_proxy_settings=Открыть Системные Настройки Прокси\nproxy_type=Тип Прокси\nproxy_do_not_use_proxy_for=Не Использовать прокси для\nproxy_do_not_use_proxy_for_description=Список URLs, для которых нельзя использовать прокси\\nВы можете использовать подстановочный знак с *\\nнапример 192.168.1.* example.com (через пробел)\nproxy_change_title=Изменить Прокси\nchange_proxy=Изменить\nproxy_no=Без Прокси\nproxy_system=Системный Прокси\nproxy_manual=Ручная настройка\nproxy_pac=Автоматическая Настройка Прокси\nproxy_pac_url=URL-адрес Автоматической Настройки Прокси\naddress=Адрес\nport=Порт\naddress_and_port=Адрес & Порт\nuse_authentication=Использовать Аутентификацию\nwarning_you_may_have_to_restart_the_download_later=Возможно, вам придется перезапустить загрузку позже\\!\nedit_download_title=Редактировать Загрузку\nedit_download_update_from_download_page=Обновить со Страницы Загрузки\nedit_download_update_from_download_page_description=Когда это окно откроется, вы можете перейти на страницу загрузки и нажать кнопку загрузить. Приложение зафиксирует и обновит новые учетные данные для загрузки, чтобы вы могли их сохранить.\nedit_download_saved_download_item_size_not_match=Сохраненный элемент загрузки имеет размер {{currentSize}}, который не соответствует новому размеру {{newSize}}.\ntranslators_page_thanks=С Благодарностью Тем, Кто Помог Перевести Этот Проект ❤️\ntranslators=Переводчики\nlanguage=Язык\ntranslators_contribute_title=Улучшить Перевод\ntranslators_contribute_description=Хотите помочь улучшить этот проект? Если вашего языка нет в списке или он нуждается в доработке, вы можете добавить свои переводы и улучшить его\\!\ncontribute=Внести вклад\nmeet_the_translators=Знакомство с Переводчиками\nlocalized_by_translators=Локализовано Переводчиками\nconfirm_exit=Подтвердить Выход\nconfirm_exit_description=Вы уверены, что хотите выйти из AB Download Manager?\\nАктивные загрузки/очереди будут остановлены\\!\nupdate=Обновить\nupdate_updater=Модуль обновления\nupdate_available=Доступно Обновление\nupdate_error=Ошибка обновления\nupdate_available_suggest_to_to_update=Вы можете обновиться до последней версии, чтобы воспользоваться новыми функциями, улучшениями и повышением производительности.\nupdate_release_notes=Список Изменений\nupdate_check_for_update=Проверить Обновления\nupdate_checking_for_update=Проверка Обновлений\nupdate_no_update=Вы используете последнюю версию\nupdate_check_error=Ошибка при проверке обновлений\nupdate_app_updated_to_version_n=Приложение обновлено до версии {{version}}\ncreate_desktop_entry=Создать запись на рабочем столе\nshutdown_alert=Оповещение о Выключении\nsystem_shutdown_soon=Система Будет Выключена\\!\nsystem_shutdown_failed=Ошибка Выключения Системы\\!\nsystem_shutdown_soon_description=Система скоро завершит работу. Если вы всё ещё используете компьютер, пожалуйста, сохраните данные или отмените выключение.\nsystem_shutdown_reason_queue_completed=Все загрузки в очереди завершены.\nsystem_shutdown_reason_queue_end_time_reached=Достигнуто запланированное время окончания очереди загрузки.\nsystem_shutdown_download_finished=Загрузка завершена.\nshutdown_now=Выключить Сейчас\nsettings_per_host_settings_new_host=Новый хост\nsettings_per_host_settings_not_selected=Сначала создайте или выберите новый элемент\\!\nsettings_per_host_settings_host=Сервер\nsettings_per_host_settings_host_description=Эти настройки будут применены к загрузкам, соответствующим этому имени хоста. Поддерживаются подстановочные знаки (*) (например, example.com, *.example.com — используйте только один).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Сортировать по\nwelcome=Добро пожаловать\nnew_folder=Новая Папка\nskip=Пропустить\nlets_go=Начать\nnext=Продолжить\nselect_all=Выбрать Все\nselect_inside=Выделить\nselect_invert=Инвертировать выделение\nopen_settings=Открыть Настройки\nback=Назад \nservice_is_running=Служба запущена\ninitial_setup_description=Приступить к настройке\ninitial_setup_notice=Вы можете изменить эти настройки в любое время\npermission_granted=Разрешение предоставлено\npermission_not_granted=Разрешение не предоставлено\npermissions=Разрешения\ngive_permission=Выдать разрешение\ngive_storage_permission=Разрешить доступ к хранилищу\nstorage_roots=Storage Roots\npermissions_initial_title=Настройка разрешений\npermissions_initial_description=Для корректной работы приложению требуется несколько разрешений. На следующем экране вы увидите, для чего используется каждое разрешение, и сможете решить, какие из них разрешить, а какие пропустить.\npermissions_done_title=Готово\npermissions_done_description=Всё готово. Все необходимые разрешения предоставлены, и приложение готово к запуску.\npermissions_manage_storage_title=Управление доступом к хранилищу\npermissions_manage_storage_reason=Это разрешение позволяет приложению изменять папку загрузок, более точно обнаруживать дубликаты загрузок и включать некоторые дополнительные функции. Оно необязательно, но рекомендуется для наилучшего результата.\npermission_read_write_external_storage_title=Доступ к хранилищу для чтения и записи\npermission_read_write_external_storage_reason=Это разрешение позволяет приложению сохранять и управлять загруженными файлами, изменять местоположение загрузки и улучшать обнаружение дубликатов загрузок.\npermissions_post_notification_title=Post-уведомление\npermissions_post_notification_reason=Приложение должно работать в фоновом режиме для управления загрузками. Уведомления используются для информирования пользователя и обеспечения работы в фоновом режиме.\npermissions_ignore_battery_optimization_title=Отключить Экономию Батареи\npermissions_ignore_battery_optimization_reason=Некоторые устройства жёстко ограничивают фоновую активность для экономии заряда батареи, что может привести к приостановке или остановке загрузки, когда приложение закрыто. При желании вы можете исключить приложение из программы оптимизации заряда батареи, чтобы обеспечить непрерывную загрузку\nopen_in_browser=Открыть в браузере\nbrowser=Браузер\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Открыть в новой вкладке\nbrowser_open_in_new_background_tab=Открыть в новой фоновой вкладке\nbrowser_no_tab_open=Нет открытых вкладок\nbrowser_tabs=Вкладки\nbrowser_paste_and_go=Вставить и перейти\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/sq_AL.properties",
    "content": "app_title=Menaxheri i Shkarkimeve AB\nconfirm_auto_categorize_downloads_title=Auto-kategorizo shkarkimet\nconfirm_auto_categorize_downloads_description=Çdo artikull pa kategori do të shtohet automatikisht në kategorinë përkatëse.\nconfirm_reset_to_default_categories_title=Rikthe Kategoritë në Gjendjen Fillestare\nconfirm_reset_to_default_categories_description=kjo do te HEQË të gjitha kategoritë dhe do të rikthejë kateroritë fillestare\\!\nconfirm_delete_download_items_title=Konfirmo Fshirjen\nconfirm_delete_download_items_description=A je i sigurt që dëshiron të fshish {{count}} artikuj?\nconfirm_delete_download_unfinished_items_description=Are you sure you want to delete {{count}} unfinished downloads?\nconfirm_delete_download_finished_and_unfinished_items_description=Are you sure you want to delete {{finishedCount}} finished and {{unfinishedCount}} unfinished downloads?\nalso_delete_file_from_disk=Fshi gjithashtu skedarin nga disku\nconfirm_delete_category_item_title=Po fshihet kategoria {{name}}\nconfirm_delete_category_item_description=A je i sigurt që dëshiron të fshish kategorinë “{{value}}”?\nyour_download_will_not_be_deleted=Shkarkimet tuaja nuk do të fshihen\ndrag_the_file_to_another_app=Drag the file to another app\ndrop_link_or_file_here=Hidh lidhjen ose skedarin këtu.\nnothing_will_be_imported=Asgjë nuk do të importohet\nn_links_will_be_imported={{count}} lidhje do të importohen\nn_items_selected={{count}} artikuj të përzgjedhur\nwindow_close=Close\nwindow_minimize=Minimize\nwindow_maximize=Maximize\nwindow_restore=Restore\ndelete=Fshi\nremove=Hiq\ncancel=Anulo\nclose=Mbyll\nmenu=Menu\nmore_options=More Options\nok=Në rregull\nadd=Shto\npaste=Paste\nchange=Ndrysho\nedit=Edit\nchange_anyway=Change Anyway\ndownload=Shkarko\nrefresh=Rifresko\nsettings=Konfigurimet\non_completion=On Completion\nunknown=E panjohur\nunknown_error=Gabim i panjohur\ndownload_item_not_found=Artikulli i shkarkimit nuk u gjet\nname=Emri\ndownload_link=Lidhja e shkarkimit\nnot_finished=Nuk përfundoi\nall=Të gjitha\nfinished=Përfunduar\nUnfinished=E papërfunduar\ncanceled=Anuluar\nerror=Gabim\npaused=Pauzuar.\ndownloading=Duke shkarkuar\nadded=Shtuar\nidle=Në pritje\npreparing_file=Duke pergatitur skedarin.\ncreating_file=Duke krijuar skedarin\nresuming=Duke rifilluar\nretrying=Retrying\nlist_is_empty=Lista është bosh\\!\nsearch_in_the_list=Kërko në listë\nsearch=Kërko\nclear=Pastro\ngeneral=Të përgjithshme\nenabled=Aktivizuar\ndisabled=Çaktivizuar\ndefault=Default\nfile=Skedar\ntasks=Detyra\ntools=Vegla\nhelp=Ndihmë\nsystem=System\nall_missing_files=All Missing Files\nall_finished=Të gjitha të përfunduara\nall_unfinished=Të gjitha të papërfunduara\nentire_list=Lista e plotë\ndownload_browser_integration=Shkarko integrimin për Browser\nexit=Dalje\nshow_downloads=Shfaq Shkarkimet\nnew_download=Shkarkim i Ri\nstop_all=Ndal të gjitha\nimport_from_clipboard=Importo nga Clipboard\nbatch_download=Shkarkim në Grup\nopen=Hap\nshare=Share\nopen_file=Hap Skedarin\nopen_folder=Hap Dosjen\nresume=Rivazhdo\npause=Pauzo\nrestart_download=Rifillo Shkarkimin\ncopy=Copy\ncopy_link=Kopjo lidhjen\ncopy_as_curl=Copy as cURL\nshow_properties=Shfaq Vetitë\nmove_to_queue=Zhvendos në Radhë\nmove_to_this_queue=Move to this Queue\nmove_to_category=Zhvendos në Kategori\nmove_to_this_category=Move to this category\ncategories=Categories\nadd_category=Shto Kategori\nedit_category=Redakto Kategorinë\ndelete_category=Fshi Kategorinë\ncategory_name=Emri i Kategorisë\ncategory_download_location=Vendndodhja e Shkarkimit të Kategorisë\ncategory_download_location_description=Kur zgjidhet kjo kategori në “Shto Shkarkim”, përdorer kjo dosje si “Vendndodhje e Shkarkimit”\ncategory_file_types=Llojet e skedarëve të kategorisë\ncategory_file_types_description=Vendos automatikisht këto lloje skedarësh në këtë kategori. (kur shtoni shkarkim të ri) Ndani shtrirjet e skedarëve me hapësirë (ext1 ext2 ...)\ncategory_url_patterns=Modelet e URL-ve\ncategory_url_patterns_description=Vendos automatikisht shkarkimet nga këto URL në këtë kategori. (kur shtoni shkarkim të ri) Ndani URL-të me hapësirë, mund të përdorni edhe * si shenjë për të gjitha\nauto_categorize_downloads=Auto Kategorizo Shkarkimet\nrestore_defaults=Rikthe në Gjendjen Fillestare\nabout=Rreth\nversion_n=Versioni {{value}}\ndeveloped_with_love_for_you=Zhvilluar me ❤️ për ty\ndonate=Donate\nvisit_the_project_website=Vizito faqen e internetit të projektit\nthis_is_a_free_and_open_source_software=Ky është një program falas dhe me Burim të Hapur\nview_the_source_code=Shiko Kodin Burimor\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=Funksionon me Softuer të Burimit të Hapur\nview_the_open_source_licenses=Shiko licencat e Burimit të Hapur\nsupport_and_community=Mbështetje & Komuniteti\ntelegram=Telegram\nchannel=Kanal\ngroup=Grup\nadd_download=Shto Shkarkim\nadd_multi_download_page_header=Zgjidh artikujt që dëshiron të marrësh për shkarkim\nsave_to=Ruaj Në\nwhere_should_each_item_saved=Ku duhet të ruhet secili artikull?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Ka disa artikuj\\! Ju lutem zgjidhni një mënyrë se si dëshironi t'i ruani ato\neach_item_on_its_own_category=Çdo artikull në kategorinë e vet\neach_item_on_its_own_category_description=Çdo artikull do të vendoset në një kategori që ka atë lloj skedari\nall_items_in_one_category=Të gjithë artikujt në një kategori\nall_items_in_one_category_description=Të gjithë skedarët do të ruhen në vendndodhjen e kategorisë së zgjedhur\nall_items_in_one_Location=Të gjithë artikujt në një vendndodhje\nall_items_in_one_Location_description=Të gjithë artikujt do të ruhen në dosjen e zgjedhur\nunselected_all_items_in_specific_location_description=All files will be saved in the selected category location\nno_category_selected=Asnjë kategori e zgjedhur\nno_categories_found=No Categories Found\ndownload_location=Vendndodhja e Shkarkimit\nlocation=Vendndodhja\nselect_queue=Zgjidh Radhën\nwithout_queue=Pa Radhë\nuse_category=Përdor Kategorinë\ncant_write_to_this_folder=Nuk mund të shkruaj në këtë dosje\nfile_name_already_exists=Emri i skedarit ekziston tashmë\ndownload_already_exists=Download already exists\ninvalid_file_name=Emër i pavlefshëm i skedarit\nshow_solutions=Shfaq zgjidhjet\nchange_solution=Ndrysho zgjidhjen\nselect_a_solution=Zgjidh një zgjidhje\nselect_download_strategy_description=Lidhja që ofruat është tashmë në listën e shkarkimeve, ju lutem përcaktoni çfarë dëshironi të bëni\ndownload_strategy_add_a_numbered_file=Shto një skedar të numëruar\ndownload_strategy_add_a_numbered_file_description=Shto një indeks në fund të emrit të skedarit të shkarkuar\ndownload_strategy_override_existing_file=Mbivendos skedarin ekzistues\ndownload_strategy_override_existing_file_description=Hiq skedarin ekzistues dhe shkruaj në atë skedar\ndownload_strategy_update_download_link=Update existing download\ndownload_strategy_update_download_link_description=Update the existing download link and its credentials\ndownload_strategy_show_downloaded_file=Shfaq skedarin e shkarkuar\ndownload_strategy_show_downloaded_file_description=Shfaq artikullin ekzistues të shkarkuar, që të mund të vazhdoni ose ta hapni\nbatch_download_link_help=Vendos një lidhje që përmban shenja për të gjitha (përdor *)\ninvalid_url=URL e pavlefshme\nlist_is_too_large_maximum_n_items_allowed=Lista është shumë e madhe\\! maksimumi {{count}} artikuj lejohen\nenter_range=Vendos shtrirjen\nrange_from=Nga\nrange_to=Deri në\nbatch_download_wildcard_length=Gjatësia e wildcard\nfirst_link=Lidhja e Parë\nlast_link=Lidhja e fundit\nopen_source_software_used_in_this_app=Softuer me Burim të Hapur i përdorur në këtë Aplikacion\nlinks=Lidhjet\nwebsite=Faqja e Internetit\ndevelopers=Zhvilluesit\nsource_code=Kodi Burimor\nlicense=Licenca\nno_license_found=Licenca nuk u gjet\norganization=Organizata\nadd_new_queue=Shto radhë të re\nqueue_name=Emri i Radhës\nqueues=Radhët\nstop_queue=Ndal Radhën\nstart_queue=Fillo Radhën\nclear_queue_items=Empty Queue\nconfig=Konfigurimi\nitems=Artikujt\nmove_down=Zhvendos poshtë\nmove_up=Zhvendos lart\nremove_queue=Hiq Radhën\nqueue_name_help=Specifiko një emër për këtë radhë\nqueue_name_describe=Emri i radhës është {{value}}\nqueue_max_concurrent_download=Shkarkim maksimal i njëkohshëm\nqueue_max_concurrent_download_description=Shkarkim maksimal për këtë radhë\nqueue_automatic_stop=Ndalim automatik\nqueue_automatic_stop_description=Ndalim automatik i radhës kur nuk ka artikuj në të\nqueue_scheduler=Planifikuesi\nqueue_enable_scheduler=Aktivizo Planifikuesin\nqueue_active_days=Ditët aktive\nqueue_active_days_description=Në cilat ditë funksionon planifikuesi?\nqueue_scheduler_enable_auto_start_time=Enable Auto Start Time\nqueue_scheduler_auto_start_time=Koha e nisjes automatike\nqueue_scheduler_enable_auto_stop_time=Aktivizo kohën automatike të ndalimit\nqueue_scheduler_auto_stop_time=Koha e ndalimit automatik\nqueue_shutdown_on_completion=Shutdown System On Completion\nqueue_shutdown_on_completion_description=Automatically shutdown the system when this queue is completed. or when the scheduled end time is reached.\nappearance=Pamja\ndownload_engine=Motori i shkarkimit\nbrowser_integration=Integrimi në Browser\nsettings_download_max_retries_count=Maximum Download Retries\nsettings_download_max_retries_count_description=The maximum number of times the app will retry a failed download before giving up\nsettings_download_max_retries_count_describe_no_retries=Failed downloads won't be retried\nsettings_download_max_retries_count_describe_n_retries=Failed downloads will be retried {{count}} time(s)\nsettings_download_thread_count=Numri i fijeve\nsettings_download_thread_count_description=Numri maksimal i fijeve për shkarkim për çdo artikull\nsettings_download_thread_count_describe=Një shkarkim mund të ketë deri në {{count}} fije\nsettings_download_thread_count_with_large_value_describe=Warning\\: Setting a high thread count may increase system resource usage, reduce performance, or cause connection issues with servers. Use higher values only if you understand the potential impact on your system and network.\nsettings_use_server_last_modified_time=Përdor kohën e fundit të modifikimit të serverit\nsettings_use_server_last_modified_time_description=Kur shkarkon një skedar, përdor kohën e fundit të modifikimit të serverit për skedarin lokal\nsettings_append_extension_to_incomplete_downloads=Append Extension To Incomplete Downloads\nsettings_append_extension_to_incomplete_downloads_description=Append \".part\" extension to incomplete downloads. This helps to identify unfinished downloads and prevents accidental opening of incomplete files.\nsettings_use_sparse_file_allocation=Shpërndarje e Skedarëve të Rrallë\nsettings_use_sparse_file_allocation_description=Krijoni skedarë më me efektivitet, veçanërisht në SSD, duke ulur shkrimet e panevojshme të të dhënave. Kjo mund të përshpejtojë fillimin e shkarkimeve dhe të ulë përdorimin e diskut. Nëse shkarkimet fillojnë ngadalë ose hasni shpejtësi të pazakonta shkarkimi, konsideroni çaktivizimin e kësaj mundësie, pasi mund të mos jetë plotësisht e përkrahur në disa pajisje.\nsettings_ignore_ssl_certificates=Ignore SSL Certificates\nsettings_ignore_ssl_certificates_description=Disables SSL certificate verification. Use only if necessary, as it may expose your connection to security risks.\nsettings_global_speed_limiter=Kufizues Global i Shpejtësisë\nsettings_global_speed_limiter_description=Kufiri global i shpejtësisë së shkarkimit (0 do të thotë pa kufi)\nsettings_show_average_speed=Shfaq Shpejtësinë Mesatare\nsettings_show_average_speed_description=Shkarko shpejtësinë në mesatare ose me saktësi\nsettings_use_category_by_default=Use Category By Default\nsettings_use_category_by_default_description=Use category by default when adding a download.\nsettings_default_download_folder=Dosja e Paracaktuar e Shkarkimeve\nsettings_default_download_folder_description=Kur shton një shkarkim të ri, kjo vendndodhje përdoret si parazgjedhje\nsettings_default_download_folder_describe=Dosja “{{folder}}” do të përdoret\nsettings_use_proxy=Përdor Proxy\nsettings_use_proxy_description=Përdor Proxy për shkarkimin e skedarëve\nsettings_use_proxy_describe_no_proxy=Asnjë Proxy nuk do të përdoret\nsettings_use_proxy_describe_system_proxy=Proxy i Sistemit do të përdoret\nsettings_use_proxy_describe_manual_proxy={{value}} do të përdoret\nsettings_use_proxy_describe_pac_proxy=PAC file \"{{value}}\" will be used\nsettings_track_deleted_files_on_disk=Track Deleted Files On Disk\nsettings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory.\nsettings_delete_partial_file_on_download_cancellation=Delete Partial File On Download Cancellation\nsettings_delete_partial_file_on_download_cancellation_description=When a download is canceled, the partially downloaded file will be deleted from the disk. This helps keep your download folder clean and reduces unnecessary disk space usage. However, the download will restart from the beginning the next time you start it.\nsettings_default_user_agent=Default User-Agent\nsettings_default_user_agent_description=Specify the Default-User Agent string to define how requests identify to servers. This can help in accessing content optimized for particular devices or in circumventing download limitations imposed by certain websites.\nsettings_download_size_unit=Download Size Unit\nsettings_download_size_unit_description=Unit used to display the download size\nsettings_download_speed_unit=Download Speed Unit\nsettings_download_speed_unit_description=Unit used to display the download speed\nsettings_theme=Tema \nsettings_theme_description=Zgjidh një temë për Aplikacionin\nsettings_default_dark_theme=Default Dark Theme\nsettings_default_dark_theme_description=Applies when the app follows the system theme and dark mode is active\nsettings_default_light_theme=Default Light Theme\nsettings_default_light_theme_description=Applies when the app follows the system theme and light mode is active\nsettings_font=Font\nsettings_font_description=Change the font used in the app interface, Some fonts might not display correctly in the app.\nsettings_ui_scale=UI Scale\nsettings_ui_scale_description=Adjust the size of the app's interface elements\nsettings_language=Gjuha\nsettings_compact_top_bar=Shiriti i Sipërm i Ngushtë\nsettings_compact_top_bar_description=Bashko shiritin e sipërm me shiritin e titullit kur dritarja kryesore ka gjerësi të mjaftueshme\nsettings_use_native_menu_bar=Use Native Menu Bar\nsettings_use_native_menu_bar_description=Use the system's default menu bar style\nsettings_use_relative_date_time=Use relative date/time\nsettings_use_relative_date_time_description=Use relative date/time format for dates in the app (e.g., \"2 days ago\" instead of the exact date/time)\nsettings_show_icon_labels=Show Icon Labels\nsettings_show_icon_labels_description=Show labels under icons when possible ( like home toolbar actions )\nsettings_use_system_tray=Use System Tray\nsettings_use_system_tray_description=Show system tray icon when the app is running\nsettings_start_on_boot=Nisje gjatë Startimit\nsettings_start_on_boot_description=Nis automatikisht aplikacionin kur përdoruesi futet në sistem\nsettings_notification_sound=Nisje gjatë Startimit\nsettings_notification_sound_description=Tingull kur ka një njoftim të ri\nsettings_browser_integration=Integrimi në Browser\nsettings_browser_integration_description=Prano shkarkime nga Browseri\nsettings_browser_integration_server_port=Porti i Serverit\nsettings_browser_integration_server_port_description=Porti për integrimin me shfletuesin\nsettings_browser_integration_server_port_describe=Aplikacioni do të dëgjojë në portin {{port}}\nsettings_dynamic_part_creation=Krijimi Dinamik i Pjesëve\nsettings_dynamic_part_creation_description=Kur një pjesë përfundon, krijo një pjesë tjetër duke ndarë pjesët e tjera për të përmirësuar shpejtësinë e shkarkimit\nsettings_show_completion_dialog=Show Download Completion Dialog\nsettings_show_completion_dialog_description=Automatically show \"Download Complete\" dialog when a download finished.\nsettings_show_download_progress_dialog=Show Download Progress Dialog\nsettings_show_download_progress_dialog_description=Automatically show \"Download Progress\" dialog when a download started.\nsettings_per_host_settings=Per Host Settings\nsettings_per_host_settings_descriptions=These settings will be automatically applied to any new download that matches the specified host.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Kufiri i Shpejtësisë\ndownload_item_settings_speed_limit_description=Kufizo shpejtësinë e shkarkimit për këtë artikull\ndownload_item_settings_show_download_completion_dialog=Show Download Completion Dialog\ndownload_item_settings_show_download_completion_dialog_description=Automatically Show the \"Download Complete\" dialog when this download is finished.\ndownload_item_settings_shutdown_on_completion=Shutdown System On Completion\ndownload_item_settings_shutdown_on_completion_description=Automatically shutdown the system when this download is finished.\ndownload_item_settings_thread_count=Numri i Fijeve\ndownload_item_settings_thread_count_description=Sa fije përdoren për të shkarkuar këtë artikull (0 për normalen)\ndownload_item_settings_thread_count_describe={{count}} fije për këtë shkarkim\ndownload_item_settings_username_description=Vendos një emër përdoruesi nëse lidhja është një burim i mbrojtur\ndownload_item_settings_password_description=Vendos një fjalëkalim nëse lidhja është një burim i mbrojtur\ndownload_item_settings_download_page=Download Page\ndownload_item_settings_download_page_description=The webpage where this download was initiated\ndownload_item_settings_file_checksum=File Checksum\ndownload_item_settings_file_checksum_description=A hash string which can be used to check if file is downloaded correctly\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Custom User-Agent for this item (leave empty to use the default)\nfile_checksum=File Checksum\nfile_checksum_page=File Checksum Checker\nfile_checksum_page_file_checksum_default_algorithm=Default Algorithm\nfile_checksum_page_file_checksum_default_algorithm_help=The default algorithm used to calculate file checksums when they are not provided.\nstart=Start\ncalculated_checksum=Calculated Checksum\nsaved_checksum=Saved Checksum\nchecksum_algorithm=Algorithm\nfile_not_found=File not found\ndownload_not_finished=Download not finished\ndone=Done\nwaiting=Waiting\nmatches=Matches\nnot_matches=Not Matches\ncopy_to_clipboard=Copy To Clipboard\nusername=Emri i Përdoruesit\npassword=Fjalëkalimi\naverage_speed=Shpejtësia Mesatare\nexact_speed=Shpejtësia e Saktë\nunlimited=Pa Kufizim\nuse_global_settings=Përdor Cilësimet Globale\ncant_run_browser_integration=Nuk mund të kryhet integrimi me shfletuesin\ncant_open_file=Nuk mund të hapet Skedari\ncant_open_folder=Nuk mund të hapet Dosja\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} vite\nrelative_time_long_months={{months}} muaj\nrelative_time_long_days={{days}} ditë\nrelative_time_long_hours={{hours}} orë\nrelative_time_long_minutes={{minutes}} minuta\nrelative_time_long_seconds={{seconds}} sekonda\nrelative_time_short_years={{years}} v\nrelative_time_short_months={{months}} mu\nrelative_time_short_days={{days}} d\nrelative_time_short_hours={{hours}} orë\nrelative_time_short_minutes={{minutes}} min\nrelative_time_short_seconds={{seconds}} sek\nrelative_time_left={{time}} mbetur\nrelative_time_ago={{time}} më parë\nauto=Automatik\nunspecified=E Paspecifikuar\ncustom=E Përshtatur\nicon=Ikona\nauthor=Autori\nlink=Lidhja\nsize=Madhësia\nstatus=Statusi\nparts_info_downloaded_size=Shkarkuar\nparts_info_total_size=Totali\nspeed=Shpejtësia\ntime_left=Koha e Mbetur\ndate_added=Data e Shtuar\ninfo=Informacioni\ndownload_page_downloaded_size=Shkarkuar\ndownload_page_download_completed=Download Completed\nresume_support=Mbështetje për Vazhdim\nyes=Po\nno=Jo\nparts_info=Informacion për Pjesët\ndisconnected=I Shkëputur\nreceiving_data=Duke Marrë të Dhëna\nconnecting=Dërgo Kërkesën\nwarning=Paralajmërim\nunsupported_resume_warning=Ky shkarkim nuk mbështet vazhdimin\\! Mund të jetë e nevojshme ta RINISNI më vonë në Listën e Shkarkimeve\nstop_anyway=Ndal Prapëseprapë\ncustomize_columns=Personalizo Kolonat\nreset=Rivendos\nmonday=E Hënë\ntuesday=E Martë\nwednesday=E Mërkurë\nthursday=E Enjte\nfriday=E Premte\nsaturday=E Shtunë\nsunday=E Diel\nproxy_open_system_proxy_settings=Open System Proxy Settings\nproxy_type=Proxy type\nproxy_do_not_use_proxy_for=Don't Use proxy for\nproxy_do_not_use_proxy_for_description=A list of urls that may not be proxied\\nYou can use wildcard with *\\nfor example 192.168.1.* example.com (space separated)\nproxy_change_title=Change Proxy\nchange_proxy=Change Proxy\nproxy_no=No Proxy\nproxy_system=System Proxy\nproxy_manual=Manual Proxy\nproxy_pac=Proxy Auto Configuration\nproxy_pac_url=Proxy Auto Configuration URL\naddress=Address\nport=Port\naddress_and_port=Address & Port\nuse_authentication=Use Authentication\nwarning_you_may_have_to_restart_the_download_later=You may have to restart the download later\\!\nedit_download_title=Edit Download\nedit_download_update_from_download_page=Update from Download Page\nedit_download_update_from_download_page_description=When this window is open, you can go to the Download Page and click the download button. The app will capture and update the new download credentials so you can save them.\nedit_download_saved_download_item_size_not_match=The saved download item has a size of {{currentSize}}, which does not match the new size of {{newSize}}.\ntranslators_page_thanks=With Gratitude to Those Who Helped Translate This Project ❤️\ntranslators=Translators\nlanguage=Language\ntranslators_contribute_title=Improve Translations\ntranslators_contribute_description=Want to help improve this project? If your language isn't listed or needs some tweaks, you can contribute your translations and make it better\\!\ncontribute=Contribute\nmeet_the_translators=Meet the Translators\nlocalized_by_translators=Localized by Translators\nconfirm_exit=Confirm Exit\nconfirm_exit_description=Are you sure you want to exit AB Download Manager?\\nActive downloads/queues will be stopped\\!\nupdate=Update\nupdate_updater=Updater\nupdate_available=Update Available\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=You can update to the latest version to enjoy new features, enhancements, and performance improvements.\nupdate_release_notes=Release Notes\nupdate_check_for_update=Check for Update\nupdate_checking_for_update=Checking for Update\nupdate_no_update=You are using the latest version\nupdate_check_error=Error while checking for update\nupdate_app_updated_to_version_n=App updated to version {{version}}\ncreate_desktop_entry=Create Desktop Entry\nshutdown_alert=Shut Down Alert\nsystem_shutdown_soon=System Will Shut Down Soon\\!\nsystem_shutdown_failed=System Shut Down Failed\\!\nsystem_shutdown_soon_description=The system will shut down soon. If you're still using the computer, please save your work or cancel the shutdown.\nsystem_shutdown_reason_queue_completed=All downloads in the queue are complete.\nsystem_shutdown_reason_queue_end_time_reached=Scheduled end time for the download queue reached.\nsystem_shutdown_download_finished=Download completed.\nshutdown_now=Shut Down Now\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Create or select a new item first\\!\nsettings_per_host_settings_host=Host\nsettings_per_host_settings_host_description=These settings will be applied to downloads matching this hostname. Wildcards (*) are supported (e.g., example.com, *.example.com — use only one).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=Service is running\ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/th_TH.properties",
    "content": "app_title=AB DownloadManager \nconfirm_auto_categorize_downloads_title=จัดหมวดหมู่ดาวน์โหลดอัตโนมัติ\nconfirm_auto_categorize_downloads_description=รายการใด ๆ ที่ไม่ได้จัดหมวดหมู่จะถูกเพิ่มลงในหมวดหมู่ที่เกี่ยวข้องโดยอัตโนมัติ\nconfirm_reset_to_default_categories_title=รีเซ็ตเป็นหมวดหมู่เริ่มต้น\nconfirm_reset_to_default_categories_description=การดำเนินการนี้จะลบหมวดหมู่ทั้งหมดและคืนค่าหมวดหมู่เริ่มต้นกลับมา\\!\nconfirm_delete_download_items_title=ยืนยันลบ\nconfirm_delete_download_items_description=คุณแน่ใจว่าต้องการลบ {{count}} รายการ ?\nconfirm_delete_download_unfinished_items_description=คุณแน่ใจหรือไม่ว่าต้องการลบ {{count}} รายการที่ดาวน์โหลดยังไม่เสร็จ?\nconfirm_delete_download_finished_and_unfinished_items_description=คุณแน่ใจหรือไม่ว่าต้องการลบการดาวน์โหลดที่เสร็จสิ้นแล้ว {{finishedCount}} รายการและการดาวน์โหลดที่ยังไม่เสร็จสิ้น {{unfinishedCount}} รายการ?\nalso_delete_file_from_disk=ลบไฟล์ออกจากดิสก์\nconfirm_delete_category_item_title=กำลังลบหมวดหมู่ {{name}}\nconfirm_delete_category_item_description=คุณแน่ใจหรือไม่ว่าต้องการลบหมวดหมู่ \"{{value}}\"\nyour_download_will_not_be_deleted=ดาวน์โหลดของคุณจะไม่ถูกลบ\ndrag_the_file_to_another_app=ลากไฟล์ไปยังแอปอื่นๆ\ndrop_link_or_file_here=วางลิงค์หรือไฟล์ที่นี่\nnothing_will_be_imported=ไม่มีอะไรจะนำเข้า\nn_links_will_be_imported={{count}} ลิงก์จะถูกนำเข้า\nn_items_selected={{count}} รายการที่เลือก\nwindow_close=ปิด\nwindow_minimize=ย่อขนาด\nwindow_maximize=ใหญ่สุด\nwindow_restore=คืนค่าหน้าต่าง\ndelete=ลบออก\nremove=ลบถาวร\ncancel=ยกเลิก\nclose=ปิด\nmenu=\nmore_options=More Options\nok=ตกลง\nadd=เพิ่ม\npaste=Paste\nchange=เปลี่ยน\nedit=แก้ไข\nchange_anyway=เปลี่ยนแปลงต่อไป\ndownload=ดาวน์โหลด\nrefresh=รีเฟรช\nsettings=ตั้งค่า\non_completion=เมื่อเสร็จสิ้น\nunknown=ไม่ทราบ\nunknown_error=ข้อผิดพลาดที่ไม่รู้จัก\ndownload_item_not_found=ไม่พบรายการดาวน์โหลด\nname=ชื่อ\ndownload_link=ลิงค์ดาวน์โหลด\nnot_finished=ยังไม่เสร็จสิ้น\nall=ทั้งหมด\nfinished=เสร็จสิ้น\nUnfinished=ยังไม่เสร็จสิ้น\ncanceled=ยกเลิก\nerror=ผิดพลาด\npaused=หยุดชั่วคราว\ndownloading=กำลังดาวน์โหลด\nadded=เพิ่ม\nidle=ไม่ใช้งาน\npreparing_file=เตรียมไฟล์\ncreating_file=สร้างไฟล์\nresuming=ดำเนินการต่อ\nretrying=กำลังลองใหม่\nlist_is_empty=รายการว่างเปล่า\\!\nsearch_in_the_list=ค้นหาในรายการ\nsearch=ค้นหา\nclear=เคลียร์\ngeneral=ทั่วไป\nenabled=เปิดใช้งาน\ndisabled=ปิดใช้งาน\ndefault=ค่าเริ่มต้น\nfile=ไฟล์\ntasks=งาน\ntools=เครื่องมือ\nhelp=ช่วยเหลือ\nsystem=ระบบ\nall_missing_files=ไฟล์ที่หายไปทั้งหมด\nall_finished=เสร็จสิ้นทั้งหมด\nall_unfinished=ไม่เสร็จสิ้นทั้งหมด\nentire_list=รายการทั้งหมด\ndownload_browser_integration=รวมเบราว์เซอร์การดาวน์โหลด\nexit=ออก\nshow_downloads=แสดงการดาวน์โหลด\nnew_download=เพิ่มดาวน์โหลด\nstop_all=หยุดทั้งหมด\nimport_from_clipboard=นำเข้าจากคลิปบอร์ด\nbatch_download=ดาวน์โหลดหลายรายการ\nopen=เปิด\nshare=Share\nopen_file=เปิดไฟล์\nopen_folder=เปิดโฟลเดอร์\nresume=ดำเนินการต่อ\npause=หยุดชั่วคราว\nrestart_download=เริ่มการดาวน์โหลดใหม่\ncopy=คัดลอก\ncopy_link=คัดลอกลิงค์\ncopy_as_curl=คัดลอกเป็น cURL\nshow_properties=แสดงคุณสมบัติ\nmove_to_queue=ย้ายไปคิว\nmove_to_this_queue=ย้ายไปคิวนี้ \nmove_to_category=ย้ายไปยังหมวดหมู่\nmove_to_this_category=ย้ายไปหมวดหมู่นี้\ncategories=ประเภท\nadd_category=เพิ่มหมวดหมู่\nedit_category=แก้ไขหมวดหมู่\ndelete_category=ลบหมวดหมู่\ncategory_name=ชื่อหมวดหมู่\ncategory_download_location=หมวดหมู่การดาวน์โหลด\ncategory_download_location_description=เมื่อเลือกหมวดหมู่นี้ใน \"เพิ่มดาวน์โหลด\" ให้ใช้ไดเรกทอรีนี้เป็น \"ตำแหน่งดาวน์โหลด\"\ncategory_file_types=ประเภทหมวดหมู่ไฟล์\ncategory_file_types_description=ใส่ประเภทไฟล์เหล่านี้ลงในหมวดหมู่นี้โดยอัตโนมัติ (เมื่อคุณเพิ่มไฟล์ดาวน์โหลดใหม่)\\nแยกนามสกุลไฟล์ด้วยช่องว่าง (ext1 ext2 ...)\ncategory_url_patterns=รูปแบบ URL\ncategory_url_patterns_description=ใส่ลิงค์ดาวน์โหลดจาก URL อัตโนมัติเหล่านี้ลงในหมวดหมู่นี้โดยอัตโนมัติ (เมื่อคุณเพิ่มการดาวน์โหลดใหม่)\\nคั่น URL ด้วยช่องว่าง คุณยังสามารถใช้ * เป็นไวล์ดการ์ดได้อีกด้วย\nauto_categorize_downloads=จัดหมวดหมู่ดาวน์โหลดอัตโนมัติ\nrestore_defaults=คืนค่าเริ่มต้น\nabout=เกี่ยวกับ\nversion_n=เวอร์ชัน {{value}}\ndeveloped_with_love_for_you=พัฒนาด้วย ❤️ เพื่อคุณ\ndonate=บริจาค\nvisit_the_project_website=เยี่ยมชมเว็บไซต์โครงการ\nthis_is_a_free_and_open_source_software=นี่เป็นซอฟต์แวร์โอเพ่นซอร์สและฟรี\nview_the_source_code=ดูซอสโค้ด\nthird_party_libraries=Third Party Libraries\npowered_by_open_source_software=ขับเคลื่อนด้วยซอฟต์แวร์โอเพ่นซอร์ส\nview_the_open_source_licenses=ดูใบอนุญาตโอเพนซอร์ส\nsupport_and_community=การช่วยเหลือและชุมชน\ntelegram=Telegram\nchannel=ช่อง\ngroup=กลุ่ม\nadd_download=เพิ่มดาวน์โหลด\nadd_multi_download_page_header=เลือกรายการที่คุณต้องการดาวน์โหลด\nsave_to=บันทึกไปยัง\nwhere_should_each_item_saved=บันทึกแต่ละรายการไว้ที่ใด?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=มีหลายรายการ\\! กรุณาเลือกวิธีที่คุณต้องการบันทึก\neach_item_on_its_own_category=แต่ละรายการมีหมวดหมู่ของตัวเอง\neach_item_on_its_own_category_description=แต่ละรายการจะถูกจัดอยู่ในหมวดหมู่ประเภทไฟล์นั้นๆ\nall_items_in_one_category=รายการทั้งหมดอยู่ในหนึ่งหมวดหมู่\nall_items_in_one_category_description=ไฟล์ทั้งหมดจะถูกบันทึกไว้ในหมวดหมู่ที่เลือก\nall_items_in_one_Location=รายการทั้งหมดอยู่ในที่เดียว\nall_items_in_one_Location_description=รายการทั้งหมดจะถูกบันทึกไว้ในไดเร็กทอรีที่เลือก\nunselected_all_items_in_specific_location_description=ไฟล์ทั้งหมดจะถูกบันทึกไว้ในหมวดหมู่ที่เลือก\nno_category_selected=ไม่มีหมวดหมู่ที่เลือก\nno_categories_found=ไม่พบหมวดหมู่\ndownload_location=ตำแหน่งการดาวน์โหลด\nlocation=ที่ตั้ง\nselect_queue=เลือกคิว\nwithout_queue=ไม่มีคิว\nuse_category=ใช้หมวดหมู่\ncant_write_to_this_folder=ไม่สามารถเขียนลงโฟลเดอร์นี้ได้\nfile_name_already_exists=มีชื่อไฟล์นี้อยู่แล้ว\ndownload_already_exists=ดาวน์โหลดนี้มีอยู่แล้ว\ninvalid_file_name=ชื่อไฟล์ไม่ถูกต้อง\nshow_solutions=แสดงแนวทาง\nchange_solution=เปลี่ยนแปลงโซลูชั่น\nselect_a_solution=เลือกโซลูชัน\nselect_download_strategy_description=ลิงก์ที่คุณให้มามีอยู่ในรายการดาวน์โหลดแล้ว กรุณาระบุสิ่งที่คุณต้องการทำ\ndownload_strategy_add_a_numbered_file=เพิ่มไฟล์ที่มีหมายเลข\ndownload_strategy_add_a_numbered_file_description=เพิ่มดัชนีหลังชื่อไฟล์ดาวน์โหลด\ndownload_strategy_override_existing_file=แทนที่ไฟล์ที่มีอยู่แล้ว\ndownload_strategy_override_existing_file_description=ลบดาวน์โหลดที่มีอยู่และเขียนทับลงในไฟล์นั้นๆ\ndownload_strategy_update_download_link=อัพเดทดาวน์โหลดที่มีอยู่\ndownload_strategy_update_download_link_description=อัปเดตลิงก์ดาวน์โหลดและข้อมูลที่มีอยู่\ndownload_strategy_show_downloaded_file=แสดงไฟล์ที่ดาวน์โหลด\ndownload_strategy_show_downloaded_file_description=แสดงรายการดาวน์โหลดที่มีอยู่แล้ว คุณสามารถกดดำเนินการต่อหรือเปิดได้\nbatch_download_link_help=ระบุลิงก์ที่มีไวด์การ์ด (ใช้ *)\ninvalid_url=URL ไม่ถูกต้อง\nlist_is_too_large_maximum_n_items_allowed=รายการมีขนาดใหญ่เกินไป\\! อนุญาตให้มีรายการสูงสุด {{count}} รายการ\nenter_range=ระบุช่วง\nrange_from=จาก\nrange_to=ถึง\nbatch_download_wildcard_length=ความยาวของไวด์การ์ด\nfirst_link=จากลิงค์\nlast_link=ถึงลิงค์\nopen_source_software_used_in_this_app=ซอฟต์แวร์โอเพ่นซอร์สที่ใช้ในแอพนี้\nlinks=ลิงค์\nwebsite=เว็บไซต์\ndevelopers=ผู้พัฒนา\nsource_code=ซอร์สโค้ด\nlicense=ใบอนุญาต\nno_license_found=ไม่พบใบอนุญาต\norganization=องค์กร\nadd_new_queue=เพิ่มรายการใหม่\nqueue_name=ชื่อคิว\nqueues=คิว\nstop_queue=หยนุดคิว\nstart_queue=เริ่มคิว\nclear_queue_items=ไม่มีคิว\nconfig=ตั้งค่า\nitems=รายการ\nmove_down=เลื่อนลง\nmove_up=เลื่อนขึ้น\nremove_queue=ลบคิว\nqueue_name_help=ระบุชื่อสำหรับคิวนี้\nqueue_name_describe=ชื่อคิวคือ {{value}}\nqueue_max_concurrent_download=ดาวน์โหลดพร้อมกันสูงสุด\nqueue_max_concurrent_download_description=ดาวน์โหลดสูงสุดสำหรับคิวนี้\nqueue_automatic_stop=หยุดอัตโนมัติ\nqueue_automatic_stop_description=หยุดคิวอัตโนมัติเมื่อไม่มีรายการ\nqueue_scheduler=ตารางเวลา\nqueue_enable_scheduler=เปิดใช้งานตัวกำหนดเวลา\nqueue_active_days=วันแอ็คทีฟ\nqueue_active_days_description=ตัวกำหนดตารางงานมีวันไหนบ้าง?\nqueue_scheduler_enable_auto_start_time=เปิดใช้งานเริ่มต้นอัตโนมัติ\nqueue_scheduler_auto_start_time=เวลาเริ่มต้นอัตโนมัติ\nqueue_scheduler_enable_auto_stop_time=เปิดใช้งานการหยุดเวลาอัตโนมัติ\nqueue_scheduler_auto_stop_time=เวลาหยุดอัตโนมัติ\nqueue_shutdown_on_completion=ปิดเครื่องเมื่อเสร็จสิ้น\nqueue_shutdown_on_completion_description=ปิดระบบโดยอัตโนมัติเมื่อคิวนี้เสร็จสิ้น หรือเมื่อถึงเวลาสิ้นสุดที่กำหนดไว้\nappearance=รูปแบบ\ndownload_engine=ดาวน์โหลดเครื่องมือ\nbrowser_integration=ใช้งานบนเบราว์เซอร์\nsettings_download_max_retries_count=จำนวนครั้งสูงสุดในการดาวน์โหลดซ้ำ\nsettings_download_max_retries_count_description=จำนวนครั้งสูงสุดที่โปรแกรมจะลองดาวน์โหลดใหม่อีกครั้งก่อนที่จะยกเลิก\nsettings_download_max_retries_count_describe_no_retries=ดาวน์โหลดล้มเหลวจะไม่ถูกลองอีกครั้ง\nsettings_download_max_retries_count_describe_n_retries=ดาวน์โหลดล้มเหลวจะลองใหม่อีก {{count}} ครั้ง\nsettings_download_thread_count=จำนวนเธรด\nsettings_download_thread_count_description=เธรดดาวน์โหลดสูงสุดต่อรายการดาวน์โหลด\nsettings_download_thread_count_describe=การดาวน์โหลดสามารถมีเธรดได้สูงสุด {{count}} เธรด\nsettings_download_thread_count_with_large_value_describe=คำเตือน\\: การตั้งค่าจำนวนเธรดที่สูงอาจเพิ่มการใช้ทรัพยากรระบบ ลดประสิทธิภาพ หรือทำให้เกิดปัญหาการเชื่อมต่อกับเซิร์ฟเวอร์ ใช้ค่าที่สูงขึ้นเฉพาะเมื่อคุณเข้าใจถึงผลกระทบที่อาจเกิดขึ้นกับระบบและเครือข่ายของคุณเท่านั้น\nsettings_use_server_last_modified_time=ใช้เวลาที่แก้ไขล่าสุดของเซิร์ฟเวอร์\nsettings_use_server_last_modified_time_description=เมื่อดาวน์โหลดไฟล์ ให้ใช้เวลของเซิร์ฟเวอร์ล่าสุดสำหรับไฟล์\nsettings_append_extension_to_incomplete_downloads=Append Extension To Incomplete Downloads\nsettings_append_extension_to_incomplete_downloads_description=\nsettings_use_sparse_file_allocation=จัดสรรไฟล์ขนาดเล็ก\nsettings_use_sparse_file_allocation_description=สร้างไฟล์อย่างมีประสิทธิภาพมากขึ้น โดยเฉพาะบน SSD โดยลดการเขียนข้อมูลที่ไม่จำเป็น วิธีนี้จะช่วยให้การดาวน์โหลดเริ่มต้นเร็วขึ้นและลดการใช้ดิสก์ หากการดาวน์โหลดเริ่มต้นช้าลงหรือคุณพบว่าความเร็วในการดาวน์โหลดผิดปกติ โปรดพิจารณาปิดใช้งานตัวเลือกนี้ เนื่องจากอุปกรณ์บางเครื่องอาจไม่รองรับตัวเลือกนี้\nsettings_ignore_ssl_certificates=ไม่สนใจใบรับรอง SSL\nsettings_ignore_ssl_certificates_description=ปิดใช้งานการตรวจสอบใบรับรอง SSL ใช้เฉพาะเมื่อจำเป็นเท่านั้น เนื่องจากอาจทำให้การเชื่อมต่อของคุณเสี่ยงต่อความปลอดภัย\nsettings_global_speed_limiter=จำกัดความเร็วทั่วโลก\nsettings_global_speed_limiter_description=ขีดจำกัดความเร็วการดาวน์โหลดทั่วโลก (0 หมายถึงไม่จำกัด)\nsettings_show_average_speed=แสดงความเร็วเฉลี่ย\nsettings_show_average_speed_description=ความเร็วในการดาวน์โหลดโดยเฉลี่ยหรือความแม่นยำ\nsettings_use_category_by_default=ใช้หมวดหมู่เริ่มต้น\nsettings_use_category_by_default_description=ใช้หมวดหมู่เริ่มต้นเมื่อเพิ่มการดาวน์โหลด\nsettings_default_download_folder=โฟลเดอร์ดาวน์โหลดเริ่มต้น\nsettings_default_download_folder_description=เมื่อคุณเพิ่มดาวน์โหลดใหม่ ตำแหน่งนี้จะถูกใช้ตามค่าเริ่มต้น\nsettings_default_download_folder_describe=ใช้โฟลเดอร์ \"{{folder}}\nsettings_use_proxy=ใช้พร็อกซี\nsettings_use_proxy_description=\"ตั้งค่าใช้พร็อกซี\"\nsettings_use_proxy_describe_no_proxy=ไม่ใช้พร็อกซี\nsettings_use_proxy_describe_system_proxy=ใช้พร็อกซีของระบบ\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" จะถูกใช้\nsettings_use_proxy_describe_pac_proxy=ไฟล์ pac \"{{value}}\" จะถูกใช้\nsettings_track_deleted_files_on_disk=ติดตามไฟล์ที่ถูกลบบนดิสก์\nsettings_track_deleted_files_on_disk_description=ลบไฟล์โดยอัตโนมัติจากรายการเมื่อไฟล์ถูกลบหรือย้ายจากไดเร็กทอรีดาวน์โหลด\nsettings_delete_partial_file_on_download_cancellation=Delete Partial File On Download Cancellation\nsettings_delete_partial_file_on_download_cancellation_description=When a download is canceled, the partially downloaded file will be deleted from the disk. This helps keep your download folder clean and reduces unnecessary disk space usage. However, the download will restart from the beginning the next time you start it.\nsettings_default_user_agent=ผู้ใช้เริ่มต้น\nsettings_default_user_agent_description=ระบุสตริงตัวแทนผู้ใช้เริ่มต้นเพื่อกำหนดคำขอระบุตัวตนไปยังเซิร์ฟเวอร์ ซึ่งช่วยในการเข้าถึงเนื้อหาที่ปรับให้เหมาะสมสำหรับอุปกรณ์หรือในการหลีกเลี่ยงข้อจำกัดการดาวน์โหลดที่กำหนดโดยเว็บไซต์ที่\nsettings_download_size_unit=หน่วยของขนาดไฟล์ที่ดาวน์โหลด\nsettings_download_size_unit_description=หน่วยที่ใช้แสดงขนาดการดาวน์โหลด\nsettings_download_speed_unit=ความเร็วในการดาวน์โหลด\nsettings_download_speed_unit_description=หน่วยความเร็วดาวน์โหลด\nsettings_theme=รูปแบบ\nsettings_theme_description=เลือกรูปแบบสำหรับแอป\nsettings_default_dark_theme=ธีมมืด ค่าเริ่มต้น\nsettings_default_dark_theme_description=Applies when the app follows the system theme and dark mode is active\nsettings_default_light_theme=ธีมสว่าง ค่าเริ่มต้น\nsettings_default_light_theme_description=Applies when the app follows the system theme and light mode is active\nsettings_font=แบบอักษร\nsettings_font_description=เปลี่ยนแบบอักษรที่ใช้ในหน้าตาแอป บางแบบอักษรอาจแสดงผลไม่ถูกต้องในแอป\nsettings_ui_scale=สเกล UI\nsettings_ui_scale_description=ปรับขนาดองค์ประกอบอินเทอร์เฟซของแอป\nsettings_language=ภาษา\nsettings_compact_top_bar=ท็อปบาร์ขนาดกะทัดรัด\nsettings_compact_top_bar_description=รวมแถบด้านบนกับแถบชื่อเรื่องเมื่อหน้าต่างหลักมีความกว้างเพียงพอ\nsettings_use_native_menu_bar=ใช้แถบเมนูของระบบ\nsettings_use_native_menu_bar_description=ใช้รูปแบบแถบเมนูเริ่มต้นของระบบ\nsettings_use_relative_date_time=ใช้รูปแบบวันที่/เวลาแบบสัมพัทธ์\nsettings_use_relative_date_time_description=ใช้รูปแบบวันที่/เวลาแบบสัมพันธ์ในแอป (เช่น '2 วันที่แล้ว' แทนวันที่/เวลาที่แน่นอน)\nsettings_show_icon_labels=แสดงป้ายไอคอน\nsettings_show_icon_labels_description=แสดงป้ายกำกับใต้ไอคอนเมื่อทำได้ (เช่น การดำเนินการบนแถบเครื่องมือหน้าแรก)\nsettings_use_system_tray=ใช้ System Tray\nsettings_use_system_tray_description=แสดงไอคอนระบบเมื่อแอปกำลังทำงาน\nsettings_start_on_boot=เริ่มต้นเมื่อบูต\nsettings_start_on_boot_description=เริ่มแอปพลิเคชันอัตโนมัติเมื่อเข้าสู่ระบบ\nsettings_notification_sound=เสียงแจ้งเตือน\nsettings_notification_sound_description=เล่นเสียงเมื่อมีการแจ้งเตือนใหม่\nsettings_browser_integration=รวบรวมบนเบราว์เซอร์\nsettings_browser_integration_description=ยอมรับการดาวน์โหลดจากเบราว์เซอร์\nsettings_browser_integration_server_port=พอร์ตเซิร์ฟเวอร์\nsettings_browser_integration_server_port_description=พอร์ตสำหรับการรวบเบราว์เซอร์\nsettings_browser_integration_server_port_describe=แอปจะเฝ้าดูพอร์ต {{port}}\nsettings_dynamic_part_creation=สร้างชิ้นส่วนไดนามิก\nsettings_dynamic_part_creation_description=เมื่อส่วนหนึ่งเสร็จสิ้น ให้สร้างส่วนอื่นโดยแบ่งส่วนอื่นๆ ออกเพื่อเพิ่มความเร็วในการดาวน์โหลด\nsettings_show_completion_dialog=แสดงกล่องโต้ตอบดาวน์โหลดเมื่อเสร็จสมบูรณ์\nsettings_show_completion_dialog_description=แสดงกล่องโต้ตอบ \"ดาวน์โหลดเสร็จสิ้น\" โดยอัตโนมัติเมื่อการดาวน์โหลดเสร็จสิ้น\nsettings_show_download_progress_dialog=แสดงกล่องโต้ตอบความคืบหน้าการดาวน์โหลด\nsettings_show_download_progress_dialog_description=แสดงกล่องโต้ตอบ \"ความคืบหน้าการดาวน์โหลด\" โดยอัตโนมัติเมื่อการดาวน์โหลดเริ่มต้น\nsettings_per_host_settings=Per Host Settings\nsettings_per_host_settings_descriptions=การตั้งค่าเหล่านี้จะถูกนำไปใช้กับการดาวน์โหลดใหม่ที่ตรงกับโฮสต์ที่ระบุโดยอัตโนมัติ\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=จำกัดความเร็ว\ndownload_item_settings_speed_limit_description=จำกัดความเร็วดาวน์โหลดสำหรับรายการนี้\ndownload_item_settings_show_download_completion_dialog=แสดงกล่องโต้ตอบเมื่อดาวน์โหลดเสร็จสมบูรณ์\ndownload_item_settings_show_download_completion_dialog_description=แสดงกล่องโต้ตอบ \"ดาวน์โหลดเสร็จสิ้น\" โดยอัตโนมัติเมื่อดาวน์โหลดนี้เสร็จสิ้น\ndownload_item_settings_shutdown_on_completion=ปิดระบบเมื่อเสร็จสิ้น\ndownload_item_settings_shutdown_on_completion_description=ปิดระบบโดยอัตโนมัติเมื่อดาวน์โหลดนี้เสร็จสิ้น\ndownload_item_settings_thread_count=จำนวนเธรด\ndownload_item_settings_thread_count_description=ใช้เธรดจำนวนเท่าใดในการดาวน์โหลดดาวน์โหลดนี้ (0 เป็นค่าเริ่มต้น)\ndownload_item_settings_thread_count_describe={{count}} สำหรับการดาวน์โหลดนี้\ndownload_item_settings_username_description=ระบุชื่อผู้ใช้หากลิงก์ได้รับการป้องกัน\ndownload_item_settings_password_description=ระบุรหัสผ่านหากลิงก์ได้รับการป้องกัน\ndownload_item_settings_download_page=หน้าดาวน์โหลด\ndownload_item_settings_download_page_description=เว็บเพจที่เริ่มการดาวน์โหลดนี้\ndownload_item_settings_file_checksum=ตั้งค่าตรวจสอบไฟล์ที่ดาวน์โหลด (Checksum)\ndownload_item_settings_file_checksum_description=สตริงแฮชที่สามารถใช้ตรวจสอบว่าไฟล์ได้รับการดาวน์โหลดอย่างถูกต้องหรือไม่\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Custom User-Agent for this item (leave empty to use the default)\nfile_checksum=ตรวจสอบไฟล์ (Checksum)\nfile_checksum_page=ตรวจสอบไฟล์ (Checksum)\nfile_checksum_page_file_checksum_default_algorithm=อัลกอริทึมเริ่มต้น\nfile_checksum_page_file_checksum_default_algorithm_help=อัลกอริทึมเริ่มต้นที่ใช้เพื่อคำนวณค่าตรวจสอบความถูกต้องของไฟล์เมื่อไม่มีการระบุไว้\nstart=เริ่ม\ncalculated_checksum=คำนวน Checksum\nsaved_checksum=บันทึก Checksum\nchecksum_algorithm=อัลกอริ\nfile_not_found=ไม่พบไฟล์\ndownload_not_finished=ดาวน์โหลดไม่เสร็จสิ้น\ndone=เรียบร้อย\nwaiting=กำลังรอ\nmatches=จับคู่\nnot_matches=ไม่จับคู่\ncopy_to_clipboard=คัดลอกไปยังคลิปบอร์ด\nusername=ชื่อผู้ใช้\npassword=รหัสผ่าน\naverage_speed=ความเร็วเฉลี่ย\nexact_speed=ความเร็วที่แน่นอน\nunlimited=ไม่จำกัด\nuse_global_settings=ใช้ตั้งค่าทั่วไป\ncant_run_browser_integration=ไม่สามารถทำงานบนเบราว์เซอร์ได้\ncant_open_file=ไม่สามารถเปิดไฟล์\ncant_open_folder=ไม่สามารถเปิดโฟลเดอร์\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} ปี\nrelative_time_long_months={{months}} เดือน\nrelative_time_long_days={{days}} วัน\nrelative_time_long_hours={{hours}} ชั่วโมง\nrelative_time_long_minutes={{minutes}} นาที\nrelative_time_long_seconds={{seconds}} วินาที\nrelative_time_short_years={{years}} ปี\nrelative_time_short_months={{months}} เดือน\nrelative_time_short_days={{days}} วัน\nrelative_time_short_hours={{hours}} ชั่วโมง\nrelative_time_short_minutes={{minutes}} นาที\nrelative_time_short_seconds={{seconds}} วินาที\nrelative_time_left=เหลือเวลา {{time}}\nrelative_time_ago={{time}} ที่ผ่านมา\nauto=อัตโนมัติ\nunspecified=ไม่ระบุ\ncustom=กำหนดเอง\nicon=ไอคอน\nauthor=ผู้เขียน\nlink=ลิงค์\nsize=ขนาด\nstatus=สถานะ\nparts_info_downloaded_size=ดาวน์โหลด\nparts_info_total_size=ทั้งหมด\nspeed=ความเร็ว\ntime_left=เวลาที่เหลือ\ndate_added=วันที่เพิ่ม\ninfo=ข้อมูล\ndownload_page_downloaded_size=ดาวน์โหลดแล้ว\ndownload_page_download_completed=ดาวน์โหลดเสร็จสิ้น\nresume_support=ดำเนินการสนับสนุนการต่อ\nyes=ใช่\nno=ไม่\nparts_info=ข้อมูลชิ้นส่วน\ndisconnected=ถูกตัดการเชื่อมต่อ\nreceiving_data=รับข้อมูล\nconnecting=กำลังเชื่อมต่อ\nwarning=คำเตือน\nunsupported_resume_warning=ดาวน์โหลดนี้ไม่รองรับการเริ่มใหม่อีกครั้ง\\! คุณอาจต้องเริ่มใหม่อีกครั้งในภายหลังในรายการดาวน์โหลด\nstop_anyway=หยุดต่อไป\ncustomize_columns=ปรับแต่งคอลัมน์\nreset=รีเซ็ต\nmonday=จันทร์\ntuesday=อังคาร\nwednesday=พุธ\nthursday=พฤหัสบดี\nfriday=ศุกร์\nsaturday=เสาร์\nsunday=อาทิตย์\nproxy_open_system_proxy_settings=เปิดการตั้งค่าพร็อกซีระบบ\nproxy_type=ประเภทพร็อกซี\nproxy_do_not_use_proxy_for=อย่าใช้พร็อกซีสำหรับ\nproxy_do_not_use_proxy_for_description=รายการพร็อกซี คุณสามารถใช้ไวล์ดการ์ด * ได้\\nเช่น 192.168.1.* example.com (คั่นด้วยช่องว่าง)\nproxy_change_title=เปลี่ยนพร็อกซี \nchange_proxy=เปลี่ยนพร็อกซี\nproxy_no=ไม่มีพร็อกซี\nproxy_system=พร็อกซีของระบบ\nproxy_manual=ระบุพร็อกซีเอง\nproxy_pac=ตั้งค่าพร็อกซีอัตโนมัติ\nproxy_pac_url=URL กำหนดค่าพร็อกซีอัตโนมัติ\naddress=ที่อยู่\nport=พอร์ต\naddress_and_port=ที่อยู่และพอร์ต\nuse_authentication=ใช้การตรวจสอบสิทธิ์\nwarning_you_may_have_to_restart_the_download_later=คุณอาจต้องเริ่มการดาวน์โหลดใหม่อีกครั้งในภายหลัง\\!\nedit_download_title=แก้ไขดาวน์โหลด\nedit_download_update_from_download_page=อัปเดตจากหน้าดาวน์โหลด\nedit_download_update_from_download_page_description=เมื่อหน้าต่างนี้เปิดขึ้น คุณสามารถไปที่หน้าดาวน์โหลดและคลิกปุ่มดาวน์โหลด โปรแกรมจะบันทึกและอัปเดตข้อมูลการดาวน์โหลดใหม่ เพื่อให้คุณสามารถบันทึกข้อมูลเหล่านี้ได้\nedit_download_saved_download_item_size_not_match=รายการดาวน์โหลดมีขนาด {{currentSize}} ไม่ตรงกับขนาดใหม่ของ {{newSize}}\ntranslators_page_thanks=ขอบคุณที่ช่วยแปลโครงการนี้ ❤️\ntranslators=แปลภาษา\nlanguage=ภาษา\ntranslators_contribute_title=ปรับปรุงการแปล\ntranslators_contribute_description=ต้องการช่วยปรับปรุงโครงการนี้หรือไม่ หากภาษาของคุณไม่อยู่ในรายการหรือต้องการแก้ไข คุณสามารถช่วยแปลและปรับปรุงให้ดีขึ้นได้\\!\ncontribute=สนับสนุน\nmeet_the_translators=พบกับนักแปลภาษา\nlocalized_by_translators=แปลโดยเจ้าของภาษา\nconfirm_exit=ยืนยันการออก\nconfirm_exit_description=คุณต้องการออกจากโปรแกรม ?\\nดาวน์โหลด/คิวที่ใช้งานอยู่จะหยุดลง\\!\nupdate=อัปเดต\nupdate_updater=อัปเดต\nupdate_available=อัปเดตพร้อมใช้งาน\nupdate_error=Update Error\nupdate_available_suggest_to_to_update=คุณสามารถอัปเดตเป็นเวอร์ชั่นล่าสุดเพื่อพบกับคุณสมบัติใหม่ การปรับปรุง และการปรับปรุงประสิทธิภาพ\nupdate_release_notes=หมายเหตุ\nupdate_check_for_update=ตรวจสอบอัปเดต\nupdate_checking_for_update=กำลังตรวจสอบอัปเดต\nupdate_no_update=คุณกำลังใช้เวอร์ชั่นล่าสุด\nupdate_check_error=เกิดข้อผิดพลาดขณะตรวจสอบการอัปเดต\nupdate_app_updated_to_version_n=ได้อัพเดตเวอร์ชัน{{version}}\ncreate_desktop_entry=Create Desktop Entry\nshutdown_alert=แจ้งเตือนการปิดเครื่อง\nsystem_shutdown_soon=ระบบจะปิดเครื่องในเร็ว ๆ นี้\\!\nsystem_shutdown_failed=การปิดระบบล้มเหลว\\!\nsystem_shutdown_soon_description=ระบบจะปิดเครื่องในเร็ว ๆ นี้ หากคุณยังใช้งานอยู่ กรุณาบันทึกงานของคุณหรือยกเลิกการปิดเครื่อง\nsystem_shutdown_reason_queue_completed=การดาวน์โหลดทั้งหมดในคิวเสร็จสมบูรณ์แล้ว\nsystem_shutdown_reason_queue_end_time_reached=ถึงเวลาสิ้นสุดที่กำหนดไว้สำหรับคิวดาวน์โหลดแล้ว\nsystem_shutdown_download_finished=ดาวน์โหลดเสร็จสมบูรณ์\nshutdown_now=ปิดเครื่องทันที\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Create or select a new item first\\!\nsettings_per_host_settings_host=โฮสต์\nsettings_per_host_settings_host_description=These settings will be applied to downloads matching this hostname. Wildcards (*) are supported (e.g., example.com, *.example.com — use only one).\nsettings_browser_in_launcher=Browser Icon In Launcher\nsettings_browser_in_launcher_description=Show or hide the browser icon in the launcher (app list).\nsort_by=Sort By\nwelcome=Welcome\nnew_folder=New Folder\nskip=Skip\nlets_go=Let's Go\nnext=Next\nselect_all=Select All\nselect_inside=Select Inside\nselect_invert=Select Invert\nopen_settings=Open Settings\nback=Back\nservice_is_running=กำลังดำเนินการ \ninitial_setup_description=Let’s set things up\ninitial_setup_notice=You can change these settings anytime later\npermission_granted=Permission granted\npermission_not_granted=Permission not granted\npermissions=Permissions\ngive_permission=Allow permission\ngive_storage_permission=Allow storage access\nstorage_roots=Storage Roots\npermissions_initial_title=Permissions setup\npermissions_initial_description=To work properly, the app needs a few permissions. On the next screen, you’ll see what each permission is used for and you can decide which ones to allow or skip.\npermissions_done_title=You’re all set\npermissions_done_description=Everything is ready. All required permissions have been granted and the app is good to go.\npermissions_manage_storage_title=Manage storage access\npermissions_manage_storage_reason=This permission lets the app change the download folder, detect duplicate downloads more accurately, and enable some extra features. It’s optional, but recommended for the best experience.\npermission_read_write_external_storage_title=Read and write storage\npermission_read_write_external_storage_reason=This permission allows the app to save and manage downloaded files, change the download location, and improve duplicate download detection.\npermissions_post_notification_title=Post Notification\npermissions_post_notification_reason=The app needs to run in the background to manage downloads. Notifications are used to keep you informed and allow background operation.\npermissions_ignore_battery_optimization_title=Ignore Battery Optimization\npermissions_ignore_battery_optimization_reason=Some devices aggressively limit background activity to save battery, which can pause or stop downloads when the app isn’t open. You can optionally exclude the app from battery optimization to ensure downloads continue uninterrupted\nopen_in_browser=Open In Browser\nbrowser=Browser\nbrowser_new_tab=New Tab\nbrowser_close_tab=Close Tab\nbrowser_open_in_new_tab=Open In New Tab\nbrowser_open_in_new_background_tab=Open In New Background Tab\nbrowser_no_tab_open=No tabs are open\nbrowser_tabs=Tabs\nbrowser_paste_and_go=Paste And Go\nbrowser_bookmarks=Bookmarks\nbrowser_add_bookmark=Add Bookmark\nbrowser_edit_bookmark=Edit Bookmark\nbrowser_add_to_bookmarks=Add To Bookmarks\nbrowser_remove_from_bookmarks=Remove From Bookmarks\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/tr_TR.properties",
    "content": "app_title=AB İndirme Yöneticisi\nconfirm_auto_categorize_downloads_title=İndirmeleri otomatik kategorize et\nconfirm_auto_categorize_downloads_description=Kategorize edilmemiş herhangi bir öğe, otomatik olarak ilgili kategorisine eklenecektir.\nconfirm_reset_to_default_categories_title=Varsayılan Kategorilere Sıfırla\nconfirm_reset_to_default_categories_description=Bu işlem, tüm kategorileri SİLECEK ve varsayılan kategorileri geri getirecektir\\!\nconfirm_delete_download_items_title=Silmeyi Onayla\nconfirm_delete_download_items_description={{count}} öğeyi silmek istediğinizden emin misiniz?\nconfirm_delete_download_unfinished_items_description={{count}} tamamlanmamış indirmeyi silmek istediğinizden emin misiniz?\nconfirm_delete_download_finished_and_unfinished_items_description={{finishedCount}} tamamlanmış ve {{unfinishedCount}} tamamlanmamış indirmeyi silmek istediğinizden emin misiniz?\nalso_delete_file_from_disk=Dosyayı diskten de sil\nconfirm_delete_category_item_title={{name}} kategorisi kaldırılıyor\nconfirm_delete_category_item_description=\"{{value}}\" Kategorisini silmek istediğinizden emin misiniz?\nyour_download_will_not_be_deleted=İndirmeleriniz silinmeyecektir\ndrag_the_file_to_another_app=Dosyayı başka bir uygulamaya sürükleyin\ndrop_link_or_file_here=Bağlantıyı veya dosyayı buraya bırakın.\nnothing_will_be_imported=Hiçbir şey içe aktarılmayacak\nn_links_will_be_imported={{count}} bağlantı içe aktarılacak\nn_items_selected={{count}} öğe seçildi\nwindow_close=Kapat\nwindow_minimize=Simge Durumuna Küçült\nwindow_maximize=Ekranı Kapla\nwindow_restore=Geri Yükle\ndelete=Sil\nremove=Kaldır\ncancel=İptal\nclose=Kapat\nmenu=Menü\nmore_options=Diğer Seçenekler\nok=Tamam\nadd=Ekle\npaste=Yapıştır\nchange=Değiştir\nedit=Düzenle\nchange_anyway=Yine de Değiştir\ndownload=İndir\nrefresh=Yenile\nsettings=Ayarlar\non_completion=Tamamlandığında\nunknown=Bilinmiyor\nunknown_error=Bilinmeyen Hata\ndownload_item_not_found=İndirme öğesi bulunamadı\nname=Ad\ndownload_link=İndirme bağlantısı\nnot_finished=Tamamlanmadı\nall=Tümü\nfinished=Tamamlananlar\nUnfinished=Tamamlanmayanlar\ncanceled=İptal Edildi\nerror=Hata\npaused=Duraklatıldı\ndownloading=İndiriliyor\nadded=Eklendi\nidle=BOŞTA\npreparing_file=Dosya Hazırlanıyor\ncreating_file=Dosya Oluşturuluyor\nresuming=Devam Ediliyor\nretrying=Yeniden Deneniyor\nlist_is_empty=Liste boş\\!\nsearch_in_the_list=Listede Ara\nsearch=Ara\nclear=Temizle\ngeneral=Genel\nenabled=Etkin\ndisabled=Devre Dışı\ndefault=Varsayılan\nfile=Dosya\ntasks=Görevler\ntools=Araçlar\nhelp=Yardım\nsystem=Sistem\nall_missing_files=Tüm Eksik Dosyalar\nall_finished=Tüm Tamamlananlar\nall_unfinished=Tüm Tamamlanmayanlar\nentire_list=Tüm Liste\ndownload_browser_integration=Tarayıcı Entegrasyonunu İndir\nexit=Çıkış\nshow_downloads=İndirmeleri Göster\nnew_download=Yeni İndirme\nstop_all=Tümünü Durdur\nimport_from_clipboard=Panodan İçe Aktar\nbatch_download=Toplu İndirme\nopen=Aç\nshare=Paylaş\nopen_file=Dosyayı Aç\nopen_folder=Klasörü Aç\nresume=Devam Et\npause=Duraklat\nrestart_download=İndirmeyi Yeniden Başlat\ncopy=Kopyala\ncopy_link=Bağlantıyı Kopyala\ncopy_as_curl=cURL olarak Kopyala\nshow_properties=Özellikleri Göster\nmove_to_queue=Kuyruğa Taşı\nmove_to_this_queue=Bu Kuyruğa Taşı\nmove_to_category=Kategoriye Taşı\nmove_to_this_category=Bu kategoriye taşı\ncategories=Kategoriler\nadd_category=Kategori Ekle\nedit_category=Kategoriyi Düzenle\ndelete_category=Kategoriyi Sil\ncategory_name=Kategori Adı\ncategory_download_location=Kategori İndirme Konumu\ncategory_download_location_description=\"İndirme Ekle\" penceresinde bu kategori seçildiğinde, \"İndirme Konumu\" olarak bu dizini kullan\ncategory_file_types=Kategori dosya türleri\ncategory_file_types_description=Bu dosya türlerini otomatik olarak bu kategoriye yerleştir (yeni indirme eklediğinizde).\\nDosya uzantılarını boşlukla ayırın (uznt1 uznt2 ...)\ncategory_url_patterns=URL Desenleri\ncategory_url_patterns_description=Bu URL'lerden yapılan indirmeleri otomatik olarak bu kategoriye yerleştir (yeni indirme eklediğinizde).\\nURL'leri boşlukla ayırın, joker karakter için * kullanabilirsiniz\nauto_categorize_downloads=İndirmeleri Otomatik Kategorize Et\nrestore_defaults=Varsayılanları Geri Yükle\nabout=Hakkında\nversion_n=Sürüm {{value}}\ndeveloped_with_love_for_you=Sizin için ❤️ ile geliştirildi\ndonate=Bağış Yap\nvisit_the_project_website=Proje web sitesini ziyaret et\nthis_is_a_free_and_open_source_software=Bu, ücretsiz ve açık kaynaklı bir yazılımdır\nview_the_source_code=Kaynak Kodunu Gör\nthird_party_libraries=Üçüncü Parti Kütüphaneler\npowered_by_open_source_software=Açık Kaynaklı Yazılımlar Tarafından Desteklenmektedir\nview_the_open_source_licenses=Açık Kaynak Lisanslarını Görüntüle\nsupport_and_community=Destek & Topluluk\ntelegram=Telegram\nchannel=Kanal\ngroup=Grup\nadd_download=İndirme Ekle\nadd_multi_download_page_header=İndirmek için almak istediğiniz öğeleri seçin\nsave_to=Kaydetme Yeri\nwhere_should_each_item_saved=Her bir öğe nereye kaydedilmeli?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Birden fazla öğe var\\! Lütfen onları kaydetmek için bir yol seçin\neach_item_on_its_own_category=Her öğe kendi kategorisine\neach_item_on_its_own_category_description=Her öğe, o dosya türüne sahip bir kategoriye yerleştirilecektir\nall_items_in_one_category=Tüm öğeler tek bir kategoride\nall_items_in_one_category_description=Tüm dosyalar seçilen kategoriye kaydedilecektir\nall_items_in_one_Location=Tüm öğeler tek bir konumda\nall_items_in_one_Location_description=Tüm öğeler seçilen dizine kaydedilecektir\nunselected_all_items_in_specific_location_description=Tüm dosyalar, seçilen kategori konumuna kaydedilecektir\nno_category_selected=Kategori Seçilmedi\nno_categories_found=Kategori Bulunamadı\ndownload_location=İndirme Konumu\nlocation=Konum\nselect_queue=Kuyruk Seç\nwithout_queue=Kuyruksuz\nuse_category=Kategori Kullan\ncant_write_to_this_folder=Bu klasöre yazılamıyor\nfile_name_already_exists=Dosya adı zaten mevcut\ndownload_already_exists=İndirme zaten mevcut\ninvalid_file_name=Geçersiz dosya adı\nshow_solutions=Çözümleri göster...\nchange_solution=Çözümü değiştir\nselect_a_solution=Bir çözüm seçin\nselect_download_strategy_description=Sağladığınız bağlantı zaten indirme listelerinde, lütfen ne yapmak istediğinizi belirtin\ndownload_strategy_add_a_numbered_file=Numaralandırılmış bir dosya ekle\ndownload_strategy_add_a_numbered_file_description=İndirme dosya adının sonuna bir dizin ekle\ndownload_strategy_override_existing_file=Mevcut dosyanın üzerine yaz\ndownload_strategy_override_existing_file_description=Mevcut indirmeyi kaldır ve o dosyaya yaz\ndownload_strategy_update_download_link=Mevcut indirmeyi güncelle\ndownload_strategy_update_download_link_description=Mevcut indirme bağlantısını ve kimlik bilgilerini güncelle\ndownload_strategy_show_downloaded_file=İndirilen dosyayı göster\ndownload_strategy_show_downloaded_file_description=Zaten mevcut olan indirme öğesini göster, böylece devam et'e basabilir veya açabilirsiniz\nbatch_download_link_help=Joker karakterler içeren bir bağlantı girin (* kullanın)\ninvalid_url=Geçersiz URL\nlist_is_too_large_maximum_n_items_allowed=Liste çok büyük\\! En fazla {{count}} öğeye izin veriliyor\nenter_range=Aralık girin\nrange_from=Başlangıç\nrange_to=Bitiş\nbatch_download_wildcard_length=Joker karakter uzunluğu\nfirst_link=İlk Bağlantı\nlast_link=Son Bağlantı\nopen_source_software_used_in_this_app=Bu Uygulamada Kullanılan Açık Kaynaklı Yazılımlar\nlinks=Bağlantılar\nwebsite=Web Sitesi\ndevelopers=Geliştiriciler\nsource_code=Kaynak Kodu\nlicense=Lisans\nno_license_found=Lisans bulunamadı\norganization=Kuruluş\nadd_new_queue=Yeni Kuyruk Ekle\nqueue_name=Kuyruk Adı\nqueues=Kuyruklar\nstop_queue=Kuyruğu Durdur\nstart_queue=Kuyruğu Başlat\nclear_queue_items=Kuyruğu Boşalt\nconfig=Yapılandırma\nitems=Öğeler\nmove_down=Aşağı Taşı\nmove_up=Yukarı Taşı\nremove_queue=Kuyruğu Kaldır\nqueue_name_help=Bu kuyruk için bir ad belirtin\nqueue_name_describe=Kuyruk adı {{value}}\nqueue_max_concurrent_download=Maksimum eş zamanlı indirme\nqueue_max_concurrent_download_description=Bu kuyruk için maksimum indirme sayısı\nqueue_automatic_stop=Otomatik durdurma\nqueue_automatic_stop_description=İçinde öğe kalmadığında kuyruğu otomatik olarak durdur\nqueue_scheduler=Zamanlayıcı\nqueue_enable_scheduler=Zamanlayıcıyı Etkinleştir\nqueue_active_days=Aktif Günler\nqueue_active_days_description=Zamanlayıcı hangi günler çalışsın?\nqueue_scheduler_enable_auto_start_time=Otomatik Başlatma Zamanını Etkinleştir\nqueue_scheduler_auto_start_time=Otomatik Başlatma Zamanı\nqueue_scheduler_enable_auto_stop_time=Otomatik Durdurma Zamanını Etkinleştir\nqueue_scheduler_auto_stop_time=Otomatik Durdurma Zamanı\nqueue_shutdown_on_completion=Tamamlandığında Sistemi Kapat\nqueue_shutdown_on_completion_description=Bu kuyruk tamamlandığında veya zamanlanmış bitiş zamanına ulaşıldığında sistemi otomatik olarak kapat.\nappearance=Görünüm\ndownload_engine=İndirme Motoru\nbrowser_integration=Tarayıcı Entegrasyonu\nsettings_download_max_retries_count=Maksimum İndirme Deneme Sayısı\nsettings_download_max_retries_count_description=Uygulamanın, başarısız bir indirmeyi pes etmeden önce en fazla kaç kez yeniden deneyeceği\nsettings_download_max_retries_count_describe_no_retries=Başarısız indirmeler yeniden denenmeyecek\nsettings_download_max_retries_count_describe_n_retries=Başarısız indirmeler {{count}} kez yeniden denenecek\nsettings_download_thread_count=İş Parçacığı Sayısı\nsettings_download_thread_count_description=İndirme öğesi başına maksimum indirme iş parçacığı\nsettings_download_thread_count_describe=Bir indirme en fazla {{count}} iş parçacığına sahip olabilir\nsettings_download_thread_count_with_large_value_describe=Uyarı\\: Yüksek bir iş parçacığı sayısı ayarlamak, sistem kaynak kullanımını artırabilir, performansı düşürebilir veya sunucularla bağlantı sorunlarına neden olabilir. Yüksek değerleri yalnızca sisteminiz ve ağınız üzerindeki potansiyel etkisini anlıyorsanız kullanın.\nsettings_use_server_last_modified_time=Sunucunun Son Değiştirilme Zamanını Kullan\nsettings_use_server_last_modified_time_description=Bir dosyayı indirirken, yerel dosya için sunucunun son değiştirilme zamanını kullan\nsettings_append_extension_to_incomplete_downloads=Tamamlanmamış İndirmelere Uzantı Ekle\nsettings_append_extension_to_incomplete_downloads_description=Tamamlanmamış indirmelere \".part\" uzantısını ekle. Bu, bitmemiş indirmeleri tanımlamaya yardımcı olur ve eksik dosyaların yanlışlıkla açılmasını önler.\nsettings_use_sparse_file_allocation=Seyrek Dosya Ayırma Kullanımı\nsettings_use_sparse_file_allocation_description=Gereksiz veri yazımını azaltarak, özellikle SSD'lerde dosyaları daha verimli oluşturun. Bu, indirme başlangıcını hızlandırabilir ve disk kullanımını azaltabilir. İndirmeler yavaş başlıyorsa veya olağandışı indirme hızları yaşıyorsanız, bazı cihazlarda tam olarak desteklenmeyebileceği için bu seçeneği devre dışı bırakmayı düşünün.\nsettings_ignore_ssl_certificates=SSL Sertifikalarını Yoksay\nsettings_ignore_ssl_certificates_description=SSL sertifika doğrulamasını devre dışı bırakır. Bağlantınızı güvenlik risklerine maruz bırakabileceğinden, yalnızca gerekliyse kullanın.\nsettings_global_speed_limiter=Genel Hız Sınırlayıcı\nsettings_global_speed_limiter_description=Genel indirme hızı sınırı (0, sınırsız anlamına gelir)\nsettings_show_average_speed=Ortalama Hızı Göster\nsettings_show_average_speed_description=İndirme hızını ortalama veya anlık olarak göster\nsettings_use_category_by_default=Varsayılan Olarak Kategoriyi Kullan\nsettings_use_category_by_default_description=Bir indirme eklerken varsayılan olarak kategori kullan.\nsettings_default_download_folder=Varsayılan İndirme Klasörü\nsettings_default_download_folder_description=Yeni bir indirme eklediğinizde bu konum varsayılan olarak kullanılır\nsettings_default_download_folder_describe=\"{{folder}}\" kullanılacak\nsettings_use_proxy=Proxy Kullan\nsettings_use_proxy_description=Dosyaları indirmek için proxy kullan\nsettings_use_proxy_describe_no_proxy=Proxy kullanılmayacak\nsettings_use_proxy_describe_system_proxy=Sistem Proxy'si kullanılacak\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" kullanılacak\nsettings_use_proxy_describe_pac_proxy=PAC dosyası \"{{value}}\" kullanılacak\nsettings_track_deleted_files_on_disk=Diskteki Silinmiş Dosyaları İzle\nsettings_track_deleted_files_on_disk_description=Dosyalar indirme dizininden silindiğinde veya taşındığında listeden otomatik olarak kaldır.\nsettings_delete_partial_file_on_download_cancellation=İndirme İptalinde Kısmi Dosyayı Sil\nsettings_delete_partial_file_on_download_cancellation_description=Bir indirme iptal edildiğinde, kısmen indirilmiş dosya diskten silinir. Bu, indirme klasörünüzü temiz tutmaya yardımcı olur ve gereksiz disk alanı kullanımını azaltır. Ancak, bir sonraki başlatışınızda indirme baştan başlayacaktır.\nsettings_default_user_agent=Varsayılan User Agent\nsettings_default_user_agent_description=İsteklerin sunuculara nasıl tanımlandığını belirtmek için Varsayılan User Agent dizesini belirleyin. Bu, belirli cihazlar için optimize edilmiş içeriğe erişmeye veya bazı web sitelerinin uyguladığı indirme sınırlamalarını aşmaya yardımcı olabilir.\nsettings_download_size_unit=İndirme Boyutu Birimi\nsettings_download_size_unit_description=İndirme boyutunu göstermek için kullanılan birim\nsettings_download_speed_unit=İndirme Hızı Birimi\nsettings_download_speed_unit_description=İndirme hızını göstermek için kullanılan birim\nsettings_theme=Tema\nsettings_theme_description=Uygulama için bir tema seçin\nsettings_default_dark_theme=Varsayılan Koyu Tema\nsettings_default_dark_theme_description=Uygulama sistem temasını takip ettiğinde ve karanlık mod aktif olduğunda uygulanır\nsettings_default_light_theme=Varsayılan Açık Tema\nsettings_default_light_theme_description=Uygulama sistem temasını takip ettiğinde ve aydınlık mod aktif olduğunda uygulanır\nsettings_font=Yazı Tipi\nsettings_font_description=Uygulama arayüzünde kullanılan yazı tipini değiştirin, bazı yazı tipleri uygulamada doğru görüntülenmeyebilir.\nsettings_ui_scale=Arayüz Ölçeği\nsettings_ui_scale_description=Uygulamanın arayüz elemanlarının boyutunu ayarlayın\nsettings_language=Dil\nsettings_compact_top_bar=Kompakt Üst Çubuk\nsettings_compact_top_bar_description=Ana pencere yeterli genişliğe sahip olduğunda üst çubuğu başlık çubuğuyla birleştir\nsettings_use_native_menu_bar=Yerel Menü Çubuğunu Kullan\nsettings_use_native_menu_bar_description=Sistemin varsayılan menü çubuğu stilini kullan\nsettings_use_relative_date_time=Göreceli tarih/saat kullan\nsettings_use_relative_date_time_description=Uygulamadaki tarihler için göreceli tarih/saat formatını kullanın (örneğin, tam tarih/saat yerine \"2 gün önce\")\nsettings_show_icon_labels=Simge Etiketlerini Göster\nsettings_show_icon_labels_description=Mümkün olduğunda simgelerin altında etiketleri göster (örneğin ana araç çubuğu eylemleri)\nsettings_use_system_tray=Sistem Tepsisini Kullan\nsettings_use_system_tray_description=Uygulama çalışırken sistem tepsisi simgesini göster\nsettings_start_on_boot=Açılışta Başlat\nsettings_start_on_boot_description=Kullanıcı oturum açtığında uygulamayı otomatik başlat\nsettings_notification_sound=Bildirim Sesi\nsettings_notification_sound_description=Yeni bildirim geldiğinde ses çal\nsettings_browser_integration=Tarayıcı Entegrasyonu\nsettings_browser_integration_description=Tarayıcılardan indirmeleri kabul et\nsettings_browser_integration_server_port=Sunucu Portu\nsettings_browser_integration_server_port_description=Tarayıcı entegrasyonu için port\nsettings_browser_integration_server_port_describe=Uygulama {{port}} portunu dinleyecek\nsettings_dynamic_part_creation=Dinamik parça oluşturma\nsettings_dynamic_part_creation_description=Bir parça bittiğinde, indirme hızını artırmak için diğer parçaları bölerek başka bir parça oluştur.\nsettings_show_completion_dialog=İndirme Tamamlanma Kutusunu Göster\nsettings_show_completion_dialog_description=Bir indirme tamamlandığında \"İndirme Tamamlandı\" iletişim kutusunu otomatik olarak göster.\nsettings_show_download_progress_dialog=İndirme İlerlemesi iletişim kutusunu göster\nsettings_show_download_progress_dialog_description=Bir indirme başladığında \"İndirme İlerlemesi\" iletişim kutusunu otomatik olarak göster.\nsettings_per_host_settings=Sunucu Başına Ayarlar\nsettings_per_host_settings_descriptions=Bu ayarlar, belirtilen sunucu ile eşleşen her yeni indirmeye otomatik olarak uygulanacaktır.\nsettings_download_max_concurrent_downloads=Maximum Concurrent Downloads\nsettings_download_max_concurrent_downloads_description=The maximum number of files that can be downloaded at the same time (downloads managed by queues are not counted; set to 0 for unlimited)\ndownload_item_settings_speed_limit=Hız Sınırı\ndownload_item_settings_speed_limit_description=Bu öğe için indirme hızını sınırla\ndownload_item_settings_show_download_completion_dialog=İndirme Tamamlandı iletişim kutusunu göster\ndownload_item_settings_show_download_completion_dialog_description=Bu indirme tamamlandığında \"İndirme Tamamlandı\" iletişim kutusunu otomatik olarak göster.\ndownload_item_settings_shutdown_on_completion=Tamamlandığında Sistemi Kapat\ndownload_item_settings_shutdown_on_completion_description=Bu indirme tamamlandığında sistemi otomatik olarak kapat.\ndownload_item_settings_thread_count=İş Parçacığı sayısı\ndownload_item_settings_thread_count_description=Bu indirme öğesini indirmek için kaç iş parçacığı kullanılacak (0 varsayılan için)\ndownload_item_settings_thread_count_describe=Bu indirme için {{count}} iş parçacığı\ndownload_item_settings_username_description=Bağlantı korumalı bir kaynak ise bir kullanıcı adı sağlayın\ndownload_item_settings_password_description=Bağlantı korumalı bir kaynak ise bir şifre sağlayın\ndownload_item_settings_download_page=İndirme Sayfası\ndownload_item_settings_download_page_description=Bu indirmenin başlatıldığı web sayfası\ndownload_item_settings_file_checksum=Dosya Sağlaması\ndownload_item_settings_file_checksum_description=Dosyanın doğru indirilip indirilmediğini kontrol etmek için kullanılabilecek bir hash dizesi\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Bu öğe için özel Kullanıcı Aracısı (User-Agent) (varsayılanı kullanmak için boş bırakın)\nfile_checksum=Dosya Sağlaması\nfile_checksum_page=Dosya Sağlama Denetleyicisi\nfile_checksum_page_file_checksum_default_algorithm=Varsayılan Algoritma\nfile_checksum_page_file_checksum_default_algorithm_help=Dosya sağlamaları belirtilmediğinde hesaplamak için kullanılan varsayılan algoritma.\nstart=Başlat\ncalculated_checksum=Hesaplanan Sağlama\nsaved_checksum=Kaydedilen Sağlama\nchecksum_algorithm=Algoritma\nfile_not_found=Dosya bulunamadı\ndownload_not_finished=İndirme tamamlanmadı\ndone=Bitti\nwaiting=Bekliyor\nmatches=Eşleşiyor\nnot_matches=Eşleşmiyor\ncopy_to_clipboard=Panoya Kopyala\nusername=Kullanıcı Adı\npassword=Şifre\naverage_speed=Ortalama Hız\nexact_speed=Anlık Hız\nunlimited=Sınırsız\nuse_global_settings=Genel Ayarları Kullan\ncant_run_browser_integration=Tarayıcı entegrasyonu çalıştırılamıyor\ncant_open_file=Dosya Açılamıyor\ncant_open_folder=Klasör Açılamıyor\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} yıl\nrelative_time_long_months={{months}} ay\nrelative_time_long_days={{days}} gün\nrelative_time_long_hours={{hours}} saat\nrelative_time_long_minutes={{minutes}} dakika\nrelative_time_long_seconds={{seconds}} saniye\nrelative_time_short_years={{years}} y\nrelative_time_short_months={{months}} ay\nrelative_time_short_days={{days}} g\nrelative_time_short_hours={{hours}} sa\nrelative_time_short_minutes={{minutes}} dk\nrelative_time_short_seconds={{seconds}} sn\nrelative_time_left={{time}} kaldı\nrelative_time_ago={{time}} önce\nauto=Otomatik\nunspecified=Belirtilmemiş\ncustom=Özel\nicon=Simge\nauthor=Yazar\nlink=Bağlantı\nsize=Boyut\nstatus=Durum\nparts_info_downloaded_size=İndirilen\nparts_info_total_size=Toplam\nspeed=Hız\ntime_left=Kalan Süre\ndate_added=Eklenme Tarihi\ninfo=Bilgi\ndownload_page_downloaded_size=İndirilen\ndownload_page_download_completed=İndirme Tamamlandı\nresume_support=Devam Desteği\nyes=Evet\nno=Hayır\nparts_info=Parça Bilgisi\ndisconnected=Bağlantı Kesildi\nreceiving_data=Veri Alınıyor\nconnecting=Bağlanılıyor\nwarning=Uyarı\nunsupported_resume_warning=Bu indirme devam etmeyi desteklemiyor\\! İndirme Listesinde daha sonra YENİDEN BAŞLATMANIZ gerekebilir\nstop_anyway=Yine de Durdur\ncustomize_columns=Sütunları Özelleştir\nreset=Sıfırla\nmonday=Pazartesi\ntuesday=Salı\nwednesday=Çarşamba\nthursday=Perşembe\nfriday=Cuma\nsaturday=Cumartesi\nsunday=Pazar\nproxy_open_system_proxy_settings=Sistem Proxy Ayarlarını Aç\nproxy_type=Proxy türü\nproxy_do_not_use_proxy_for=Proxy kullanılmayacak adresler\nproxy_do_not_use_proxy_for_description=Proxy kullanılmayacak URL'lerin listesi\\n* ile joker karakter kullanabilirsiniz\\nörneğin 192.168.1.* example.com (boşlukla ayrılmış)\nproxy_change_title=Proxy Değiştir\nchange_proxy=Proxy Değiştir\nproxy_no=Proxy Yok\nproxy_system=Sistem Proxy'si\nproxy_manual=Manuel Proxy\nproxy_pac=Proxy Otomatik Yapılandırma\nproxy_pac_url=Proxy Otomatik Yapılandırma URL'si\naddress=Adres\nport=Port\naddress_and_port=Adres & Port\nuse_authentication=Kimlik Doğrulama Kullan\nwarning_you_may_have_to_restart_the_download_later=İndirmeyi daha sonra yeniden başlatmanız gerekebilir\\!\nedit_download_title=İndirmeyi Düzenle\nedit_download_update_from_download_page=İndirme Sayfasından Güncelle\nedit_download_update_from_download_page_description=Bu pencere açıkken, İndirme Sayfasına gidip indirme düğmesine tıklayabilirsiniz. Uygulama, yeni indirme kimlik bilgilerini yakalayacak ve güncelleyecektir, böylece onları kaydedebilirsiniz.\nedit_download_saved_download_item_size_not_match=Kaydedilen indirme öğesinin boyutu {{currentSize}}, yeni boyut olan {{newSize}} ile eşleşmiyor.\ntranslators_page_thanks=Bu Projenin Çevrilmesine Yardımcı Olanlara Minnetle ❤️\ntranslators=Çevirmenler\nlanguage=Dil\ntranslators_contribute_title=Çevirileri İyileştir\ntranslators_contribute_description=Bu projeyi geliştirmeye yardımcı olmak ister misiniz? Diliniz listede yoksa veya bazı düzenlemelere ihtiyacı varsa, çevirilerinize katkıda bulunabilir ve daha iyi hale getirebilirsiniz\\!\ncontribute=Katkıda Bulun\nmeet_the_translators=Çevirmenlerle Tanışın\nlocalized_by_translators=Çevirmenler Tarafından Yerelleştirildi\nconfirm_exit=Çıkışı Onayla\nconfirm_exit_description=AB İndirme Yöneticisi'nden çıkmak istediğinizden emin misiniz?\\nAktif indirmeler/kuyruklar durdurulacaktır\\!\nupdate=Güncelle\nupdate_updater=Güncelleyici\nupdate_available=Güncelleme Mevcut\nupdate_error=Güncelleme Hatası\nupdate_available_suggest_to_to_update=Yeni özelliklerin, geliştirmelerin ve performans iyileştirmelerinin tadını çıkarmak için en son sürüme güncelleyebilirsiniz.\nupdate_release_notes=Sürüm Notları\nupdate_check_for_update=Güncellemeleri Kontrol Et\nupdate_checking_for_update=Güncellemeler kontrol ediliyor\nupdate_no_update=En son sürümü kullanıyorsunuz\nupdate_check_error=Güncelleme kontrolü sırasında hata oluştu\nupdate_app_updated_to_version_n=Uygulama {{version}} sürümüne güncellendi\ncreate_desktop_entry=Masaüstü Kısayolu Oluştur\nshutdown_alert=Kapatma Uyarısı\nsystem_shutdown_soon=Sistem Yakında Kapanacak\\!\nsystem_shutdown_failed=Sistem Kapatma Başarısız Oldu\\!\nsystem_shutdown_soon_description=Sistem yakında kapanacak. Bilgisayarı hala kullanıyorsanız, lütfen çalışmanızı kaydedin veya kapatma işlemini iptal edin.\nsystem_shutdown_reason_queue_completed=Kuyruktaki tüm indirmeler tamamlandı.\nsystem_shutdown_reason_queue_end_time_reached=İndirme kuyruğu için zamanlanmış bitiş zamanına ulaşıldı.\nsystem_shutdown_download_finished=İndirme tamamlandı.\nshutdown_now=Hemen Kapat\nsettings_per_host_settings_new_host=<Yeni Sunucu>\nsettings_per_host_settings_not_selected=Önce yeni bir öğe oluşturun veya seçin\\!\nsettings_per_host_settings_host=Sunucu\nsettings_per_host_settings_host_description=Bu ayarlar, bu hostname (sunucu adı) ile eşleşen indirmelere uygulanacaktır. Joker (*) karakteri desteklenir (örneğin\\: example.com, *.example.com — yalnızca birini kullanın).\nsettings_browser_in_launcher=Başlatıcıda Tarayıcı Simgesi\nsettings_browser_in_launcher_description=Tarayıcı simgesini menüde göster veya gizle.\nsort_by=Sıralama Ölçütü\nwelcome=Hoşgeldiniz\nnew_folder=Yeni Klasör\nskip=Geç\nlets_go=Hadi\nnext=Sonraki\nselect_all=Tümünü Seç\nselect_inside=İçeriği Seç\nselect_invert=Seçimi Tersine Çevir\nopen_settings=Ayarlar\nback=Geri\nservice_is_running=Hizmet Çalışıyor\ninitial_setup_description=Hadi Ayarlayalım\ninitial_setup_notice=Bu ayarları istediğiniz zaman değiştirebilirsiniz\npermission_granted=Erişim Onaylandı\npermission_not_granted=Erişim Reddedildi\npermissions=Erişim\ngive_permission=Erişime izin ver\ngive_storage_permission=Depolama erişimine izin ver\nstorage_roots=Storage Roots\npermissions_initial_title=İzinleri yapılandır\npermissions_initial_description=Uygulamanın doğru çalışması için bazı izinler gereklidir. Bir sonraki ekranda izinlerin amacını görebilir; dilediğinizi onaylayabilir veya atlayabilirsiniz.\npermissions_done_title=Her şey hazır\npermissions_done_description=Her şey hazır. Gerekli tüm izinler verildi ve uygulama kullanıma hazır.\npermissions_manage_storage_title=Depolama izinlerini yönet\npermissions_manage_storage_reason=Bu izin indirme klasörünü değiştirme, kopya dosyaları algılama ve ek özellikleri kullanma imkanı sunar. İsteğe bağlıdır ancak tam performans için önerilir.\npermission_read_write_external_storage_title=Okuma ve yazma erişimi\npermission_read_write_external_storage_reason=Bu izinle uygulama indirilenleri kaydedip yönetebilir, indirme konumunu değiştirebilir ve kopya dosyaları daha iyi tespit edebilir.\npermissions_post_notification_title=Bildirimlere izin ver\npermissions_post_notification_reason=İndirmeleri yönetmek için uygulamanın arka planda çalışması gerekir. Bildirimler, hem sizi bilgilendirmek hem de arka planda çalışmayı sürdürmek için kullanılır.\npermissions_ignore_battery_optimization_title=Pil Optimizasyonunu Yoksay\npermissions_ignore_battery_optimization_reason=Bazı cihazlar pil tasarrufu için arka plan işlemlerini kısıtlayarak indirmeleri durdurabilir. İndirmelerin kesintisiz sürmesi için uygulamayı isteğe bağlı olarak pil optimizasyonu dışında bırakabilirsiniz\nopen_in_browser=Tarayıcıda Aç\nbrowser=Tarayıcı\nbrowser_new_tab=Yeni Sekme\nbrowser_close_tab=Sekmeyi Kapat\nbrowser_open_in_new_tab=Yeni Sekmede Aç\nbrowser_open_in_new_background_tab=Arka Planda Aç\nbrowser_no_tab_open=Hiçbir Sekme Açık Değil\nbrowser_tabs=Sekmeler\nbrowser_paste_and_go=Yapıştır ve devam et\nbrowser_bookmarks=Yer İşaretleri\nbrowser_add_bookmark=Yer İşareti Ekle\nbrowser_edit_bookmark=Yer İşaretini Düzenle\nbrowser_add_to_bookmarks=Yer İşaretlerine Ekle\nbrowser_remove_from_bookmarks=Yer İşaretlerinden Kaldır\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/uk_UA.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Автоматична категоризація завантажень\nconfirm_auto_categorize_downloads_description=Елементи без категорії будуть автоматично додаватись до відповідних категорій.\nconfirm_reset_to_default_categories_title=Відновлення типових категорій\nconfirm_reset_to_default_categories_description=Всі категорії будуть видалені та відновлені за замовчуванням\\!\nconfirm_delete_download_items_title=Підтвердження видалення\nconfirm_delete_download_items_description=Видалити {{count}} елементів ?\nconfirm_delete_download_unfinished_items_description=Ви дійсно бажаєте видалити {{count}} незавершених завантажень ?\nconfirm_delete_download_finished_and_unfinished_items_description=Ви дійсно бажаєте видалити {{finishedCount}} завершених та {{unfinishedCount}} незавершених завантажень ?\nalso_delete_file_from_disk=Видалити файл з диска\nconfirm_delete_category_item_title=Видалення {{name}} категорії\nconfirm_delete_category_item_description=Бажаєте видалити категорію {{value}} ?\nyour_download_will_not_be_deleted=Завантаження не будуть видалені\ndrag_the_file_to_another_app=Перетягнути файл в інший застосунок\ndrop_link_or_file_here=Перетягніть посилання або файл сюди.\nnothing_will_be_imported=Імпортувати нічого\nn_links_will_be_imported=Посилань буде імпортовано\\: {{count}}\nn_items_selected=Елементів обрано\\: {{count}}\nwindow_close=Закрити\nwindow_minimize=Згорнути\nwindow_maximize=Розгорнути\nwindow_restore=Відновити\ndelete=Видалити\nremove=Видалити\ncancel=Скасувати\nclose=Закрити\nmenu=Меню\nmore_options=Інші параметри\nok=Ок\nadd=Додати\npaste=Вставити\nchange=Змінити\nedit=Редагувати\nchange_anyway=Застосувати зміни\ndownload=Завантажити\nrefresh=Оновити\nsettings=Налаштування\non_completion=Після завершення\nunknown=Невідомо\nunknown_error=Невідома помилка\ndownload_item_not_found=Завантаження не знайдено\nname=Ім'я\ndownload_link=Посилання\nnot_finished=Не завершено\nall=Всі\nfinished=Завантажено\nUnfinished=Не завантажено\ncanceled=Скасовані\nerror=З помилкою\npaused=Призупинено\ndownloading=Завантаження\nadded=Додано\nidle=Призупинено\npreparing_file=Підготовка файлу\ncreating_file=Створення файлу\nresuming=Відновлення\nretrying=Повторна спроба\nlist_is_empty=Список пустий\\!\nsearch_in_the_list=Пошук в списку\nsearch=Пошук\nclear=Очистити\ngeneral=Основне\nenabled=Увімкнено\ndisabled=Вимкнено\ndefault=За замовчуванням\nfile=Файл\ntasks=Задачі\ntools=Інструменти\nhelp=Допомога\nsystem=Система\nall_missing_files=Усі відсутні файли\nall_finished=Всі завершені\nall_unfinished=Всі незавершені\nentire_list=Весь список\ndownload_browser_integration=Завантажити інтеграцію з браузером\nexit=Вийти\nshow_downloads=Показати завантаження\nnew_download=Нове завантаження\nstop_all=Зупинити всі\nimport_from_clipboard=Імпортувати з буферу обміну\nbatch_download=Пакетне завантаження\nopen=Відкрити\nshare=Поділитися\nopen_file=Відкрити файл\nopen_folder=Відкрити каталог\nresume=Продовжити\npause=Призупинити\nrestart_download=Перезавантажити\ncopy=Копіювати\ncopy_link=Копіювати посилання\ncopy_as_curl=Копіювати як cURL\nshow_properties=Показати властивості\nmove_to_queue=Перемістити до черги\nmove_to_this_queue=Перемістити до цієї черги\nmove_to_category=Перемістити до категорії\nmove_to_this_category=Перемістити до цієї категорії\ncategories=Категорії\nadd_category=Додати категорію\nedit_category=Редагувати категорію\ndelete_category=Видалити категорію\ncategory_name=Назва категорії\ncategory_download_location=Розташування завантажень в категорії\ncategory_download_location_description=Використовувати цей каталог в якості розташування завантаження, якщо ця категорія обрана у вікні \"Додавання завантаження\"\ncategory_file_types=Типи файлів в категорії\ncategory_file_types_description=Автоматично додавати ці типи файлів до цієї категорії при додаванні завантаження.\\nРозділіть розширення файлів за допомогою пробілу (ext1 ext2 ...)\ncategory_url_patterns=Шаблони URL\ncategory_url_patterns_description=Автоматично додати завантаження із зазначених посилань до цієї категорії (при доданні нового завантаження).\\nРозділіть посилання пробілом. Можна використовувати * підставляння\nauto_categorize_downloads=Автоматично категоризувати завантаження\nrestore_defaults=Відновити налаштування за замовчуванням\nabout=Про застосунок\nversion_n=Версія {{value}}\ndeveloped_with_love_for_you=Створено з ❤️ до вас\ndonate=Пожертвувати\nvisit_the_project_website=Перейти на веб-сайт проекту\nthis_is_a_free_and_open_source_software=Це безкоштовне та вільне програмне забезпечення\nview_the_source_code=Переглянути початковий код\nthird_party_libraries=Сторонні бібліотеки\npowered_by_open_source_software=Powered by Open Source Software\nview_the_open_source_licenses=Переглянути ліцензію з відкритим кодом\nsupport_and_community=Підтримка та спільнота\ntelegram=Telegram\nchannel=Канал\ngroup=Група\nadd_download=Додавання завантаження\nadd_multi_download_page_header=Оберіть елементи, котрі бажаєте завантажити\\n\nsave_to=Зберегти до\nwhere_should_each_item_saved=Куди потрібно зберегти кожен елемент?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Є кілька предметів\\! Будь ласка, оберіть спосіб їх збереження\neach_item_on_its_own_category=Кожен елемент в окремій категорії\neach_item_on_its_own_category_description=Кожен елемент буде розміщуватися у категорії, яка має вказаний тип файлу\nall_items_in_one_category=Всі елементи в одній категорії\nall_items_in_one_category_description=Все елементи будуть збережені до каталогу обраної категорії\nall_items_in_one_Location=Всі елементи в одному місці\nall_items_in_one_Location_description=Все елементи будуть збережені до обраного каталогу\nunselected_all_items_in_specific_location_description=Всі файли будуть збережені до розташування обраної категорії\nno_category_selected=Категорія не обрана\nno_categories_found=Категорії не знайдено\ndownload_location=Каталог завантаження\nlocation=Розташування\nselect_queue=Обрати чергу\nwithout_queue=Без черги\nuse_category=Використати категорію\ncant_write_to_this_folder=Не вдалося записати до цього каталогу\nfile_name_already_exists=Файл з таким ім'ям вже існує\ndownload_already_exists=Завантаження вже існує\ninvalid_file_name=Некоректне ім'я файлу\nshow_solutions=Показати рішення...\nchange_solution=Змінити рішення\nselect_a_solution=Оберіть рішення\nselect_download_strategy_description=Завантаження з таким посиланням вже є у списку завантажень. Що бажаєте зробити ?\ndownload_strategy_add_a_numbered_file=Додати номер до назви файлу\ndownload_strategy_add_a_numbered_file_description=В кінець назви нового завантаження буде додано числовий індекс.\ndownload_strategy_override_existing_file=Перезаписати наявний файл\ndownload_strategy_override_existing_file_description=Наявне завантаження буде видалено зі списку, а файл — перезаписано.\ndownload_strategy_update_download_link=Оновити чинне завантаження\ndownload_strategy_update_download_link_description=Оновити наявне посилання завантаження та його параметри\ndownload_strategy_show_downloaded_file=Показати відомості про завантажений файл\ndownload_strategy_show_downloaded_file_description=Буде показано вікно з властивостями вже наявного завантаження, щоб ви могли продовжити завантаження та подивитись відомості про нього.\nbatch_download_link_help=Введіть посилання, що містить символи підстановки (використовуйте  *)\ninvalid_url=Некоректна адреса URL\nlist_is_too_large_maximum_n_items_allowed=Список занадто великий. Дозволена кількість\\: {{count}}\nenter_range=Введіть діапазон\nrange_from=З\nrange_to=До\nbatch_download_wildcard_length=Довжина підстановки\nfirst_link=Перше посилання\nlast_link=Останнє посилання\nopen_source_software_used_in_this_app=Програмне забезпечення з відкритим кодом використовується в цьому застосунку\nlinks=Посилання\nwebsite=Веб-сайт\ndevelopers=Розробники\nsource_code=Початковий код\nlicense=Ліцензія\nno_license_found=Ліцензію не знайдено\norganization=Організація\nadd_new_queue=Додати нову чергу\nqueue_name=Назва черги\\:\nqueues=Черги\nstop_queue=Зупинити чергу\nstart_queue=Запустити чергу\nclear_queue_items=Очистити чергу\nconfig=Конфігурація\nitems=Елементи\nmove_down=Перемістити вниз\nmove_up=Перемістити вгору\nremove_queue=Видалити чергу\nqueue_name_help=Вкажіть назву цієї черги\nqueue_name_describe=Назва черги {{value}}\nqueue_max_concurrent_download=Кількість одночасних завантажень\nqueue_max_concurrent_download_description=Максимальна кількість одночасних завантажень для цієї черги\nqueue_automatic_stop=Автоматична зупинка\nqueue_automatic_stop_description=Автоматично зупинити чергу при відсутності елементів в ній.\nqueue_scheduler=Планувальник\nqueue_enable_scheduler=Увімкнути\nqueue_active_days=Дні\nqueue_active_days_description=В які дні буде працювати планувальник ?\nqueue_scheduler_enable_auto_start_time=Почати завантаження у вказаний час\nqueue_scheduler_auto_start_time=Час автоматичного запуску\nqueue_scheduler_enable_auto_stop_time=Зупинити завантаження у вказаний час\nqueue_scheduler_auto_stop_time=Час автоматичної зупинки\nqueue_shutdown_on_completion=Вимкнути комп'ютер\nqueue_shutdown_on_completion_description=Автоматично вимкнути комп'ютер коли черга буде завершена, або настане час зупинки черги.\nappearance=Зовнішній вигляд\ndownload_engine=Завантажувач\nbrowser_integration=Інтеграція з браузером\nsettings_download_max_retries_count=Кількість спроб відновлення завантаження\nsettings_download_max_retries_count_description=Застосунок буде намагатись автоматично відновити невдале завантаження вказану кількість разів. Якщо кількість спроб буде вичерпана - завантаження буде зупинено.\nsettings_download_max_retries_count_describe_no_retries=Завантаження з помилкою не будуть відновлені\nsettings_download_max_retries_count_describe_n_retries=Завантаження з помилкою будуть відновлені {{count}} разів\nsettings_download_thread_count=Кількість потоків\nsettings_download_thread_count_description=Максимальна кількість з'єднань (потоків) на завантаження.\nsettings_download_thread_count_describe=Для завантаження файлу буде використано до {{count}} потоків\nsettings_download_thread_count_with_large_value_describe=Попередження\\: висока кількість потоків завантаження може збільшити споживання ресурсів системи, зменшити продуктивність або призвести до проблем із підключенням до серверів, що може унеможливити подальше завантаження файлу. Використовуйте великі значення тільки в тому випадку, якщо розумієте потенційний ризик впливу на вашу систему та мережу.\nsettings_use_server_last_modified_time=Отримувати час редагування файлу з сервера\nsettings_use_server_last_modified_time_description=Використовувати дату редагування файлу, що вказана на сервері.\nsettings_append_extension_to_incomplete_downloads=Додавати розширення \".part\" до незавершених завантажень\nsettings_append_extension_to_incomplete_downloads_description=До незавершених завантажень буде додаватись розширення \".part\". Ця можливість допоможе розпізнати недозавантажені файли та запобігти їх застосуванню.\nsettings_use_sparse_file_allocation=Оптимізоване розподілення файлів\nsettings_use_sparse_file_allocation_description=Ефективніше створюйте файли, особливо на SSD, за рахунок зменшення непотрібного запису даних. Це може пришвидшити завантаження та зменшити використання диска. Якщо завантаження починаються повільно або у вас спостерігається незвичайна швидкість завантаження, вимкніть цю опцію, оскільки вона може не підтримуватися на деяких пристроях.\nsettings_ignore_ssl_certificates=Ігнорувати SSL-сертифікати\nsettings_ignore_ssl_certificates_description=Вимикає перевірку сертифікатів SSL. Рекомендується використовувати лише тоді, коли це дійсно необхідно, оскільки це може призвести до зниження захисту вашого з'єднання.\nsettings_global_speed_limiter=Загальне обмеження швидкості\nsettings_global_speed_limiter_description=Загальне обмеження швидкості завантажень.\nsettings_show_average_speed=Показувати середню швидкість\nsettings_show_average_speed_description=Показувати середню або точну швидкість завантаження.\nsettings_use_category_by_default=Категоризація завантажень\nsettings_use_category_by_default_description=Автоматично переміщувати завантаження до відповідної категорії під час його додавання.\nsettings_default_download_folder=Каталог завантажень за замовчуванням\nsettings_default_download_folder_description=В цьому каталозі будуть розташовані всі завантаження.\nsettings_default_download_folder_describe=\"{{folder}}\" буде використано\nsettings_use_proxy=Проксі-сервер\nsettings_use_proxy_description=Використовувати проксі для завантаження файлів\nsettings_use_proxy_describe_no_proxy=Проксі-сервер не використовується\nsettings_use_proxy_describe_system_proxy=Буде використано системний проксі-сервер\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" буде використано\nsettings_use_proxy_describe_pac_proxy=pac-файл \"{{value}}\" буде використано \nsettings_track_deleted_files_on_disk=Відстежувати вилучені з диску файли\nsettings_track_deleted_files_on_disk_description=Автоматично вилучати файли зі списку, якщо вони були переміщені чи видалені з каталогу завантаження.\nsettings_delete_partial_file_on_download_cancellation=Видаляти недозавантажений файл при скасуванні завантаження\nsettings_delete_partial_file_on_download_cancellation_description=Недозавантажені файли будуть видалятись після скасування завантаження. Ця функція допоможе підтримувати порядок в каталозі завантажень та зменшить зайве використання пам'яті накопичувача. Однак, наступного разу, коли ви розпочнете завантаження, воно почнеться з початку.\nsettings_default_user_agent=User-Agent\nsettings_default_user_agent_description=Вкажіть рядок User-Agent, щоб визначити спосіб ідентифікації запитів на серверах. Це може допомогти отримати доступ до матеріалів, що оптимізовані для деяких пристроїв, або обійти обмеження завантаження на певних вебсайтах.\nsettings_download_size_unit=Одиниця вимірювання обсягу даних\nsettings_download_size_unit_description=Одиниця вимірювання, що буде використовуватись для відображення розміру завантажень (обсягу даних).\nsettings_download_speed_unit=Одиниця вимірювання швидкості завантаження\nsettings_download_speed_unit_description=Одиниця вимірювання, що буде використовуватись для виведення швидкості завантаження.\nsettings_theme=Тема оформлення\nsettings_theme_description=Тема оформлення застосунку\nsettings_default_dark_theme=Системна темна тема\nsettings_default_dark_theme_description=Ця тема буде застосована, якщо в системі увімкнена темна тема.\nsettings_default_light_theme=Системна світла тема\nsettings_default_light_theme_description=Ця тема буде застосована, якщо в системі увімкнена світла тема.\nsettings_font=Шрифт\nsettings_font_description=За допомогою цього параметру можна змінити шрифт інтерфейсу застосунку. Зверніть увагу\\: деякі шрифти можуть некоректно відображатись в застосунку.\nsettings_ui_scale=Масштабування інтерфейсу\nsettings_ui_scale_description=Цей параметр дозволяє налаштувати розмір елементів інтерфейсу застосунку.\nsettings_language=Мова\nsettings_compact_top_bar=Компактна верхня панель\nsettings_compact_top_bar_description=Об'єднати верхню панель з заголовком, коли основне вікно має достатню ширину.\nsettings_use_native_menu_bar=Використовувати нативну панель меню\nsettings_use_native_menu_bar_description=Для панелі меню буде застосований системний стиль\nsettings_use_relative_date_time=Використовувати відносну дату/час\nsettings_use_relative_date_time_description=Використовувати відносний формат дати/часу в застосунку (наприклад, \"2 дні тому\" замість точної дати та часу).\nsettings_show_icon_labels=Показувати мітки піктограм\nsettings_show_icon_labels_description=Показувати мітки піктограм на панелі інструментів коли це можливо.\nsettings_use_system_tray=Використовувати системний лоток\nsettings_use_system_tray_description=Показувати іконку застосунку в системному лотку, коли застосунок запущений.\nsettings_start_on_boot=Запускати під час завантаження\nsettings_start_on_boot_description=Автозапуск застосунку при вході користувача в систему.\nsettings_notification_sound=Звук сповіщення\nsettings_notification_sound_description=Програвати звук сповіщення\nsettings_browser_integration=Інтеграція з браузером\nsettings_browser_integration_description=Дозволити захоплення завантажень з браузера. Для використання цієї функції необхідно встановити додаток для браузера.\nsettings_browser_integration_server_port=Порт сервера\nsettings_browser_integration_server_port_description=Порт для інтеграції з браузером\nsettings_browser_integration_server_port_describe=Застосунок буде прослуховувати порт {{port}}\nsettings_dynamic_part_creation=Створювати динамічні фрагменти\nsettings_dynamic_part_creation_description=Створювати новий фрагмент шляхом розбиття інших, коли завантаження фрагменту завершено. Ця функція підвищую швидкість завантаження файлу.\nsettings_show_completion_dialog=Показувати діалог завершення завантаження\nsettings_show_completion_dialog_description=Показувати вікно \"Завершення завантаження\" коли завантаження файлу завершено.\nsettings_show_download_progress_dialog=Показувати діалог прогресу завантаження\nsettings_show_download_progress_dialog_description=Показувати вікно \"Прогрес завантаження\" коли завантаження розпочато.\nsettings_per_host_settings=Налаштування веб-сайтів\nsettings_per_host_settings_descriptions=Ці налаштування будуть автоматично застосовані до кожного нового завантаження, що було розпочато з вказаного веб-сайту.\nsettings_download_max_concurrent_downloads=Кількість одночасних завантажень\nsettings_download_max_concurrent_downloads_description=Максимальна кількість файлів, що можуть завантажуватись одночасно. Завантаження, що керуються чергами не рахуються. Використовуйте значення 0 для зняття обмеження.\ndownload_item_settings_speed_limit=Обмеження швидкості\ndownload_item_settings_speed_limit_description=Обмеження швидкості завантаження для поточного файлу\ndownload_item_settings_show_download_completion_dialog=Показувати діалог завершення завантаження\ndownload_item_settings_show_download_completion_dialog_description=Показувати вікно \"Завершення завантаження\" коли поточне завантаження завершено.\ndownload_item_settings_shutdown_on_completion=Вимкнути комп'ютер\ndownload_item_settings_shutdown_on_completion_description=Автоматично вимкнути комп'ютер після завершення цього завантаження.\ndownload_item_settings_thread_count=Кількість потоків\ndownload_item_settings_thread_count_description=Кількість потоків, котрі будуть використовуватись для завантаження цього файлу. Якщо вказано значення 0, то будуть застосовані глобальні налаштування.\ndownload_item_settings_thread_count_describe=Для цього завантаження буде використано {{count}} потоків\ndownload_item_settings_username_description=Надати ім'я користувача, якщо посилання веде на захищений ресурс\ndownload_item_settings_password_description=Надати пароль, якщо посилання веде на захищений ресурс\ndownload_item_settings_download_page=Сторінка завантаження\ndownload_item_settings_download_page_description=Сторінка, на якій було розпочато завантаження\ndownload_item_settings_file_checksum=Контрольна сума\ndownload_item_settings_file_checksum_description=Хеш-рядок, котрий використовується для перевірки, що файл завантажений правильно\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=Власний User-Agent для цього завантаження. Залиште поле пустим, якщо бажаєте використовувати значення за замовчуванням.\nfile_checksum=Контрольна сума\nfile_checksum_page=Перевірка контрольної суми файлу\nfile_checksum_page_file_checksum_default_algorithm=Алгоритм\nfile_checksum_page_file_checksum_default_algorithm_help=Алгоритм за замовчуванням використовується для розрахунку контрольних сум тих файлів, для яких контрольна сума не була вказана на початку завантаження.\nstart=Почати\ncalculated_checksum=Розрахована контрольна сума\nsaved_checksum=Збережена контрольна сума\nchecksum_algorithm=Алгоритм\nfile_not_found=Файл не знайдено\ndownload_not_finished=Завантаження не закінчено\ndone=Завершено\nwaiting=Очікування\nmatches=Збігається\nnot_matches=Не збігається\ncopy_to_clipboard=Копіювати до буферу обміну\nusername=Ім'я користувача\npassword=Пароль\naverage_speed=Середня швидкість\nexact_speed=Точна швидкість\nunlimited=Необмежено\nuse_global_settings=Використовувати глобальні налаштування\ncant_run_browser_integration=Не вдалося запустити інтеграцію з браузером\ncant_open_file=Не вдалося відкрити файл\ncant_open_folder=Не вдалося відкрити каталог\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} років\nrelative_time_long_months={{months}} місяців\nrelative_time_long_days={{days}} днів\nrelative_time_long_hours={{hours}} годин\nrelative_time_long_minutes={{minutes}} хвилин\nrelative_time_long_seconds={{seconds}} секунд\nrelative_time_short_years={{years}} р\nrelative_time_short_months={{months}} м\nrelative_time_short_days={{days}} д\nrelative_time_short_hours={{hours}} год.\nrelative_time_short_minutes={{minutes}} хв.\nrelative_time_short_seconds={{seconds}} сек.\nrelative_time_left={{time}} залишилось\nrelative_time_ago={{time}} тому\nauto=Автоматично\nunspecified=Не вказано\ncustom=Власний\nicon=Іконка\nauthor=Автор\nlink=Посилання\nsize=Розмір\nstatus=Статус\nparts_info_downloaded_size=Завантажено\nparts_info_total_size=Всього\nspeed=Швидкість\ntime_left=Часу залишилося\ndate_added=Дата додавання\ninfo=Відомості\ndownload_page_downloaded_size=Завантажено\ndownload_page_download_completed=Завантаження завершено\nresume_support=Відновлення завантаження\nyes=Так\nno=Ні\nparts_info=Інформація про фрагменти\ndisconnected=Роз'єднано\nreceiving_data=Отримання даних\nconnecting=Підключення\nwarning=Попередження\nunsupported_resume_warning=Це завантаження не підтримує відновлення. Можливо вам доведеться почати завантаження спочатку в разі його зупинки.\nstop_anyway=Зупинити примусово\ncustomize_columns=Налаштувати стовпці\nreset=Скидання\nmonday=Понеділок\ntuesday=Вівторок\nwednesday=Середа\nthursday=Четвер\nfriday=П’ятниця\nsaturday=Субота\nsunday=Неділя\nproxy_open_system_proxy_settings=Відкрити системні параметри проксі\nproxy_type=Тип проксі\nproxy_do_not_use_proxy_for=Не використовувати проксі для\nproxy_do_not_use_proxy_for_description=Список Url-адрес, які не можуть бути проксі-серверами\\nВи можете використовувати символ підстановки з *\\nнаприклад 192.168.1.* example.com (через пробіл)\nproxy_change_title=Змінити проксі\nchange_proxy=Змінити проксі\nproxy_no=Без проксі\nproxy_system=Системний проксі\nproxy_manual=Ручний проксі\nproxy_pac=Автоналаштування проксі-сервера\nproxy_pac_url=URL-адреса автоматичної конфігурації проксі\naddress=Адреса\nport=Порт\naddress_and_port=Адреса та порт\nuse_authentication=Використовувати автентифікацію\nwarning_you_may_have_to_restart_the_download_later=Можливо, вам доведеться почати завантаження спочатку\\!\nedit_download_title=Редагування завантаження\nedit_download_update_from_download_page=Оновити зі сторінки завантаження\nedit_download_update_from_download_page_description=Коли це вікно відкрите, ви можете перейти на сторінку завантаження файлу та натиснути кнопку завантаження. Застосунок автоматично захопить та оновить дані про завантаження, щоб ви могли зберегти їх.\nedit_download_saved_download_item_size_not_match=Розмір нового завантаження ({{newSize}}) не збігається з розміром вже існуючого ({{currentSize}}).\ntranslators_page_thanks=Висловлюю подяку перекладачам цього проєкту ❤️\ntranslators=Перекладачі\nlanguage=Мова\ntranslators_contribute_title=Покращити переклад\ntranslators_contribute_description=Бажаєте допомогти покращити цей проєкт ? Якщо в перекладі є помилки або ваша мова відсутня в списку, ви можете долучитись до перекладу та зробити його ще краще\\!\ncontribute=Зробити внесок\nmeet_the_translators=Переглянути перекладачів\nlocalized_by_translators=Перекладено спільнотою\nconfirm_exit=Підтвердження виходу\nconfirm_exit_description=Ви впевнені, що бажаєте завершити роботу AB Download Manager? Активні завантаження будуть зупинені.\nupdate=Оновити\nupdate_updater=Майстер оновлень\nupdate_available=Доступне оновлення\nupdate_error=Помилка оновлення\nupdate_available_suggest_to_to_update=Ви можете оновитися до останньої версії, щоб насолоджуватися новими функціями та покращеннями.\nupdate_release_notes=Примітки до випуску\nupdate_check_for_update=Перевірити наявність оновлень\nupdate_checking_for_update=Перевірка оновлень\nupdate_no_update=Ви використовуєте останню версію\nupdate_check_error=Помилка під час перевірки оновлення\nupdate_app_updated_to_version_n=Застосунок оновлено до версії {{version}}\ncreate_desktop_entry=Створити ярлик (Desktop Entry)\nshutdown_alert=Сповіщення при вимкненні комп'ютера\nsystem_shutdown_soon=Комп'ютер скоро вимкнеться\\!\nsystem_shutdown_failed=Не вдалося вимкнути систему\\!\nsystem_shutdown_soon_description=Скоро буде завершено роботу системи. Якщо ви все ще використовуєте комп'ютер, вам варто зберегти свої файли або скасувати вимкнення комп'ютера.\nsystem_shutdown_reason_queue_completed=Всі завантаження в черзі завершено.\nsystem_shutdown_reason_queue_end_time_reached=Запланований час зупинки черги настав.\nsystem_shutdown_download_finished=Завантаження завершено.\nshutdown_now=Завершити роботу зараз\nsettings_per_host_settings_new_host=<Новий веб-сайт>\nsettings_per_host_settings_not_selected=Оберіть елемент або створіть його\\!\nsettings_per_host_settings_host=Веб-сайт\nsettings_per_host_settings_host_description=Ці налаштування будуть застосовані до завантажень, що були розпочаті з вказаних веб-сайтів. В іменах веб-сайтів дозволено використовувати символи підстановки (*). Наприклад,  \"github.com\",  \"*.yahoo.com\").\nsettings_browser_in_launcher=Іконка веб-браузера в лаунчері\nsettings_browser_in_launcher_description=Показувати піктограму веб-браузера AB DM в лаунчері (меню програм) системи.\nsort_by=Сортувати за\nwelcome=Ласкаво просимо\\!\nnew_folder=Новий каталог\nskip=Пропустити\nlets_go=Поїхали\\!\nnext=Далі\nselect_all=Виділити всі\nselect_inside=Обрати всередині\nselect_invert=Інвертувати обране\nopen_settings=Відкрити налаштування\nback=Назад\nservice_is_running=Служба працює\ninitial_setup_description=Нумо все налаштуємо.\ninitial_setup_notice=Ви можете змінити ці налаштування будь-коли.\npermission_granted=Дозвіл надано\npermission_not_granted=Дозвіл не надано\npermissions=Дозволи\ngive_permission=Надати дозвіл\ngive_storage_permission=Дозволити доступ до сховища\nstorage_roots=Кореневі каталоги\npermissions_initial_title=Налаштування дозволів\npermissions_initial_description=Для коректної роботи застосунку необхідні деякі дозволи. На наступному екрані ви побачите для яких цілей потрібен кожен з них та зможете вирішити — приймати його чи ні.\npermissions_done_title=Все налаштовано\\!\npermissions_done_description=Все готово. Усі необхідні дозволи надано - застосунок може добре працювати.\npermissions_manage_storage_title=Керування доступом до сховища\npermissions_manage_storage_reason=Цей дозвіл дає право застосунку змінювати теку завантаження, визначати повторювані завантаження більш точно, та увімкнути додаткові функції. Це необов'язково, проте рекомендується для найкращого досвіду.\npermission_read_write_external_storage_title=Читання та запис сховища\npermission_read_write_external_storage_reason=Цей дозвіл дає можливість застосунку зберігати та керувати завантаженими файлами, змінювати каталог завантажень та покращувати виявлення завантажень-дублікатів.\npermissions_post_notification_title=Публікація сповіщень\npermissions_post_notification_reason=Для керування завантаженнями, застосунку необхідно працювати у фоновому режимі. Сповіщення будуть інформувати вас та дозволять виконувати операції у фоновому режимі.\npermissions_ignore_battery_optimization_title=Ігнорувати оптимізацію акумулятора\npermissions_ignore_battery_optimization_reason=Деякі пристрої в агресивній манері обмежують фонову активність застосунків задля економії заряду акумулятору. Через це завантаження можуть бути призупинені або скасовані, коли застосунок не відкритий. Ви можете вилучити цей застосунок зі списку на Оптимізацію Батареї, щоб бути впевненими, що завантаження не будуть перервані.\nopen_in_browser=Відкрити у веб-браузері\nbrowser=Веб-браузер\nbrowser_new_tab=Нова вкладка\nbrowser_close_tab=Закрити вкладку\nbrowser_open_in_new_tab=Відкрити у новій вкладці\nbrowser_open_in_new_background_tab=Відкрити у новій фоновій вкладці\nbrowser_no_tab_open=Немає відкритих вкладок\nbrowser_tabs=Вкладки\nbrowser_paste_and_go=Вставити та перейти\nbrowser_bookmarks=Закладки\nbrowser_add_bookmark=Додати закладку\nbrowser_edit_bookmark=Редагувати закладку\nbrowser_add_to_bookmarks=Додати до закладок\nbrowser_remove_from_bookmarks=Видалити із закладок\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/vi_VN.properties",
    "content": "app_title=AB Download Manager\nconfirm_auto_categorize_downloads_title=Danh mục tải về tự động\nconfirm_auto_categorize_downloads_description=Các tập tin chưa được phân loại sẽ được sắp xếp tự động vào các danh mục liên quan.\nconfirm_reset_to_default_categories_title=Cài đặt lại danh mục mặc định\nconfirm_reset_to_default_categories_description=điều này sẽ XÓA tất cả danh mục và sẽ đưa trở về danh mục mặc định\\!\nconfirm_delete_download_items_title=Xác nhận xóa\nconfirm_delete_download_items_description=Bạn có chắc là muốn xóa {{count}} tập tin?\nconfirm_delete_download_unfinished_items_description=Bạn có chắc chắn muốn xóa {{count}} lượt tải xuống chưa hoàn tất không?\nconfirm_delete_download_finished_and_unfinished_items_description=Bạn có chắc chắn muốn xóa {{finishedCount}} lượt tải xuống đã hoàn thành và {{unfinishedCount}} lượt tải xuống chưa hoàn thành không?\nalso_delete_file_from_disk=Cũng xóa tập tin từ ổ cứng\nconfirm_delete_category_item_title=Đang xóa danh mục {{name}}\nconfirm_delete_category_item_description=Bạn có chắc là muốn xóa danh mục \"{{value}}?\nyour_download_will_not_be_deleted=Tập tin đã tải của bạn sẽ không bị xóa\ndrag_the_file_to_another_app=Kéo tệp vào một ứng dụng khác\ndrop_link_or_file_here=Thả đường dẫn hoặc tập tin ở đây.\nnothing_will_be_imported=Không có gì được nạp\nn_links_will_be_imported={{count}} đường dẫn sẽ được nạp\nn_items_selected={{count}} tập tin được chọn\nwindow_close=Đóng\nwindow_minimize=Thu nhỏ\nwindow_maximize=Phóng to\nwindow_restore=Khôi phục\ndelete=Xóa\nremove=Loại bỏ\ncancel=Hủy\nclose=Đóng\nmenu=Menu\nmore_options=Tùy chọn khác\nok=OK\nadd=Thêm\npaste=Dán\nchange=Đổi\nedit=Sửa\nchange_anyway=Vẫn thay đổi\ndownload=Tải xuống\nrefresh=Làm mới\nsettings=Cài đặt\non_completion=Hoàn tất\nunknown=Không biết\nunknown_error=Lỗi không rõ\ndownload_item_not_found=Không tìm thấy tập tin đã tải\nname=Tên\ndownload_link=Đường dẫn tải\nnot_finished=Chưa hoàn thành\nall=Tất cả\nfinished=Hoàn thành\nUnfinished=Chưa hoàn thành\ncanceled=Hủy\nerror=Lỗi\npaused=Dừng\ndownloading=Đang tải\nadded=Thêm\nidle=CHỜ\npreparing_file=Đang chuẩn bị tập tin\ncreating_file=Đang tạo tập tin\nresuming=Đang khôi phục\nretrying=Đang thử lại\nlist_is_empty=Danh sách trống\\!\nsearch_in_the_list=Tìm trong danh sách\nsearch=Tìm\nclear=Làm sạch\ngeneral=Tổng quan\nenabled=Cho phép\ndisabled=Vô hiệu\ndefault=Mặc định\nfile=Tệp\ntasks=Tác Vụ\ntools=Công Cụ\nhelp=Trợ Giúp\nsystem=Hệ thống\nall_missing_files=Tất cả các tập tin bị mất\nall_finished=Tất cả đã hoàn tất\nall_unfinished=Tất cả chưa hoàn thành\nentire_list=Toàn bộ danh sách\ndownload_browser_integration=Liên kết với trình duyệt\nexit=Đóng\nshow_downloads=Hiển thị mục tải\nnew_download=Mục tải xuống mới\nstop_all=Dừng tất cả\nimport_from_clipboard=Nhập từ khay nhớ tạm\nbatch_download=Tải hàng loạt\nopen=Mở\nshare=Chia sẻ\nopen_file=Mở tệp\nopen_folder=Mở thư mục\nresume=Tiếp tục\npause=Tạm Dừng\nrestart_download=Khởi Động Lại Mục Tải\ncopy=Sao chép\ncopy_link=Sao Chép Liên Kết\ncopy_as_curl=Sao chép dưới dạng cURL\nshow_properties=Xem Thuộc Tính\nmove_to_queue=Thêm Vào Hàng Chờ\nmove_to_this_queue=Di chuyển đến hàng đợi này\nmove_to_category=Thêm Vào Danh Mục\nmove_to_this_category=Di chuyển đến danh mục này\ncategories=Danh mục\nadd_category=Thêm Danh Mục\nedit_category=Sửa Danh Mục\ndelete_category=Xoá Danh Mục\ncategory_name=Tên Danh Mục\ncategory_download_location=Vị Trí Tải Của Danh Mục\ncategory_download_location_description=Khi danh mục này được chọn trong \"Thêm tải xuống\", hãy sử dụng thư mục này làm \"Vị trí tải xuống\"\ncategory_file_types=Loại Tệp Của Danh Mục\ncategory_file_types_description=Tự động đưa các loại tệp này vào danh mục này. (khi bạn thêm tệp tải xuống mới)\\nPhân tách phần mở rộng tệp bằng dấu cách (ext1 ext2 ...)\ncategory_url_patterns=Mẫu liên kết\ncategory_url_patterns_description=Tự động đưa tải xuống từ các liên kết này vào danh mục này. (khi bạn thêm tải xuống mới)\\nPhân tách các liên kết bằng dấu cách, bạn cũng có thể sử dụng * cho ký tự đại diện\nauto_categorize_downloads=Tự Động Sắp Xếp Danh Mục Tải\nrestore_defaults=Đặt Về Mặc Định\nabout=Giới Thiệu\nversion_n=Phiên bản {{value}}\ndeveloped_with_love_for_you=Được phát triển với ❤️ dành cho bạn\ndonate=Đóng góp\nvisit_the_project_website=Xem trang web của ứng dụng\nthis_is_a_free_and_open_source_software=Đây là một phần mềm miễn phí với mã nguồn mở\nview_the_source_code=Xem mã nguồn\nthird_party_libraries=Thư viện bên thứ ba\npowered_by_open_source_software=Được làm nên từ các phần mềm mã nguồn mở\nview_the_open_source_licenses=Xem các giấy phép mã nguồn mở\nsupport_and_community=Hỗ trợ & Cộng đồng\ntelegram=Telegram\nchannel=Kênh\ngroup=Nhóm\nadd_download=Thêm Mục Tải\nadd_multi_download_page_header=Chọn tập tin bạn muốn tải\nsave_to=Lưu tại\nwhere_should_each_item_saved=Mỗi tập tin được lưu tại đâu?\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=Có nhiều tập tin\\! Vui lòng chọn cách mà bạn muốn lưu\neach_item_on_its_own_category=Mỗi tập tin trong danh mục của nó\neach_item_on_its_own_category_description=Mỗi tập tin sẽ lưu tại danh mục theo định dạng tập tin\nall_items_in_one_category=Tất cả tập tin trong một danh mục\nall_items_in_one_category_description=Tất cả tập tin sẽ được lưu tại vị trí danh mục đã chọn\nall_items_in_one_Location=Tất cả tập tin tại một vị trí\nall_items_in_one_Location_description=Tất cả tập tin sẽ được lưu tại danh mục được chọn\nunselected_all_items_in_specific_location_description=Tất cả các tập tin sẽ được lưu trong vị trí danh mục đã chọn\nno_category_selected=Không có danh mục được chọn\nno_categories_found=Không tìm thấy danh mục nào\ndownload_location=Vị trí tải về\nlocation=Vị trí\nselect_queue=Chọn hàng đợi\nwithout_queue=Không đợi\nuse_category=Sử dụng danh mục\ncant_write_to_this_folder=Không thể lưu tại thư mục này\nfile_name_already_exists=Tên tập tin đã tồn tại\ndownload_already_exists=Tải xuống đã tồn tại\ninvalid_file_name=Tên tập tin vô hiệu\nshow_solutions=Chỉ ra giải pháp...\nchange_solution=Đổi giải pháp\nselect_a_solution=Chọn giải pháp\nselect_download_strategy_description=Đường dẫn bạn cung cấp đã nằm trong danh sách tải, hãy chọn bạn muốn làm gì\ndownload_strategy_add_a_numbered_file=Thêm số vào tập tin\ndownload_strategy_add_a_numbered_file_description=Thêm chỉ mục vào cuối tên tập tin được tải\ndownload_strategy_override_existing_file=Ghi đè tập tin đã tồn tại\ndownload_strategy_override_existing_file_description=Xóa tập tin đã tồn tại và lưu lại tập tin đó\ndownload_strategy_update_download_link=Cập nhật bản tải xuống hiện có\ndownload_strategy_update_download_link_description=Cập nhật liên kết tải xuống hiện có và thông tin đăng nhập của nó\ndownload_strategy_show_downloaded_file=Chỉ tập tin đã tải\ndownload_strategy_show_downloaded_file_description=Tập tin đã tồn tại, bạn hãy chọn tải lại hoặc mở nó\nbatch_download_link_help=Nhập đường dẫn chứa ký thự thay thế (sử dụng *)\ninvalid_url=URL sai\nlist_is_too_large_maximum_n_items_allowed=Danh sách quá lớn\\! Cho phép tối đa {{count}} tập tin\nenter_range=Nhập vào dãy\nrange_from=Từ\nrange_to=Đến\nbatch_download_wildcard_length=Độ dài thay thế\nfirst_link=Đường dẫn đầu tiên\nlast_link=Đường dẫn cuối cùng\nopen_source_software_used_in_this_app=Mở phần mềm nguồn mở sử dụng trong ứng dụng này\nlinks=Đường dẫn\nwebsite=Website\ndevelopers=Các tác giả\nsource_code=Mã nguồn\nlicense=Bản quyền\nno_license_found=Không có bản quyền được tìm thấy\norganization=Tổ chức\nadd_new_queue=Thêm hàng đợi mới\nqueue_name=Tên hàng đợi\nqueues=Hàng đợi\nstop_queue=Dừng hàng đợi\nstart_queue=Bắt đầu hàng đợi\nclear_queue_items=Hàng đợi trống\nconfig=Cài Đặt\nitems=Mục\nmove_down=Di chuyển xuống\nmove_up=Di chuyển lên\nremove_queue=Xoá Khỏi Hàng Chờ\nqueue_name_help=Chọn tên cho hàng chờ\nqueue_name_describe=Tên hàng chờ là {{value}}\nqueue_max_concurrent_download=Số mục tải xuống tối đa cùng lúc\nqueue_max_concurrent_download_description=Số tải xuống tối đa cho hàng chờ này\nqueue_automatic_stop=Tự động dừng\nqueue_automatic_stop_description=Tự động dừng hàng chờ khi hết mục tải\nqueue_scheduler=Lịch trình\nqueue_enable_scheduler=Bật lịch trình\nqueue_active_days=Ngày hoạt động\nqueue_active_days_description=Chọn những ngày lịch trình hoạt động\nqueue_scheduler_enable_auto_start_time=Bật thời gian bắt đầu tự động\nqueue_scheduler_auto_start_time=Thời gian tự bắt đầu\nqueue_scheduler_enable_auto_stop_time=Kích hoạt tự bắt đầu\nqueue_scheduler_auto_stop_time=Thời gian tự kết thúc\nqueue_shutdown_on_completion=Tắt máy khi hoàn tất\nqueue_shutdown_on_completion_description=Tự động tắt máy khi hàng đợi này hoàn thành hoặc khi đạt đến thời gian kết thúc đã lên lịch.\nappearance=Giao diện\ndownload_engine=Trình tải\nbrowser_integration=Liên kết với trình duyệt\nsettings_download_max_retries_count=Số lần thử tải xuống tối đa\nsettings_download_max_retries_count_description=Số lần tối đa ứng dụng sẽ thử lại một lần tải xuống không thành công trước khi từ bỏ\nsettings_download_max_retries_count_describe_no_retries=Tải xuống không thành công sẽ không được thử lại\nsettings_download_max_retries_count_describe_n_retries=Tải xuống không thành công sẽ được thử lại {{count}} lần\nsettings_download_thread_count=Số luồng\nsettings_download_thread_count_description=Số luồng tải xuống tối đa cho mỗi mục tải xuống\nsettings_download_thread_count_describe=Một lượt tải xuống có thể có tới {{count}} luồng\nsettings_download_thread_count_with_large_value_describe=Cảnh báo\\: Việc thiết lập số luồng cao có thể làm tăng mức sử dụng tài nguyên hệ thống, giảm hiệu suất hoặc gây ra sự cố kết nối với máy chủ. Chỉ sử dụng giá trị cao hơn nếu bạn hiểu được tác động tiềm ẩn lên hệ thống và mạng của mình.\nsettings_use_server_last_modified_time=Sử dụng thời gian sửa đổi cuối cùng của máy chủ\nsettings_use_server_last_modified_time_description=Khi tải xuống tệp, hãy sử dụng thời gian sửa đổi cuối cùng của máy chủ cho tệp cục bộ\nsettings_append_extension_to_incomplete_downloads=Thêm phần mở rộng vào Tải xuống chưa hoàn tất\nsettings_append_extension_to_incomplete_downloads_description=Thêm phần mở rộng \".part\" vào các bản tải xuống chưa hoàn tất. Điều này giúp xác định các bản tải xuống chưa hoàn tất và ngăn chặn việc vô tình mở các tệp chưa hoàn tất.\nsettings_use_sparse_file_allocation=Phân bổ không gian tập tin thưa thớt\nsettings_use_sparse_file_allocation_description=Tạo tệp hiệu quả hơn, đặc biệt là trên SSD, bằng cách giảm ghi dữ liệu không cần thiết. Điều này có thể tăng tốc độ bắt đầu tải xuống và giảm mức sử dụng đĩa. Nếu tải xuống bắt đầu chậm hoặc bạn gặp phải tốc độ tải xuống bất thường, hãy cân nhắc tắt tùy chọn này vì tùy chọn này có thể không được hỗ trợ đầy đủ trên một số thiết bị.\nsettings_ignore_ssl_certificates=Bỏ qua chứng chỉ SSL\nsettings_ignore_ssl_certificates_description=Tắt xác minh chứng chỉ SSL. Chỉ sử dụng khi cần thiết vì nó có thể khiến kết nối của bạn gặp rủi ro về bảo mật.\nsettings_global_speed_limiter=Bộ giới hạn tốc độ chung\nsettings_global_speed_limiter_description=Giới hạn tốc độ tải xuống chung (0 nghĩa là không giới hạn)\nsettings_show_average_speed=Hiện tốc độ trung bình\nsettings_show_average_speed_description=Tốc độ tải xuống trung bình hoặc chính xác\nsettings_use_category_by_default=Sử dụng danh mục theo mặc định\nsettings_use_category_by_default_description=Sử dụng danh mục theo mặc định khi thêm mục tải xuống.\nsettings_default_download_folder=Thư mục tải xuống mặc định\nsettings_default_download_folder_description=Khi bạn thêm tải xuống mới, vị trí này được sử dụng theo mặc định\nsettings_default_download_folder_describe=\"{{folder}}\" sẽ được sử dụng\nsettings_use_proxy=Sử dụng Proxy\nsettings_use_proxy_description=Sử dụng proxy để tải xuống tệp\nsettings_use_proxy_describe_no_proxy=Không sử dụng Proxy\nsettings_use_proxy_describe_system_proxy=Hệ thống Proxy sẽ được sử dụng\nsettings_use_proxy_describe_manual_proxy=\"{{value}}\" sẽ được sử dụng\nsettings_use_proxy_describe_pac_proxy=tệp pac \"{{value}}\" sẽ được sử dụng\nsettings_track_deleted_files_on_disk=Theo dõi các tập tin đã xóa trên đĩa\nsettings_track_deleted_files_on_disk_description=Tự động xóa tệp khỏi danh sách khi chúng bị xóa hoặc di chuyển khỏi thư mục tải xuống.\nsettings_delete_partial_file_on_download_cancellation=Xóa tập tin tải xuống dở khi hủy tải\nsettings_delete_partial_file_on_download_cancellation_description=Khi một lượt tải xuống bị hủy, một phần tệp đã tải xuống sẽ bị xóa khỏi ổ đĩa. Điều này giúp giữ cho thư mục tải xuống của bạn sạch sẽ và giảm việc sử dụng dung lượng ổ đĩa không cần thiết. Tuy nhiên, lượt tải xuống sẽ khởi động lại từ đầu vào lần tiếp theo bạn bắt đầu.\nsettings_default_user_agent=Tác nhân người dùng mặc định\nsettings_default_user_agent_description=Chỉ định chuỗi Tác nhân người dùng mặc định để xác định cách các yêu cầu tự nhận diện với máy chủ. Điều này có thể giúp truy cập nội dung được tối ưu hóa cho các thiết bị cụ thể hoặc vượt qua các giới hạn tải xuống do một số trang web áp đặt.\nsettings_download_size_unit=Đơn vị của kích thước tải xuống\nsettings_download_size_unit_description=Đơn vị được sử dụng để hiển thị kích thước tải xuống\nsettings_download_speed_unit=Đơn vị tốc độ tải xuống\nsettings_download_speed_unit_description=Đơn vị được sử dụng để hiển thị tốc độ tải xuống\nsettings_theme=Chủ đề\nsettings_theme_description=Chọn chủ đề cho ứng dụng\nsettings_default_dark_theme=Chủ đề tối mặc định\nsettings_default_dark_theme_description=Áp dụng khi ứng dụng theo chủ đề hệ thống và chế độ tối đang hoạt động\nsettings_default_light_theme=Chủ đề sáng mặc định\nsettings_default_light_theme_description=Áp dụng khi ứng dụng theo chủ đề hệ thống và chế độ sáng đang hoạt động\nsettings_font=Phông chữ\nsettings_font_description=Thay đổi phông chữ được sử dụng trong giao diện ứng dụng. Một số phông chữ có thể không hiển thị chính xác trong ứng dụng.\nsettings_ui_scale=Tỉ lệ UI\nsettings_ui_scale_description=Điều chỉnh kích thước của các thành phần giao diện ứng dụng\nsettings_language=Ngôn ngữ\nsettings_compact_top_bar=Thu gọn thanh trên cùng\nsettings_compact_top_bar_description=Gộp thanh trên cùng với thanh tiêu đề khi cửa sổ chính có đủ chiều rộng\nsettings_use_native_menu_bar=Sử dụng thanh Menu gốc\nsettings_use_native_menu_bar_description=Sử dụng kiểu thanh menu mặc định của hệ thống\nsettings_use_relative_date_time=Sử dụng ngày/giờ tương đối\nsettings_use_relative_date_time_description=Sử dụng định dạng ngày/giờ tương đối cho các ngày trong ứng dụng (ví dụ\\: \"2 ngày trước\" thay vì ngày/giờ chính xác)\nsettings_show_icon_labels=Hiện nhãn biểu tượng\nsettings_show_icon_labels_description=Hiển thị nhãn dưới các biểu tượng khi có thể (như các hành động trên thanh công cụ chính)\nsettings_use_system_tray=Sử dụng khay hệ thống\nsettings_use_system_tray_description=Hiện biểu tượng khay hệ thống khi ứng dụng đang chạy\nsettings_start_on_boot=Bắt đầu khi khởi động\nsettings_start_on_boot_description=Tự khởi động ứng dụng khi người dùng đăng nhập\nsettings_notification_sound=Âm thanh thông báo\nsettings_notification_sound_description=Phát âm thanh khi có thông báo mới\nsettings_browser_integration=Tích hợp trình duyệt\nsettings_browser_integration_description=Chấp nhận tải từ trình duyệt\nsettings_browser_integration_server_port=Cổng server\nsettings_browser_integration_server_port_description=Cổng cho tích hợp trình duyệt\nsettings_browser_integration_server_port_describe=Ứng dụng sẽ lắng nghe cổng {{port}}\nsettings_dynamic_part_creation=Tạo thành phần tự động\nsettings_dynamic_part_creation_description=Khi một phần tải xong, tạo thêm phần khác bằng cách chia nhỏ các phần khác để cải thiện tốc độ tải xuống\nsettings_show_completion_dialog=Hiển thị hộp thoại hoàn tất tải xuống\nsettings_show_completion_dialog_description=Tự động hiển thị hộp thoại \"Hoàn tất tải xuống\" khi quá trình tải xuống hoàn tất.\nsettings_show_download_progress_dialog=Hiển thị hộp thoại tiến trình tải xuống\nsettings_show_download_progress_dialog_description=Tự động hiển thị hộp thoại \"Tiến trình tải xuống\" khi quá trình tải xuống bắt đầu.\nsettings_per_host_settings=Cài đặt cho từng máy chủ\nsettings_per_host_settings_descriptions=Các cài đặt này sẽ tự động được áp dụng cho bất kỳ lượt tải xuống mới nào khớp với máy chủ đã chỉ định.\nsettings_download_max_concurrent_downloads=Số lượt tải xuống đồng thời tối đa\nsettings_download_max_concurrent_downloads_description=Số lượng tệp tối đa có thể được tải xuống cùng lúc (các lượt tải xuống được quản lý bởi hàng đợi không được tính; đặt thành 0 để không giới hạn)\ndownload_item_settings_speed_limit=Giới hạn tốc độ\ndownload_item_settings_speed_limit_description=Giới hạn tốc độ tải xuống cho mục này\ndownload_item_settings_show_download_completion_dialog=Hiển thị hộp thoại hoàn tất tải xuống\ndownload_item_settings_show_download_completion_dialog_description=Tự động hiển thị hộp thoại \"Hoàn tất tải xuống\" khi quá trình tải xuống hoàn tất.\ndownload_item_settings_shutdown_on_completion=Tắt máy khi hoàn tất\ndownload_item_settings_shutdown_on_completion_description=Tự động tắt máy khi quá trình tải xuống này hoàn tất.\ndownload_item_settings_thread_count=Số luồng\ndownload_item_settings_thread_count_description=Đã sử dụng bao nhiêu luồng để tải xuống mục tải xuống này (mặc định là 0)\ndownload_item_settings_thread_count_describe={{count}} luồng cho tải xuống này\ndownload_item_settings_username_description=Cung cấp tên người dùng nếu liên kết là tài nguyên được bảo vệ\ndownload_item_settings_password_description=Cung cấp mật khẩu nếu liên kết là tài nguyên được bảo vệ\ndownload_item_settings_download_page=Trang tải xuống\ndownload_item_settings_download_page_description=Trang web nơi tải xuống này được bắt đầu\ndownload_item_settings_file_checksum=Tệp Checksum\ndownload_item_settings_file_checksum_description=Chuỗi băm có thể được sử dụng để kiểm tra xem tệp có được tải xuống đúng cách hay không\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=User-Agent tùy chỉnh cho mục này (để trống để sử dụng mặc định)\nfile_checksum=Tệp Checksum\nfile_checksum_page=Kiểm tra tập tin Checksum\nfile_checksum_page_file_checksum_default_algorithm=Thuật toán mặc định\nfile_checksum_page_file_checksum_default_algorithm_help=Thuật toán mặc định được sử dụng để tính toán tổng kiểm tra tệp khi chúng không được cung cấp.\nstart=Bắt đầu\ncalculated_checksum=Đã tính toán Checksum\nsaved_checksum=Đã lưu Checksum\nchecksum_algorithm=Thuật toán\nfile_not_found=Không tìm thấy tập tin\ndownload_not_finished=Tải xuống chưa hoàn tất\ndone=Xong\nwaiting=Đang chờ\nmatches=Khớp\nnot_matches=Không khớp\ncopy_to_clipboard=Sao chép vào bảng tạm\nusername=Tên đăng nhập\npassword=Mật khẩu\naverage_speed=Tốc độ trung bình\nexact_speed=Tốc độ chính xác\nunlimited=Không giới hạn\nuse_global_settings=Sử dụng cài đặt chung\ncant_run_browser_integration=Không thể chạy tích hợp trình duyệt\ncant_open_file=Không thể mở tập tin\ncant_open_folder=Không thể mở thư mục\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} năm\nrelative_time_long_months={{months}} tháng\nrelative_time_long_days={{days}} ngày\nrelative_time_long_hours={{hours}} giờ\nrelative_time_long_minutes={{minutes}} phút\nrelative_time_long_seconds={{seconds}} giây\nrelative_time_short_years={{years}} n\nrelative_time_short_months={{months}} th\nrelative_time_short_days={{days}} ng\nrelative_time_short_hours={{hours}} h\nrelative_time_short_minutes={{minutes}} p\nrelative_time_short_seconds={{seconds}} s\nrelative_time_left={{time}} trái\nrelative_time_ago={{time}} trước\nauto=Tự động\nunspecified=Không xác định\ncustom=Tuỳ chỉnh\nicon=Biểu tượng\nauthor=Tác giả\nlink=Liên kết\nsize=Kích thước\nstatus=Trạng thái\nparts_info_downloaded_size=Đã tải xuống\nparts_info_total_size=Tổng\nspeed=Tốc độ\ntime_left=Thời gian còn lại\ndate_added=Ngày thêm vào\ninfo=Thông tin\ndownload_page_downloaded_size=Đã tải xuống\ndownload_page_download_completed=Tải xuống hoàn tất\nresume_support=Hỗ trợ khôi phục\nyes=Có\nno=Không\nparts_info=Thông tin các phần\ndisconnected=Ngắt kết nối\nreceiving_data=Đang nhận dữ liệu\nconnecting=Đang kết nối\nwarning=Cảnh báo\nunsupported_resume_warning=Tải xuống này không hỗ trợ khôi phục\\! Bạn có thể phải KHỞI ĐỘNG LẠI sau trong danh sách tải xuống\nstop_anyway=Dừng luôn\ncustomize_columns=Tuỳ chỉnh cột\nreset=Đặt lại\nmonday=Thứ hai\ntuesday=Thứ ba\nwednesday=Thứ tư\nthursday=Thứ năm\nfriday=Thứ sáu\nsaturday=Thứ bảy\nsunday=Chủ nhật\nproxy_open_system_proxy_settings=Mở cài đặt Proxy hệ thống\nproxy_type=Loại Proxy\nproxy_do_not_use_proxy_for=Không sử dụng proxy cho\nproxy_do_not_use_proxy_for_description=Danh sách các liên kết có thể không sử dụng được proxy\\nBạn có thể sử dụng ký tự đại diện với *\\nví dụ 192.168.1.* example.com (phân cách bằng dấu cách)\nproxy_change_title=Đổi Proxy\nchange_proxy=Đổi Proxy\nproxy_no=Không Proxy\nproxy_system=Proxy hệ thống\nproxy_manual=Proxy thủ công\nproxy_pac=Cấu hình tự động Proxy\nproxy_pac_url=Liên kết cấu hình Proxy tự động\naddress=Địa chỉ\nport=Cổng\naddress_and_port=Địa chỉ & Cổng\nuse_authentication=Sử dụng xác thực\nwarning_you_may_have_to_restart_the_download_later=Bạn có thể phải khởi động lại quá trình tải xuống sau\\!\nedit_download_title=Chỉnh sửa tải xuống\nedit_download_update_from_download_page=Cập nhật từ Trang tải xuống\nedit_download_update_from_download_page_description=Khi cửa sổ này mở ra, bạn có thể vào Trang tải xuống và nhấp vào nút tải xuống. Ứng dụng sẽ chụp và cập nhật thông tin xác thực tải xuống mới để bạn có thể lưu chúng.\nedit_download_saved_download_item_size_not_match=Mục tải xuống đã lưu có kích thước là {{currentSize}}, không khớp với kích thước mới là {{newSize}}.\ntranslators_page_thanks=Cảm ơn những người đã giúp dịch dự án này ❤️\ntranslators=Người dịch\nlanguage=Ngôn ngữ\ntranslators_contribute_title=Cải thiện bản dịch\ntranslators_contribute_description=Bạn có muốn giúp cải thiện dự án này không? Nếu ngôn ngữ của bạn không được liệt kê hoặc cần một số điều chỉnh, bạn có thể đóng góp bản dịch của mình và làm cho nó tốt hơn\\!\ncontribute=Đóng góp\nmeet_the_translators=Gặp gỡ các biên dịch viên\nlocalized_by_translators=Bản địa hóa bởi biên dịch viên\nconfirm_exit=Xác nhận thoát\nconfirm_exit_description=Bạn có chắc chắn muốn thoát khỏi AB Download Manager không?\\nCác lượt tải xuống/hàng đợi đang hoạt động sẽ bị dừng\\!\nupdate=Cập nhật\nupdate_updater=Trình cập nhật\nupdate_available=Đã có bản cập nhật\nupdate_error=Cập nhật lỗi\nupdate_available_suggest_to_to_update=Bạn có thể cập nhật lên phiên bản mới nhất để tận hưởng các tính năng mới, cải tiến và nâng cao hiệu suất.\nupdate_release_notes=Thông tin phiên bản\nupdate_check_for_update=Kiểm tra cập nhật\nupdate_checking_for_update=Đang kiểm tra cập nhật\nupdate_no_update=Bạn đang sử dụng phiên bản mới nhất\nupdate_check_error=Lỗi khi kiểm tra bản cập nhật\nupdate_app_updated_to_version_n=Ứng dụng đã được cập nhật lên phiên bản {{version}}\ncreate_desktop_entry=Tạo mục nhập trên Desktop\nshutdown_alert=Cảnh báo tắt máy\nsystem_shutdown_soon=Máy sắp tắt\\!\nsystem_shutdown_failed=Tắt máy không thành công\\!\nsystem_shutdown_soon_description=Máy sẽ sớm tắt. Nếu bạn vẫn đang sử dụng máy tính, vui lòng lưu công việc của bạn hoặc hủy bỏ việc tắt máy.\nsystem_shutdown_reason_queue_completed=Tất cả các lượt tải xuống trong hàng đợi đã hoàn tất.\nsystem_shutdown_reason_queue_end_time_reached=Đã đến thời gian kết thúc dự kiến cho hàng đợi tải xuống.\nsystem_shutdown_download_finished=Đã tải xong.\nshutdown_now=Tắt máy ngay\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=Tạo hoặc chọn một mục mới trước\\!\nsettings_per_host_settings_host=Máy chủ\nsettings_per_host_settings_host_description=Các cài đặt này sẽ được áp dụng cho các lượt tải xuống khớp với tên máy chủ này. Hỗ trợ ký tự đại diện (*). (ví dụ\\: example.com, *.example.com — chỉ sử dụng một).\nsettings_browser_in_launcher=Biểu tượng trình duyệt trong Trình khởi chạy\nsettings_browser_in_launcher_description=Hiện hoặc ẩn biểu tượng trình duyệt trong trình khởi chạy (danh sách ứng dụng).\nsort_by=Sắp xếp theo\nwelcome=Chào mừng\nnew_folder=Thư mục mới\nskip=Bỏ qua\nlets_go=Đi nào\nnext=Tiếp\nselect_all=Chọn tất cả\nselect_inside=Chọn bên trong\nselect_invert=Chọn đảo ngược\nopen_settings=Mở cài đặt\nback=Quay lại\nservice_is_running=Dịch vụ đang chạy\ninitial_setup_description=Hãy cùng thiết lập nào\ninitial_setup_notice=Bạn có thể thay đổi các cài đặt này bất cứ lúc nào sau này\npermission_granted=Đã được cấp quyền\npermission_not_granted=Chưa cấp phép quyền\npermissions=Quyền\ngive_permission=Cấp quyền\ngive_storage_permission=Cho phép truy cập bộ nhớ\nstorage_roots=Thư mục gốc lưu trữ\npermissions_initial_title=Thiết lập quyền\npermissions_initial_description=Để hoạt động bình thường, ứng dụng cần một vài quyền. Trên màn hình tiếp theo, bạn sẽ thấy mỗi quyền được sử dụng cho mục đích gì và bạn có thể quyết định cho phép hoặc bỏ qua quyền nào.\npermissions_done_title=Bạn đã sẵn sàng\npermissions_done_description=Mọi thứ đã sẵn sàng. Tất cả các quyền cần thiết đã được cấp và ứng dụng đã sẵn sàng hoạt động.\npermissions_manage_storage_title=Quản lý quyền truy cập lưu trữ\npermissions_manage_storage_reason=Quyền này cho phép ứng dụng thay đổi thư mục tải xuống, phát hiện các tệp tải xuống trùng lặp chính xác hơn và kích hoạt một số tính năng bổ sung. Quyền này là tùy chọn, nhưng được khuyến nghị để có trải nghiệm tốt nhất.\npermission_read_write_external_storage_title=Lưu trữ đọc và ghi\npermission_read_write_external_storage_reason=Quyền này cho phép ứng dụng lưu và quản lý các tệp đã tải xuống, thay đổi vị trí tải xuống và cải thiện khả năng phát hiện các tệp tải xuống trùng lặp.\npermissions_post_notification_title=Gửi thông báo\npermissions_post_notification_reason=Ứng dụng cần chạy ngầm để quản lý các lượt tải xuống. Thông báo được sử dụng để thông báo cho bạn và cho phép ứng dụng hoạt động trong nền.\npermissions_ignore_battery_optimization_title=Bỏ qua tối ưu hóa pin\npermissions_ignore_battery_optimization_reason=Một số thiết bị hạn chế mạnh mẽ các hoạt động nền để tiết kiệm pin, điều này có thể tạm dừng hoặc dừng quá trình tải xuống khi ứng dụng không được mở. Bạn có thể tùy chọn loại trừ ứng dụng khỏi tính năng tối ưu hóa pin để đảm bảo quá trình tải xuống tiếp tục diễn ra không bị gián đoạn\nopen_in_browser=Mở bằng trình duyệt\nbrowser=Trình duyệt\nbrowser_new_tab=Thẻ mới\nbrowser_close_tab=Đóng thẻ\nbrowser_open_in_new_tab=Mở trong thẻ mới\nbrowser_open_in_new_background_tab=Mở trong thẻ nền mới\nbrowser_no_tab_open=Không có thẻ nào đang mở\nbrowser_tabs=Thẻ\nbrowser_paste_and_go=Dán và truy cập\nbrowser_bookmarks=Dấu trang\nbrowser_add_bookmark=Thêm dấu trang\nbrowser_edit_bookmark=Chỉnh sửa dấu trang\nbrowser_add_to_bookmarks=Thêm vào dấu trang\nbrowser_remove_from_bookmarks=Xoá khỏi dấu trang\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/zh_CN.properties",
    "content": "app_title=AB 下载管理器\nconfirm_auto_categorize_downloads_title=自动分类下载项\nconfirm_auto_categorize_downloads_description=所有未分类的下载项将自动归入相应分类。\nconfirm_reset_to_default_categories_title=重置为默认分类\nconfirm_reset_to_default_categories_description=这将删除所有自定义分类并恢复默认分类！\nconfirm_delete_download_items_title=确认删除\nconfirm_delete_download_items_description=您确定要删除这 {{count}} 个下载项吗？\nconfirm_delete_download_unfinished_items_description=您确定要删除 {{count}} 个未完成的下载项吗？\nconfirm_delete_download_finished_and_unfinished_items_description=您确定要删除 {{finishedCount}} 个已完成和 {{unfinishedCount}} 个未完成的下载项吗？\nalso_delete_file_from_disk=同时从硬盘中删除文件\nconfirm_delete_category_item_title=删除 {{name}} 分类\nconfirm_delete_category_item_description=您确定要删除 “{{value}}” 分类吗？\nyour_download_will_not_be_deleted=您的下载项将被保留\ndrag_the_file_to_another_app=将文件拖动到另一个应用程序\ndrop_link_or_file_here=将链接或文件拖到此处\nnothing_will_be_imported=没有可导入的项目\nn_links_will_be_imported={{count}} 个链接将被导入\nn_items_selected=已选中 {{count}} 个下载项\nwindow_close=关闭\nwindow_minimize=最小化\nwindow_maximize=最大化\nwindow_restore=还原\ndelete=删除\nremove=移除\ncancel=取消\nclose=关闭\nmenu=菜单\nmore_options=更多选项\nok=确定\nadd=添加\npaste=粘贴\nchange=修改\nedit=编辑\nchange_anyway=继续更改\ndownload=下载\nrefresh=刷新\nsettings=设置\non_completion=完成后\nunknown=未知\nunknown_error=未知错误\ndownload_item_not_found=找不到下载项\nname=名称\ndownload_link=下载链接\nnot_finished=未完成\nall=全部\nfinished=已完成\nUnfinished=未完成\ncanceled=已取消\nerror=错误\npaused=已暂停\ndownloading=正在下载\nadded=已添加\nidle=空闲\npreparing_file=正在准备文件\ncreating_file=正在创建文件\nresuming=正在恢复\nretrying=正在重试\nlist_is_empty=列表为空！\nsearch_in_the_list=在列表中搜索\nsearch=搜索\nclear=清空\ngeneral=常规\nenabled=启用\ndisabled=禁用\ndefault=默认\nfile=文件\ntasks=任务\ntools=工具\nhelp=帮助\nsystem=系统设置\nall_missing_files=所有缺失的文件\nall_finished=全部完成\nall_unfinished=全部未完成\nentire_list=完整列表\ndownload_browser_integration=下载浏览器插件\nexit=退出\nshow_downloads=查看下载项\nnew_download=新建下载\nstop_all=全部停止\nimport_from_clipboard=从剪切板导入\nbatch_download=批量下载\nopen=打开\nshare=分享\nopen_file=打开文件\nopen_folder=打开文件夹\nresume=恢复\npause=暂停\nrestart_download=重新下载\ncopy=复制\ncopy_link=复制链接\ncopy_as_curl=复制为 cURL\nshow_properties=查看属性\nmove_to_queue=移动到队列\nmove_to_this_queue=移动到此队列\nmove_to_category=移动到分类\nmove_to_this_category=移动到此分类\ncategories=分类\nadd_category=添加分类\nedit_category=编辑分类\ndelete_category=删除分类\ncategory_name=分类名称\ncategory_download_location=分类下载位置\ncategory_download_location_description=在 “添加下载” 时选择此分类后，将使用此目录作为 “下载位置”\ncategory_file_types=分类文件类型\ncategory_file_types_description=添加新下载项时，将自动把这些类型的文件归入此分类\\n请用空格分隔文件扩展名（例如：ext1 ext2 ...）\ncategory_url_patterns=URL 匹配规则\ncategory_url_patterns_description=自动将来自这些 URL 的下载放入此类别。(当您添加新下载时)\\n用空格分隔 URL，您也可以使用 * 作为通配符\nauto_categorize_downloads=自动分类下载项\nrestore_defaults=恢复默认设置\nabout=关于\nversion_n=版本 {{value}}\ndeveloped_with_love_for_you=用❤️为您开发\ndonate=捐赠\nvisit_the_project_website=访问项目官网\nthis_is_a_free_and_open_source_software=这是一个免费的开源软件\nview_the_source_code=查看源代码\nthird_party_libraries=依赖\npowered_by_open_source_software=由开源软件驱动\nview_the_open_source_licenses=查看开源协议\nsupport_and_community=支持 & 社区\ntelegram=Telegram\nchannel=频道\ngroup=群组\nadd_download=添加下载\nadd_multi_download_page_header=选择您想要下载的文件\nsave_to=保存到\nwhere_should_each_item_saved=每个文件应该保存到哪里？\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=发现了多个文件！请选择一个保存它们的地方\neach_item_on_its_own_category=每个文件都有属于自己的分类\neach_item_on_its_own_category_description=所有的文件都会基于它们的文件类型被保存到对应的分类下\nall_items_in_one_category=所有文件都保存在同一分类\nall_items_in_one_category_description=所有的文件都会被保存到选定的分类位置\nall_items_in_one_Location=所有文件都保存在同一位置\nall_items_in_one_Location_description=所有的文件都会被保存到选定的目录\nunselected_all_items_in_specific_location_description=所有文件将保存在选定的分类位置\nno_category_selected=未选择分类\nno_categories_found=未找到分类\ndownload_location=下载位置\nlocation=位置\nselect_queue=选择队列\nwithout_queue=不使用队列\nuse_category=分类到\ncant_write_to_this_folder=无法写入该目录\nfile_name_already_exists=文件名已存在\ndownload_already_exists=已存在下载项\ninvalid_file_name=文件名无效\nshow_solutions=查看解决方案...\nchange_solution=更改解决方案\nselect_a_solution=选择一个解决方案\nselect_download_strategy_description=您提供的下载链接已经在下载列表里面了，请选择您想做的操作\ndownload_strategy_add_a_numbered_file=在文件后添加编号\ndownload_strategy_add_a_numbered_file_description=在文件名末尾添加编号\ndownload_strategy_override_existing_file=覆盖现有文件\ndownload_strategy_override_existing_file_description=移除现有下载项并开始下载\ndownload_strategy_update_download_link=更新现有的下载\ndownload_strategy_update_download_link_description=更新现有的下载链接及其凭据\ndownload_strategy_show_downloaded_file=显示已下载的文件\ndownload_strategy_show_downloaded_file_description=显示已经存在的下载项以便您恢复下载或打开它\nbatch_download_link_help=输入包含通配符的链接 (使用*)\ninvalid_url=无效的URL\nlist_is_too_large_maximum_n_items_allowed=批量下载的文件数太多啦！最大支持数量为 {{count}}\nenter_range=输入范围\nrange_from=从\nrange_to=到\nbatch_download_wildcard_length=通配符长度\nfirst_link=首个链接\nlast_link=末尾链接\nopen_source_software_used_in_this_app=此App使用到的开源软件\nlinks=链接\nwebsite=网站\ndevelopers=开发者\nsource_code=源代码\nlicense=许可证\nno_license_found=找不到许可证\norganization=组织\nadd_new_queue=添加新的队列\nqueue_name=队列名称\nqueues=队列\nstop_queue=停止队列\nstart_queue=开始队列\nclear_queue_items=清空队列\nconfig=配置\nitems=项目\nmove_down=下移\nmove_up=上移\nremove_queue=移除队列\nqueue_name_help=为队列指定名称\nqueue_name_describe=队列名称为 {{value}}\nqueue_max_concurrent_download=下载最大并发数\nqueue_max_concurrent_download_description=此队列最大同时下载个数\nqueue_automatic_stop=自动停止\nqueue_automatic_stop_description=当队列中没有下载项时自动停止\nqueue_scheduler=计划任务\nqueue_enable_scheduler=启用计划任务\nqueue_active_days=每周运行日\nqueue_active_days_description=此计划任务应该在哪些日子启动？\nqueue_scheduler_enable_auto_start_time=启用自动开始时间\nqueue_scheduler_auto_start_time=自动开始时间\nqueue_scheduler_enable_auto_stop_time=启用自动停止计划\nqueue_scheduler_auto_stop_time=自动停止时间\nqueue_shutdown_on_completion=完成后关机\nqueue_shutdown_on_completion_description=当此队列完成或达到计划结束时间时，自动关机。\nappearance=外观\ndownload_engine=下载引擎\nbrowser_integration=浏览器插件集成\nsettings_download_max_retries_count=最大下载重试次数\nsettings_download_max_retries_count_description=应用在放弃之前重新尝试失败下载的最大次数\nsettings_download_max_retries_count_describe_no_retries=下载失败不会重试\nsettings_download_max_retries_count_describe_n_retries=下载失败将重试 {{count}} 次\nsettings_download_thread_count=线程数目\nsettings_download_thread_count_description=每个下载项的最大线程数目\nsettings_download_thread_count_describe=每个下载项最高可达 {{count}} 个线程\nsettings_download_thread_count_with_large_value_describe=警告：设置较高的线程数可能会增加系统资源的使用，降低性能，或导致与服务器的连接问题。只有在了解其对系统和网络潜在影响的情况下，才应使用更高的值。\nsettings_use_server_last_modified_time=使用服务器提供的最后修改时间\nsettings_use_server_last_modified_time_description=下载文件时，为本地文件使用服务器提供的最后修改时间\nsettings_append_extension_to_incomplete_downloads=将扩展名添加到未完成的下载\nsettings_append_extension_to_incomplete_downloads_description=将 “.part” 扩展名附加到未完成的下载文件。这有助于识别未完成的下载，并防止意外打开不完整的文件。\nsettings_use_sparse_file_allocation=稀疏文件空间分配\nsettings_use_sparse_file_allocation_description=使用稀疏文件分配方式，通过减少不必要的数据写入来提高文件创建效率（尤其是在SSD上）。这可以加快下载启动速度并减少硬盘占用。如果发现下载启动较慢或出现异常的下载速度，建议禁用此选项，因为某些设备可能不完全支持此功能。\nsettings_ignore_ssl_certificates=忽略 SSL 证书\nsettings_ignore_ssl_certificates_description=禁用 SSL 证书验证。仅在必要时使用，因为这可能会使您的连接面临安全风险。\nsettings_global_speed_limiter=全局速度限制\nsettings_global_speed_limiter_description=全局下载速度限制 (0 表示无限制)\nsettings_show_average_speed=显示平均速度\nsettings_show_average_speed_description=下载速度\nsettings_use_category_by_default=默认使用分类\nsettings_use_category_by_default_description=添加下载时默认使用分类。\nsettings_default_download_folder=默认下载文件夹\nsettings_default_download_folder_description=当您添加新的下载项时，默认使用此位置保存文件\nsettings_default_download_folder_describe=将使用 {{folder}}\nsettings_use_proxy=使用代理\nsettings_use_proxy_description=为下载使用代理\nsettings_use_proxy_describe_no_proxy=不使用代理\nsettings_use_proxy_describe_system_proxy=使用系统代理\nsettings_use_proxy_describe_manual_proxy=将使用 {{value}}\nsettings_use_proxy_describe_pac_proxy=将使用 pac 文件 “{{value}}”\nsettings_track_deleted_files_on_disk=跟踪磁盘上的已删除文件\nsettings_track_deleted_files_on_disk_description=当文件从下载目录中删除或移动时，自动将其从列表中移除。\nsettings_delete_partial_file_on_download_cancellation=下载取消时删除分块文件\nsettings_delete_partial_file_on_download_cancellation_description=当下载被取消时，分块文件将从磁盘中删除。这有助于保持下载文件夹的整洁，并减少不必要的磁盘空间占用。然而，下次重新开始下载时，下载将从头开始。\nsettings_default_user_agent=默认的用户代理\nsettings_default_user_agent_description=指定默认的用户代理字符串，以定义请求如何向服务器标识。这有助于访问为特定设备优化的内容或绕过某些网站施加的下载限制。\nsettings_download_size_unit=下载大小单位\nsettings_download_size_unit_description=用于显示下载大小的单位\nsettings_download_speed_unit=下载速度单位\nsettings_download_speed_unit_description=用于显示下载速度的单位\nsettings_theme=主题\nsettings_theme_description=选择应用主题\nsettings_default_dark_theme=默认深色主题\nsettings_default_dark_theme_description=当应用跟随系统主题且使用深色模式时\nsettings_default_light_theme=默认浅色主题\nsettings_default_light_theme_description=当应用跟随系统主题且使用浅色模式时\nsettings_font=字体\nsettings_font_description=更改应用界面中使用的字体，某些字体可能无法在应用中正确显示。\nsettings_ui_scale=用户界面缩放\nsettings_ui_scale_description=调整应用界面元素的大小\nsettings_language=语言\nsettings_compact_top_bar=紧凑型顶栏\nsettings_compact_top_bar_description=当主窗口宽度足够时，合并顶栏和标题栏\nsettings_use_native_menu_bar=使用原生菜单栏\nsettings_use_native_menu_bar_description=使用系统默认的菜单栏样式\nsettings_use_relative_date_time=使用相对日期/时间\nsettings_use_relative_date_time_description=在应用中使用相对日期/时间格式 (例如 “2天前” 而不是具体的日期/时间)\nsettings_show_icon_labels=显示图标标签\nsettings_show_icon_labels_description=尽可能在图标下方显示标签（例如首页工具栏操作）\nsettings_use_system_tray=使用系统托盘\nsettings_use_system_tray_description=当应用程序运行时显示系统托盘图标\nsettings_start_on_boot=开机自启\nsettings_start_on_boot_description=在用户登录时自动启动\nsettings_notification_sound=通知声音\nsettings_notification_sound_description=通知时播放声音\nsettings_browser_integration=浏览器插件集成\nsettings_browser_integration_description=接受来自浏览器的下载\nsettings_browser_integration_server_port=服务器端口\nsettings_browser_integration_server_port_description=浏览器插件使用的端口\nsettings_browser_integration_server_port_describe=App 将监听端口 {{port}}\nsettings_dynamic_part_creation=动态分段\nsettings_dynamic_part_creation_description=当一段下载完成后，通过拆分其他段来创建另一个下载段，以提高下载速度\nsettings_show_completion_dialog=显示下载完成对话框\nsettings_show_completion_dialog_description=下载完成时自动显示 “下载完成” 对话框。\nsettings_show_download_progress_dialog=显示下载进度对话框\nsettings_show_download_progress_dialog_description=开始下载时自动显示 “下载进度” 对话框。\nsettings_per_host_settings=分主机设置\nsettings_per_host_settings_descriptions=这些设置将自动应用于所有符合指定主机的新下载项。\nsettings_download_max_concurrent_downloads=最大并发下载数\nsettings_download_max_concurrent_downloads_description=同时下载文件数上限（队列管理的下载不计入；设置为0表示无限制）\ndownload_item_settings_speed_limit=速度限制\ndownload_item_settings_speed_limit_description=为此下载项限制下载速度\ndownload_item_settings_show_download_completion_dialog=显示下载完成对话框\ndownload_item_settings_show_download_completion_dialog_description=此下载项完成时自动显示 “下载完成” 对话框。\ndownload_item_settings_shutdown_on_completion=完成后关机\ndownload_item_settings_shutdown_on_completion_description=下载完成后自动关机。\ndownload_item_settings_thread_count=线程数\ndownload_item_settings_thread_count_description=此下载项使用多少个线程 （0代表使用全局设置）\ndownload_item_settings_thread_count_describe=为此下载项使用 {{count}} 线程\ndownload_item_settings_username_description=若链接指向受保护的资源，请提供用户名\ndownload_item_settings_password_description=若链接指向受保护的资源，请提供密码\ndownload_item_settings_download_page=下载页\ndownload_item_settings_download_page_description=此下载项开始下载时所处的网页\ndownload_item_settings_file_checksum=文件校验和\ndownload_item_settings_file_checksum_description=用于检查文件是否正确下载的哈希字符串\ndownload_item_settings_user_agent=用户代理\ndownload_item_settings_user_agent_description=对此物品使用自定义代理 (留空以使用预设代理)\nfile_checksum=文件校验和\nfile_checksum_page=文件校验和检查器\nfile_checksum_page_file_checksum_default_algorithm=默认算法\nfile_checksum_page_file_checksum_default_algorithm_help=未提供时用于计算文件校验和的默认算法。\nstart=开始\ncalculated_checksum=计算出的校验和\nsaved_checksum=保存的校验和\nchecksum_algorithm=算法\nfile_not_found=找不到文件\ndownload_not_finished=下载未完成\ndone=已完成\nwaiting=等待中\nmatches=匹配\nnot_matches=不匹配\ncopy_to_clipboard=复制到剪贴板\nusername=用户名\npassword=密码\naverage_speed=平均速度\nexact_speed=精确速度\nunlimited=无限制\nuse_global_settings=使用全局设置\ncant_run_browser_integration=无法运行浏览器插件集成\ncant_open_file=无法打开文件\ncant_open_folder=无法打开目录\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} 年\nrelative_time_long_months={{months}} 月\nrelative_time_long_days={{days}} 天\nrelative_time_long_hours={{hours}} 小时\nrelative_time_long_minutes={{minutes}} 分钟\nrelative_time_long_seconds={{seconds}} 秒\nrelative_time_short_years={{years}} 年\nrelative_time_short_months={{months}} 月\nrelative_time_short_days={{days}} 天\nrelative_time_short_hours={{hours}} 小时\nrelative_time_short_minutes={{minutes}} 分\nrelative_time_short_seconds={{seconds}} 秒\nrelative_time_left=剩余 {{time}}\nrelative_time_ago={{time}} 前\nauto=自动\nunspecified=未指定\ncustom=自定义\nicon=图标\nauthor=作者\nlink=链接\nsize=大小\nstatus=状态\nparts_info_downloaded_size=已下载\nparts_info_total_size=总大小\nspeed=速度\ntime_left=剩余时间\ndate_added=加入时间\ninfo=信息\ndownload_page_downloaded_size=已下载\ndownload_page_download_completed=下载已完成\nresume_support=断点续传\nyes=是\nno=否\nparts_info=分段信息\ndisconnected=断开连接\nreceiving_data=接收数据\nconnecting=发送GET请求\nwarning=警告\nunsupported_resume_warning=此下载项不支持断点续传！您可能需要在下载列表中重新下载此文件\nstop_anyway=强制停止\ncustomize_columns=自定义列\nreset=重置\nmonday=星期一\ntuesday=星期二\nwednesday=星期三\nthursday=星期四\nfriday=星期五\nsaturday=星期六\nsunday=星期日\nproxy_open_system_proxy_settings=使用系统代理设置\nproxy_type=代理类型\nproxy_do_not_use_proxy_for=对以下网址不使用代理\nproxy_do_not_use_proxy_for_description=一个不应被代理的网址列表\\n您可以使用通配符 *\\n比如 192.168.1.* example.com (两个网址用空格隔开)\nproxy_change_title=更改代理\nchange_proxy=更改代理\nproxy_no=无代理\nproxy_system=系统代理\nproxy_manual=手动设置代理\nproxy_pac=自动代理配置（PAC）\nproxy_pac_url=自动代理配置（PAC） URL\naddress=地址\nport=端口\naddress_and_port=地址 & 端口\nuse_authentication=使用身份验证\nwarning_you_may_have_to_restart_the_download_later=您可能需要稍后重新开始下载！\nedit_download_title=编辑下载\nedit_download_update_from_download_page=从下载页更新\nedit_download_update_from_download_page_description=当此窗口打开时，您可以转到下载页面并单击下载按钮。本应用将捕获并更新新的下载凭据，以便您保存它们。\nedit_download_saved_download_item_size_not_match=已保存的下载项的大小为 {{currentSize}}，与新的大小 {{newSize}} 不匹配。\ntranslators_page_thanks=感谢那些帮助翻译这个项目的人❤️\ntranslators=翻译人员\nlanguage=语言\ntranslators_contribute_title=改进翻译\ntranslators_contribute_description=想要帮助改进这个项目吗？如果您使用的语言尚未被收录，或者现有翻译需要改进，欢迎贡献您的翻译，让这个项目变得更好！\ncontribute=贡献\nmeet_the_translators=查看翻译人员\nlocalized_by_translators=由翻译人员提供本地化\nconfirm_exit=确认退出\nconfirm_exit_description=您确定要退出 AB 下载管理器吗？\\n正在进行的下载和队列都将被停止！\nupdate=更新\nupdate_updater=更新程序\nupdate_available=有可用的更新\nupdate_error=更新失败\nupdate_available_suggest_to_to_update=您可以更新到最新版本，以享受新功能、增强功能和性能提升。\nupdate_release_notes=更新日志\nupdate_check_for_update=检查更新\nupdate_checking_for_update=正在检查更新\nupdate_no_update=您正在使用最新版本\nupdate_check_error=检查更新时出错\nupdate_app_updated_to_version_n=应用已更新至版本 {{version}}\ncreate_desktop_entry=创建桌面条目\nshutdown_alert=关机警告\nsystem_shutdown_soon=系统将很快关机！\nsystem_shutdown_failed=系统关机失败！\nsystem_shutdown_soon_description=系统即将关机。如果您仍在使用计算机，请保存您的工作或取消关机。\nsystem_shutdown_reason_queue_completed=队列中的所有下载已完成。\nsystem_shutdown_reason_queue_end_time_reached=下载队列的预定结束时间已到。\nsystem_shutdown_download_finished=下载完毕。\nshutdown_now=立即关机\nsettings_per_host_settings_new_host=<新主机>\nsettings_per_host_settings_not_selected=首先创建或选择一个新项目 ！\nsettings_per_host_settings_host=主机名\nsettings_per_host_settings_host_description=这些设置将应用于匹配此主机名的下载。支持通配符(*) (例如，example.com，*.example.com — 仅使用一个)。\nsettings_browser_in_launcher=启动器里的浏览器图标\nsettings_browser_in_launcher_description=是否在启动器中显示浏览器图标 (应用列表)。\nsort_by=排序方式\nwelcome=欢迎\nnew_folder=新建文件夹\nskip=跳过\nlets_go=开始\nnext=下一步\nselect_all=全选\nselect_inside=在内部选择\nselect_invert=反选\nopen_settings=打开设置\nback=返回\nservice_is_running=服务运行中\ninitial_setup_description=让我们开始设置\ninitial_setup_notice=您可以稍后随时修改这些设置\npermission_granted=已授予权限\npermission_not_granted=未授予权限\npermissions=权限\ngive_permission=允许权限\ngive_storage_permission=允许访问存储\nstorage_roots=存储根目录\npermissions_initial_title=权限设置\npermissions_initial_description=为了正常运行，应用需要一些权限。在下一屏幕上，您将看到每个权限的用途，并可以决定是允许还是跳过。\npermissions_done_title=一切就绪\npermissions_done_description=一切就绪。所有必要的权限已获得，应用可以正常使用。\npermissions_manage_storage_title=管理存储访问权限\npermissions_manage_storage_reason=此权限允许应用更改下载文件夹，更准确地检测重复下载，并启用一些额外功能。虽然是可选的，但为了获得最佳体验，建议启用。\npermission_read_write_external_storage_title=读写存储设备\npermission_read_write_external_storage_reason=此权限允许应用保存和管理下载的文件、修改下载位置，并改善重复下载检测。\npermissions_post_notification_title=发布通知\npermissions_post_notification_reason=应用需要在后台运行以管理下载。通知用于通知您，并允许后台操作。\npermissions_ignore_battery_optimization_title=忽略电池优化\npermissions_ignore_battery_optimization_reason=一些设备为了节省电池会积极限制后台活动，这可能会在应用未打开时暂停或停止下载。您可以选择将应用排除在电池优化之外，以确保下载不中断\nopen_in_browser=在浏览器中打开\nbrowser=浏览器\nbrowser_new_tab=新标签\nbrowser_close_tab=关闭标签\nbrowser_open_in_new_tab=在新标签中打开\nbrowser_open_in_new_background_tab=在后台打开新标签页\nbrowser_no_tab_open=没有打开的标签页\nbrowser_tabs=标签页\nbrowser_paste_and_go=粘贴并转到\nbrowser_bookmarks=收藏夹\nbrowser_add_bookmark=添加收藏\nbrowser_edit_bookmark=编辑收藏\nbrowser_add_to_bookmarks=添加收藏\nbrowser_remove_from_bookmarks=从收藏中移除\n"
  },
  {
    "path": "shared/resources/src/commonMain/resources/com/abdownloadmanager/resources/locales/zh_TW.properties",
    "content": "app_title=AB 下載管理員\nconfirm_auto_categorize_downloads_title=自動分類下載項目\nconfirm_auto_categorize_downloads_description=未分類的下載項目都將自動歸類\nconfirm_reset_to_default_categories_title=重設為預設類別\nconfirm_reset_to_default_categories_description=這將刪除所有分類並復原預設分類！\nconfirm_delete_download_items_title=確認刪除\nconfirm_delete_download_items_description=您確定要刪除這 {{count}} 個下載項目嗎？\nconfirm_delete_download_unfinished_items_description=您確定要刪除 {{count}} 個未完成的下載檔案嗎？\nconfirm_delete_download_finished_and_unfinished_items_description=您確定要刪除 {{finishedCount}} 個已完成和 {{unfinishedCount}} 個未完成的下載檔案嗎？\nalso_delete_file_from_disk=同時從磁碟中刪除檔案\nconfirm_delete_category_item_title=確定要移除 {{name}} 類別嗎\nconfirm_delete_category_item_description=你確定要刪除 {{value}} 類別嗎？\nyour_download_will_not_be_deleted=將保留您的下載項目\ndrag_the_file_to_another_app=將檔案拖曳到另一個應用程式\ndrop_link_or_file_here=將連結或檔案拖曳至此\nnothing_will_be_imported=沒有可匯入的項目\nn_links_will_be_imported={{count}} 個連結將被匯入\nn_items_selected=已選取 {{count}} 個下載項目\nwindow_close=關閉\nwindow_minimize=最小化\nwindow_maximize=最大化\nwindow_restore=還原\ndelete=刪除\nremove=移除\ncancel=取消\nclose=關閉\nmenu=選單\nmore_options=更多選項\nok=確定\nadd=新增\npaste=貼上\nchange=修改\nedit=編輯\nchange_anyway=仍要變更\ndownload=下載\nrefresh=重新整理\nsettings=設定\non_completion=完成\nunknown=未知\nunknown_error=未知錯誤\ndownload_item_not_found=找不到下載項目\nname=名稱\ndownload_link=下載連結\nnot_finished=未完成\nall=全部\nfinished=已完成\nUnfinished=未完成\ncanceled=已取消\nerror=錯誤\npaused=已暫停\ndownloading=下載中\nadded=已添加\nidle=閒置中\npreparing_file=正在準備檔案\ncreating_file=正在建立檔案\nresuming=正在復原\nretrying=正在重試\nlist_is_empty=清單為空！\nsearch_in_the_list=在清單中搜尋\nsearch=搜尋\nclear=清空\ngeneral=一般\nenabled=啟用\ndisabled=已停用\ndefault=預設\nfile=檔案\ntasks=任務\ntools=工具\nhelp=說明\nsystem=系統\nall_missing_files=所有遺失的檔案\nall_finished=全部完成\nall_unfinished=全部未完成\nentire_list=完整清單\ndownload_browser_integration=下載瀏覽器整合\nexit=結束\nshow_downloads=顯示下載\nnew_download=新增下載\nstop_all=全部停止\nimport_from_clipboard=從剪貼簿匯入\nbatch_download=批次下載\nopen=開啟\nshare=分享\nopen_file=開啟檔案\nopen_folder=開啟資料夾\nresume=繼續下載\npause=暫停\nrestart_download=重新下載\ncopy=複製\ncopy_link=複製連結\ncopy_as_curl=複製為 cURL\nshow_properties=檢視內容\nmove_to_queue=移動到佇列\nmove_to_this_queue=移動到此佇列\nmove_to_category=移動到類別\nmove_to_this_category=移動到此類別\ncategories=類別\nadd_category=添加類別\nedit_category=編輯類別\ndelete_category=刪除類別\ncategory_name=類別名稱\ncategory_download_location=類別下載位置\ncategory_download_location_description=在「新增下載」中選擇此類別時，將此目錄用作「下載位置」\ncategory_file_types=類別檔案類型\ncategory_file_types_description=自動將這些檔案類型放入此類別。（新增下載時）用空格分隔檔案副檔名（ext1 ext2 ...）\ncategory_url_patterns=URL 模式\ncategory_url_patterns_description=自動將從這些 URL 下載的檔案歸類到此類別(新增下載時)。URL 之間以空白鍵隔開，可使用 * 萬用字元\nauto_categorize_downloads=自動分類下載項目\nrestore_defaults=復原為預設值\nabout=關於\nversion_n=版本 {{value}}\ndeveloped_with_love_for_you=用❤️為您開發\ndonate=贊助\nvisit_the_project_website=瀏覽專案官網\nthis_is_a_free_and_open_source_software=這是一個開源免費的軟體\nview_the_source_code=查看原始碼\nthird_party_libraries=第三方函式庫\npowered_by_open_source_software=由開源軟體驅動\nview_the_open_source_licenses=查看開源協議\nsupport_and_community=支援 & 社群\ntelegram=Telegram\nchannel=頻道\ngroup=群組\nadd_download=添加下載\nadd_multi_download_page_header=選擇您想要下載的檔案\nsave_to=儲存到\nwhere_should_each_item_saved=每個檔案應該儲存到哪裡？\nthere_are_multiple_items_please_select_a_way_you_want_to_save_them=發現了多個檔案！請選擇一個儲存的方法\neach_item_on_its_own_category=每個檔案都有屬於自己的類別\neach_item_on_its_own_category_description=所有的檔案都會基於它們的檔案類型被儲存到對應的類別下\nall_items_in_one_category=所有檔案都儲存在同一類別\nall_items_in_one_category_description=所有的檔案都會被儲存到選擇的類別位置\nall_items_in_one_Location=所有檔案都儲存在同一位置\nall_items_in_one_Location_description=所有的檔案都會被儲存到選擇的目錄\nunselected_all_items_in_specific_location_description=所有檔案都會被儲存到選擇的類別位置\nno_category_selected=未選擇類別\nno_categories_found=未找到類別\ndownload_location=下載位置\nlocation=位置\nselect_queue=選擇佇列\nwithout_queue=不使用佇列\nuse_category=使用分類\ncant_write_to_this_folder=無法寫入此目錄\nfile_name_already_exists=檔案名稱已存在\ndownload_already_exists=已存在下載\ninvalid_file_name=無效的檔案名稱\nshow_solutions=查看解決方案...\nchange_solution=變更解決方案\nselect_a_solution=選擇一個解決方案\nselect_download_strategy_description=您提供的下載連結已經在下載清單中了，請選擇您想做的操作\ndownload_strategy_add_a_numbered_file=在檔案後加入編號\ndownload_strategy_add_a_numbered_file_description=在下載檔案名稱的結尾加上編號\ndownload_strategy_override_existing_file=覆蓋現有檔案\ndownload_strategy_override_existing_file_description=移除現有下載項目並開始下載\ndownload_strategy_update_download_link=更新已有的下載\ndownload_strategy_update_download_link_description=更新現有的下載連結及其憑證\ndownload_strategy_show_downloaded_file=顯示已下載的檔案\ndownload_strategy_show_downloaded_file_description=顯示已經存在的下載項目以便您復原下載或開啟它\nbatch_download_link_help=輸入包含萬用字元的連結 (使用 *)\ninvalid_url=無效的網址\nlist_is_too_large_maximum_n_items_allowed=批次下載的檔案數量太多啦！最多支援 {{count}} 個\nenter_range=輸入範圍\nrange_from=從\nrange_to=到\nbatch_download_wildcard_length=萬用字元長度\nfirst_link=首個連結\nlast_link=末尾連結\nopen_source_software_used_in_this_app=這個應用程式使用的其他開源軟體\nlinks=連結\nwebsite=網站\ndevelopers=開發人員\nsource_code=原始碼\nlicense=授權條款\nno_license_found=找不到授權條款\norganization=組織\nadd_new_queue=新增佇列\nqueue_name=佇列名稱\nqueues=佇列\nstop_queue=停止佇列\nstart_queue=開始佇列\nclear_queue_items=佇列無任務\nconfig=設定\nitems=項目\nmove_down=下移\nmove_up=上移\nremove_queue=移除佇列\nqueue_name_help=指定名稱給此佇列\nqueue_name_describe=佇列名稱為 {{value}}\nqueue_max_concurrent_download=最大同時下載數量\nqueue_max_concurrent_download_description=此佇列最大同時下載數量\nqueue_automatic_stop=自動停止\nqueue_automatic_stop_description=當佇列中沒有下載項目時自動停止\nqueue_scheduler=排程器\nqueue_enable_scheduler=啟用排程器\nqueue_active_days=每週執行日\nqueue_active_days_description=這個排程器會在哪幾天運作？\nqueue_scheduler_enable_auto_start_time=啟用自動開始時間\nqueue_scheduler_auto_start_time=自動開始時間\nqueue_scheduler_enable_auto_stop_time=啟用自動停止計劃\nqueue_scheduler_auto_stop_time=自動停止時間\nqueue_shutdown_on_completion=完成後關閉系統\nqueue_shutdown_on_completion_description=系統將在當前佇列完成時或達到預定結束時間自動關閉。\nappearance=外觀\ndownload_engine=下載引擎\nbrowser_integration=瀏覽器擴充功能整合\nsettings_download_max_retries_count=最大下載重試次數\nsettings_download_max_retries_count_description=應用程式放棄失敗下載前，會重試的最大次數\nsettings_download_max_retries_count_describe_no_retries=下載失敗將不會重試\nsettings_download_max_retries_count_describe_n_retries=下載失敗時將會重試{{count}}次 (數)\nsettings_download_thread_count=執行緒數量\nsettings_download_thread_count_description=每個下載項目的最大執行緒數目\nsettings_download_thread_count_describe=每個下載項目最高可達 {{count}} 個執行緒\nsettings_download_thread_count_with_large_value_describe=警告：設定過高的執行緒數量可能會增加系統資源使用量、降低效能，或導致與伺服器的連線問題。請僅在您了解對系統和網路的潛在影響時，才使用較高的數值。\nsettings_use_server_last_modified_time=使用伺服器提供的最後修改時間\nsettings_use_server_last_modified_time_description=下載檔案時，為本機檔案使用伺服器提供的最後修改時間\nsettings_append_extension_to_incomplete_downloads=在未完成下載加上副檔名\nsettings_append_extension_to_incomplete_downloads_description=在未完成下載加上「.part」副檔名.。這將有助識別未完成的下載並防止意外開啟不完整的檔案。\nsettings_use_sparse_file_allocation=稀疏檔案配置\nsettings_use_sparse_file_allocation_description=透過減少不必要的資料寫入，更有效率地建立檔案，尤其是在 SSD 上。這可以加快下載開始速度並減少磁碟使用量。如果下載開始速度緩慢或遇到不正常的下載速度，請考慮停用此選項，因為它可能在某些裝置上未完全支援。\nsettings_ignore_ssl_certificates=忽略 SSL 憑證\nsettings_ignore_ssl_certificates_description=停用 SSL 憑證驗證。僅在必要時使用，因爲這可能會使連結面臨安全風險。\nsettings_global_speed_limiter=全域速度限制\nsettings_global_speed_limiter_description=全域下載速度限制(0 表示無限制)\nsettings_show_average_speed=顯示平均速度\nsettings_show_average_speed_description=平均下載速度或準確度\nsettings_use_category_by_default=使用預設類別\nsettings_use_category_by_default_description=新增下載時使用預設類別。\nsettings_default_download_folder=預設下載資料夾\nsettings_default_download_folder_description=當你新增下載項目時，預設會使用這個位置。\nsettings_default_download_folder_describe=將使用 {{folder}}\nsettings_use_proxy=使用代理\nsettings_use_proxy_description=為下載使用代理\nsettings_use_proxy_describe_no_proxy=不使用代理\nsettings_use_proxy_describe_system_proxy=使用系統代理\nsettings_use_proxy_describe_manual_proxy=將使用 {{value}}\nsettings_use_proxy_describe_pac_proxy=將使用 pac 檔案「{{value}}」\nsettings_track_deleted_files_on_disk=追蹤磁碟中已刪除的檔案\nsettings_track_deleted_files_on_disk_description=當檔案從下載目錄中被刪除或移動時，自動從清單中移除。\nsettings_delete_partial_file_on_download_cancellation=下載取消時，刪除部分檔案\nsettings_delete_partial_file_on_download_cancellation_description=當下載取消時，部分已下載的檔案將從磁碟中刪除。這有助於保持您的下載資料夾整潔，並減少不必要的磁碟空間使用量。然而，下次您啟動下載時，下載將會重新從頭開始。\nsettings_default_user_agent=預設的用戶代理\nsettings_default_user_agent_description=指定預設的用戶代理字符串，以定義請求如何向服務器標識。這有助於訪問為特定裝置優化的內容或繞過某些網站施加的下載限制。\nsettings_download_size_unit=下載大小單位\nsettings_download_size_unit_description=用於顯示下載大小的單位\nsettings_download_speed_unit=下載速度單位\nsettings_download_speed_unit_description=用來顯示下載速度的單位\nsettings_theme=主題\nsettings_theme_description=選擇應用程式主題\nsettings_default_dark_theme=預設深色模式\nsettings_default_dark_theme_description=此主題適用於系統主題和深色模式已啟用時\nsettings_default_light_theme=預設淺色主題\nsettings_default_light_theme_description=當應用程式遵循系統主題並啟用淺色模式時，將會生效\nsettings_font=字型\nsettings_font_description=變更字型來套用在此應用程式的介面。部分字型可能無法在此應用程式中正確顯示。\nsettings_ui_scale=使用者介面縮放\nsettings_ui_scale_description=調整應用程式介面元件的大小\nsettings_language=語言\nsettings_compact_top_bar=緊湊型頂欄\nsettings_compact_top_bar_description=當主視窗寬度足夠時，合併頂欄和標題欄\nsettings_use_native_menu_bar=使用原生選單列\nsettings_use_native_menu_bar_description=使用系統預設選單列風格\nsettings_use_relative_date_time=使用相對日期/時間\nsettings_use_relative_date_time_description=請使用相對日期/時間格式顯示應用程式中的日期（例如：“2 天前”而不是具體的日期和時間）\nsettings_show_icon_labels=顯示圖示標籤\nsettings_show_icon_labels_description=盡可能在圖示下方顯示標籤（例如首頁工具列操作）\nsettings_use_system_tray=使用系統匣\nsettings_use_system_tray_description=當應用程式執行時顯示系統匣圖示\nsettings_start_on_boot=開機時啟動\nsettings_start_on_boot_description=在使用者登入時自動啟動\nsettings_notification_sound=通知聲音\nsettings_notification_sound_description=通知時播放聲音\nsettings_browser_integration=瀏覽器擴充功能設定\nsettings_browser_integration_description=接受來自瀏覽器的下載\nsettings_browser_integration_server_port=伺服器連接埠\nsettings_browser_integration_server_port_description=瀏覽器擴充功能使用的連接埠\nsettings_browser_integration_server_port_describe=App 將監聽埠 {{port}}\nsettings_dynamic_part_creation=動態分段\nsettings_dynamic_part_creation_description=當一段下載完成後，透過分割其他段來建立另一個下載段，以提高下載速度\nsettings_show_completion_dialog=完成下載時顯示提示\nsettings_show_completion_dialog_description=下載完成後，自動顯示「下載完成」對話方塊。\nsettings_show_download_progress_dialog=顯示下載進度\nsettings_show_download_progress_dialog_description=下載開始後，自動顯示「下載進度」對話方塊。\nsettings_per_host_settings=每個主機的設定\nsettings_per_host_settings_descriptions=這些設定會自動套用到任何符合指定主機的新下載。\nsettings_download_max_concurrent_downloads=最大並發下載量\nsettings_download_max_concurrent_downloads_description=同時下載的最大數量（佇列管理的下載不計入在內；設定 0 為無限制）\ndownload_item_settings_speed_limit=速度限制\ndownload_item_settings_speed_limit_description=為此下載項目限制下載速度\ndownload_item_settings_show_download_completion_dialog=完成時顯示提示\ndownload_item_settings_show_download_completion_dialog_description=當這個下載完成後，自動顯示「下載完成」對話方塊。\ndownload_item_settings_shutdown_on_completion=完成時關機\ndownload_item_settings_shutdown_on_completion_description=在下載完成時自動關閉系統。\ndownload_item_settings_thread_count=執行緒數\ndownload_item_settings_thread_count_description=下載此下載項目時使用的執行緒數量（0 表示使用預設值）\ndownload_item_settings_thread_count_describe=為此下載項目使用 {{count}} 執行緒\ndownload_item_settings_username_description=若連結指向受保護的資源，請提供使用者名稱\ndownload_item_settings_password_description=若連結指向受保護的資源，請提供密碼\ndownload_item_settings_download_page=下載頁面\ndownload_item_settings_download_page_description=此下載項目開始下載時所處的網頁\ndownload_item_settings_file_checksum=檔案校驗和\ndownload_item_settings_file_checksum_description=一個可以用來檢查檔案是否正確下載的哈希字串\ndownload_item_settings_user_agent=User-Agent\ndownload_item_settings_user_agent_description=對此物品使用自定義代理 (留空以使用預設代理)\nfile_checksum=檔案校驗和\nfile_checksum_page=檔案校驗和檢查器\nfile_checksum_page_file_checksum_default_algorithm=預設演算法\nfile_checksum_page_file_checksum_default_algorithm_help=當未提供檔案校驗和時，用來計算檔案校驗和的預設演算法。\nstart=開始\ncalculated_checksum=計算出的校驗和\nsaved_checksum=儲存的校驗和\nchecksum_algorithm=演算法\nfile_not_found=找不到檔案\ndownload_not_finished=下載未完成\ndone=完成\nwaiting=等待中\nmatches=符合\nnot_matches=不符合\ncopy_to_clipboard=複製到剪貼簿\nusername=使用者名稱\npassword=密碼\naverage_speed=平均速度\nexact_speed=精確速度\nunlimited=無限制\nuse_global_settings=使用全域設定\ncant_run_browser_integration=無法執行瀏覽器擴充功能整合\ncant_open_file=無法打開檔案\ncant_open_folder=無法打開資料夾\n# times for example 2 seconds ago\nrelative_time_long_years={{years}} 年\nrelative_time_long_months={{months}} 月\nrelative_time_long_days={{days}} 日\nrelative_time_long_hours={{hours}} 時\nrelative_time_long_minutes={{minutes}} 分\nrelative_time_long_seconds={{seconds}} 秒\nrelative_time_short_years={{years}} 年\nrelative_time_short_months={{months}} 月\nrelative_time_short_days={{days}} 日\nrelative_time_short_hours={{hours}} 小時\nrelative_time_short_minutes={{minutes}} 分\nrelative_time_short_seconds={{seconds}} 秒\nrelative_time_left=剩餘 {{time}}\nrelative_time_ago={{time}} 前\nauto=自動\nunspecified=未指定\ncustom=自訂\nicon=圖示\nauthor=作者\nlink=連結\nsize=大小\nstatus=狀態\nparts_info_downloaded_size=已下載\nparts_info_total_size=總大小\nspeed=速度\ntime_left=剩餘時間\ndate_added=加入時間\ninfo=資訊\ndownload_page_downloaded_size=已下載\ndownload_page_download_completed=下載完成\nresume_support=斷點續傳\nyes=是\nno=否\nparts_info=分段資訊\ndisconnected=已中斷連線\nreceiving_data=正在接收資料\nconnecting=連線中\nwarning=警告\nunsupported_resume_warning=此下載項目不支援斷點續傳！你可能需要在下載清單中重新下載此檔案\nstop_anyway=強制停止\ncustomize_columns=自訂列\nreset=重設\nmonday=星期一\ntuesday=星期二\nwednesday=星期三\nthursday=星期四\nfriday=星期五\nsaturday=星期六\nsunday=星期日\nproxy_open_system_proxy_settings=開啟系統代理設定\nproxy_type=代理類型\nproxy_do_not_use_proxy_for=不使用代理的網址\nproxy_do_not_use_proxy_for_description=一個可能不會被代理的 Url 列表\\n你可以使用 * 作為萬用字元\\n例如 192.168.1.* example.com (用空格分隔)\nproxy_change_title=變更代理\nchange_proxy=變更代理\nproxy_no=無代理\nproxy_system=系統代理\nproxy_manual=手動代理\nproxy_pac=代理伺服器自動設定\nproxy_pac_url=代理伺服器自動設定網址\naddress=位址\nport=埠\naddress_and_port=位址與埠\nuse_authentication=使用驗證\nwarning_you_may_have_to_restart_the_download_later=你可能要稍後再重新開始下載！\nedit_download_title=編輯下載\nedit_download_update_from_download_page=從下載頁更新\nedit_download_update_from_download_page_description=當此視窗出現時，你可以至下載頁面點選下載。應用程式會自動擷取並更新新的下載資訊以便您儲存。\nedit_download_saved_download_item_size_not_match=已儲存的下載項目的大小為 {{currentSize}}，與新的大小 {{newSize}} 不相符。\ntranslators_page_thanks=感謝協助翻譯該專案的人 ❤️\ntranslators=譯者\nlanguage=語言\ntranslators_contribute_title=幫助改進翻譯\ntranslators_contribute_description=不支援你的語言或有需要改進的地方？不妨一起協助翻譯這個專案，讓這個專案變得越來越好！\ncontribute=貢獻\nmeet_the_translators=查看翻譯人員\nlocalized_by_translators=由翻譯人員在地化\nconfirm_exit=確認結束\nconfirm_exit_description=您確定要結束 AB Download Manager 嗎？\\n正在進行的下載/佇列將會停止！\nupdate=更新\nupdate_updater=更新程式\nupdate_available=有可用更新\nupdate_error=更新錯誤\nupdate_available_suggest_to_to_update=您可以更新至最新版本，以享有新功能、增強功能和效能提升。\nupdate_release_notes=版本說明\nupdate_check_for_update=檢查更新\nupdate_checking_for_update=正在檢查更新\nupdate_no_update=你正在使用最新版本\nupdate_check_error=檢查更新時發生錯誤\nupdate_app_updated_to_version_n=應用程式已更新至 {{version}}\ncreate_desktop_entry=建立桌面項目\nshutdown_alert=關機警報\nsystem_shutdown_soon=系統即將關閉！\nsystem_shutdown_failed=系統關機異常！\nsystem_shutdown_soon_description=系統即將關閉。如果您繼續使用電腦，請儲存您的資料或取消關機。\nsystem_shutdown_reason_queue_completed=所有下載任務已處理完畢。\nsystem_shutdown_reason_queue_end_time_reached=下載任務排隊結束時間已到達。\nsystem_shutdown_download_finished=下載已完成。\nshutdown_now=現在關機\nsettings_per_host_settings_new_host=<New Host>\nsettings_per_host_settings_not_selected=請先建立或選取一個新項目！\nsettings_per_host_settings_host=主機\nsettings_per_host_settings_host_description=這些設定將套用於符合此主機名稱的下載。支援萬用字元（*）（例如，example.com，*.example.com — 只使用一個）。\nsettings_browser_in_launcher=在啟動器中的瀏覽器圖示\nsettings_browser_in_launcher_description=在啟動器（應用程式列表）中顯示或隱藏瀏覽器圖示。\nsort_by=排序方式\nwelcome=歡迎\nnew_folder=新增資料夾\nskip=略過\nlets_go=我們走吧\nnext=下一步\nselect_all=選擇全部\nselect_inside=選取內部\nselect_invert=反向選取\nopen_settings=開啟設定\nback=返回\nservice_is_running=服務正在運行\ninitial_setup_description=讓我們開始設定吧\ninitial_setup_notice=您可以隨時變更這些設定\npermission_granted=權限已授予\npermission_not_granted=未授予權限\npermissions=權限\ngive_permission=允許權限\ngive_storage_permission=允許儲存空間存取權限\nstorage_roots=儲存根目錄\npermissions_initial_title=權限設定\npermissions_initial_description=為了正常運行，該應用程式需要一些權限。在下一個畫面上，您將看到每項權限的用途，您可以決定允許哪些權限，或跳過哪些權限。\npermissions_done_title=一切就緒\npermissions_done_description=一切準備就緒。所有必需的權限都已授予，應用程式可以正常運作了。\npermissions_manage_storage_title=管理儲存空間存取權限\npermissions_manage_storage_reason=此權限允許應用程式變更下載資料夾、更準確地偵測重複下載並啟用一些額外功能。此權限為可選，但為了獲得最佳體驗，建議授予。\npermission_read_write_external_storage_title=讀取與寫入儲存\npermission_read_write_external_storage_reason=此權限允許應用程式保存和管理下載的檔案、更改下載位置並改善重複下載檢測。\npermissions_post_notification_title=發布通知\npermissions_post_notification_reason=該應用程式需要在背景運行以管理下載。通知功能用於讓您隨時了解最新資訊並允許背景運行。\npermissions_ignore_battery_optimization_title=忽略電池最佳化\npermissions_ignore_battery_optimization_reason=有些設備為了省電會嚴格限制後台活動，這可能會導致應用程式未開啟時下載暫停或停止。您可以選擇將應用程式從電池優化中排除，以確保下載不間斷地進行\nopen_in_browser=以瀏覽器開啟\nbrowser=瀏覽器\nbrowser_new_tab=新標籤\nbrowser_close_tab=關閉標籤\nbrowser_open_in_new_tab=在新分頁中開啟\nbrowser_open_in_new_background_tab=在新背景分頁中開啟\nbrowser_no_tab_open=未開啟任何分頁\nbrowser_tabs=分頁\nbrowser_paste_and_go=貼上並前往\nbrowser_bookmarks=書籤\nbrowser_add_bookmark=新增書籤\nbrowser_edit_bookmark=編輯書籤\nbrowser_add_to_bookmarks=新增至書籤\nbrowser_remove_from_bookmarks=從書籤中移除\n"
  },
  {
    "path": "shared/updater/build.gradle.kts",
    "content": "import org.gradle.kotlin.dsl.implementation\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Android.library)\n    id(Plugins.Kotlin.serialization)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain.dependencies {\n            implementation(libs.kotlin.serialization.json)\n            api(libs.okhttp.okhttp)\n            api(libs.kotlin.coroutines.core)\n            implementation(project(\":shared:utils\"))\n            implementation(libs.semver)\n            implementation(\"ir.amirab.util:platform:1\")\n        }\n        val desktopMain by getting\n        desktopMain.dependencies {\n            implementation(libs.jna.platform)\n        }\n    }\n}\nandroid {\n    namespace = \"com.abdownloadmanager.updater\"\n    compileSdk = 36\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/androidMain/kotlin/AndroidDirectLinkUpdateApplier.kt",
    "content": "import com.abdownloadmanager.InstallableArch\nimport com.abdownloadmanager.updateapplier.BaseUpdateApplier\nimport com.abdownloadmanager.updateapplier.UpdateDownloader\nimport com.abdownloadmanager.updateapplier.UpdateInstaller\nimport com.abdownloadmanager.updateapplier.UpdatePreparer\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport com.abdownloadmanager.updatechecker.UpdateSource\n\n/**\n * this update applier works for direct downloads!\n */\nclass AndroidDirectLinkUpdateApplier(\n    private val updateDownloader: UpdateDownloader,\n) : BaseUpdateApplier() {\n    override fun updateSupported(): Boolean {\n        return true\n    }\n\n    override fun getUpdatePreparer(): UpdatePreparer {\n        return updateDownloader\n    }\n\n    override fun getBestDownloadSource(updateInfo: UpdateInfo): UpdateSource {\n        val downloadableSources =\n            updateInfo.updateSource.filterIsInstance<UpdateSource.DirectDownloadLink>()\n                .sortedBy {\n                    // universal downloads have bigger size so we put them last\n                    it.installableArch !is InstallableArch.Universal\n                }\n        val downloadSource = downloadableSources.find {\n            isApk(it.name)\n        }\n        return requireNotNull(downloadSource) {\n            \"Can't find proper download link for your platform! Please update it manually\"\n        }\n    }\n\n    override fun getUpdateInstaller(preparedUpdate: UpdatePreparer.PreparedUpdate): UpdateInstaller {\n        return ApkInstaller((preparedUpdate as UpdateDownloader.PreparedUpdateFile).file)\n    }\n\n    fun isApk(name: String): Boolean {\n        return name.endsWith(\".apk\")\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/androidMain/kotlin/ApkInstaller.kt",
    "content": "import com.abdownloadmanager.updateapplier.UpdateInstaller\nimport ir.amirab.util.osfileutil.FileUtils\nimport java.io.File\n\nclass ApkInstaller(\n    private val apkFile: File,\n) : UpdateInstaller {\n    override fun installUpdate() {\n        FileUtils.openFile(apkFile)\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/ArtifactUtil.kt",
    "content": "package com.abdownloadmanager\n\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.contract\n\ndata class AppArtifactInfo(\n    val version: Version,\n    val platform: Platform,\n    val arch: InstallableArch,\n)\n\nsealed interface InstallableArch {\n    fun isCompatible(arch: Arch): Boolean\n    data object Universal : InstallableArch {\n        override fun isCompatible(arch: Arch): Boolean {\n            return true\n        }\n        private val possibleNames = listOf(\n            \"universal\",\n            null,\n        )\n\n        fun fromString(arch: String?): InstallableArch? {\n            return if (arch?.lowercase() in possibleNames) {\n                InstallableArch.Universal\n            } else {\n                null\n            }\n        }\n    }\n\n    data class SomeArch(val arch: Arch) : InstallableArch {\n        override fun isCompatible(arch: Arch): Boolean {\n            return this.arch == arch\n        }\n\n        companion object {\n            fun fromString(arch: String?): InstallableArch? {\n                return arch\n                    ?.let(Arch::fromString)\n                    ?.let(::SomeArch)\n            }\n        }\n\n    }\n\n    companion object {\n        fun fromString(archName: String?): InstallableArch? {\n            return listOf(\n                Universal::fromString,\n                SomeArch::fromString,\n            ).firstNotNullOf {\n                it(archName)\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalContracts::class)\nfun InstallableArch.isUniversal(): Boolean {\n    contract {\n        returns(true) implies (this@isUniversal is InstallableArch.Universal)\n    }\n    return this is InstallableArch.Universal\n}\n\n\nobject ArtifactUtil {\n    val artifactRegex =\n        \"(?<appName>[a-zA-Z]+)_(?<version>(\\\\d+\\\\.\\\\d+\\\\.\\\\d+))_(?<platform>[a-zA-Z]+)_(?<arch>[a-zA-Z0-9]+)\\\\.(?<extension>.+)\".toRegex()\n\n    fun getArtifactInfo(name: String): AppArtifactInfo? {\n        val values = artifactRegex.find(name)?.groups ?: return null\n        val version = runCatching { values.get(\"version\")?.value }\n            .getOrNull()\n            ?.let(Version::parse)\n            ?: return null\n        val platform = runCatching { values.get(\"platform\")?.value }\n            .getOrNull()\n            ?.let(Platform::fromString)\n            ?: return null\n        val arch = runCatching { values.get(\"arch\")?.value }\n            .getOrNull()\n            ?.let(InstallableArch::fromString)\n            ?: return null\n        return AppArtifactInfo(\n            version = version,\n            platform = platform,\n            arch = arch,\n        )\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/UpdateDownloadLocationProvider.kt",
    "content": "package com.abdownloadmanager\n\nimport java.io.File\n\nfun interface UpdateDownloadLocationProvider {\n    fun getSaveLocation(): File\n}"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/UpdateManager.kt",
    "content": "package com.abdownloadmanager\n\nimport com.abdownloadmanager.updateapplier.UpdateApplier\nimport com.abdownloadmanager.updatechecker.UpdateChecker\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport ir.amirab.util.AppVersionTracker\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nsealed interface UpdateCheckStatus {\n    data object IDLE : UpdateCheckStatus\n    data object NoUpdate : UpdateCheckStatus\n    data object NewUpdate : UpdateCheckStatus\n    data class Error(val e: Throwable) : UpdateCheckStatus\n    data object Checking : UpdateCheckStatus\n}\n\nclass UpdateManager(\n    private val updateChecker: UpdateChecker,\n    private val updateApplier: UpdateApplier,\n    private val appVersionTracker: AppVersionTracker,\n) {\n    private var _newVersionData: MutableStateFlow<UpdateInfo?> = MutableStateFlow(null)\n    val newVersionData = _newVersionData.asStateFlow()\n    private val _updateCheckStatus: MutableStateFlow<UpdateCheckStatus> = MutableStateFlow(UpdateCheckStatus.IDLE)\n    val updateCheckStatus = _updateCheckStatus.asStateFlow()\n    suspend fun cleanDownloadedFiles() {\n        runCatching {\n            updateApplier.cleanup()\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n\n    fun isUpdateSupported(): Boolean {\n        return updateApplier.updateSupported()\n    }\n\n    suspend fun checkForUpdate(): UpdateInfo? {\n        val newUpdateCheck = try {\n            _updateCheckStatus.update { UpdateCheckStatus.Checking }\n            val checkedData = updateChecker.check()\n            _updateCheckStatus.value = if (checkedData == null) {\n                UpdateCheckStatus.NoUpdate\n            } else {\n                UpdateCheckStatus.NewUpdate\n            }\n            checkedData\n        } catch (e: Exception) {\n            _updateCheckStatus.update { UpdateCheckStatus.Error(e) }\n            null\n        }\n        _newVersionData.update { newUpdateCheck }\n        return newUpdateCheck\n    }\n\n    suspend fun update() {\n        _newVersionData.value?.let {\n            if (updateApplier.updateSupported()) {\n                updateApplier.applyUpdate(it)\n            }\n        }\n    }\n\n    // TODO add onAfter update installed\n    // ...\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/github/githubapi.kt",
    "content": "package com.abdownloadmanager.github\n\nimport ir.amirab.util.await\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\n\n@Serializable\ndata class Asset(\n    @SerialName(\"name\")\n    val name: String,\n    @SerialName(\"browser_download_url\")\n    val downloadLink: String,\n)\n\n@Serializable\ndata class Release(\n    @SerialName(\"tag_name\")\n    val version: String,\n    @SerialName(\"body\")\n    val body: String? = null,\n    @SerialName(\"assets\")\n    val assets: List<Asset>,\n)\n\nclass GithubApi(\n    private val owner: String,\n    private val repo: String,\n    private val client: OkHttpClient,\n) {\n    val json = Json {\n        ignoreUnknownKeys = true\n    }\n\n    suspend fun getLatestReleases(): Release {\n        val response = client.newCall(\n            Request.Builder()\n                .url(\"https://api.github.com/repos/${owner}/${repo}/releases/latest\")\n                .build()\n        ).await()\n        response.use {\n            if (!response.isSuccessful) {\n                error(response.message)\n            }\n            val release = json.decodeFromString<Release>(\n                response.body!!.string()\n            )\n            return release\n        }\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updateapplier/BaseUpdateApplier.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport com.abdownloadmanager.updatechecker.UpdateSource\n\nabstract class BaseUpdateApplier : UpdateApplier {\n    abstract override fun updateSupported(): Boolean\n\n    abstract fun getUpdatePreparer(): UpdatePreparer\n    override suspend fun applyUpdate(\n        updateInfo: UpdateInfo,\n    ) {\n        if (!updateSupported()) {\n            return\n        }\n        validateAppStateOnApplyUpdate()\n        //it is only check for same instance\n        // if I faced to multiple update (when user press \"update\" many times)\n        // I have to cancel this suspension job and create a new instance instead\n        if (preparing) {\n            return\n        }\n        preparing = true\n\n        val downloadSource = getBestDownloadSource(updateInfo)\n        val updatePreparer = getUpdatePreparer()\n        val preparedUpdate = try {\n            updatePreparer.prepareUpdate(downloadSource)\n        } catch (e: Exception) {\n            preparing = false\n            throw e\n        }\n        if (!preparedUpdate.isValid()) {\n            preparing = false\n            return\n        }\n        val updateInstaller = getUpdateInstaller(preparedUpdate)\n//        updateDownloader.removeUpdate(updateInfo)\n        try {\n            updateInstaller.installUpdate()\n        } catch (e: Exception) {\n            throw RuntimeException(\n                buildString {\n                    appendLine(\"can't start installation\")\n                    e.localizedMessage?.let(this::append)\n                },\n                e,\n            )\n        }\n    }\n\n    protected var preparing: Boolean = false\n\n    protected fun extension(name: String): String {\n        return name.substringAfterLast('.', \"\")\n    }\n\n    override suspend fun cleanup() {\n        getUpdatePreparer().disposeAllUpdates()\n    }\n\n    abstract fun getBestDownloadSource(updateInfo: UpdateInfo): UpdateSource\n    abstract fun getUpdateInstaller(preparedUpdate: UpdatePreparer.PreparedUpdate): UpdateInstaller\n    open fun validateAppStateOnApplyUpdate() {}\n\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updateapplier/UpdateApplier.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport com.abdownloadmanager.updatechecker.UpdateInfo\n\ninterface UpdateApplier {\n    fun updateSupported(): Boolean\n    suspend fun applyUpdate(updateInfo: UpdateInfo)\n    suspend fun cleanup()\n}"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updateapplier/UpdateDownloader.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport com.abdownloadmanager.updatechecker.UpdateSource\nimport java.io.File\n\ninterface UpdatePreparer {\n    interface PreparedUpdate {\n        fun isValid(): Boolean\n    }\n\n    suspend fun prepareUpdate(source: UpdateSource): PreparedUpdate\n    suspend fun disposeUpdate(updateSource: UpdateSource)\n    suspend fun disposeAllUpdates()\n    fun accept(updateSource: UpdateSource): Boolean\n}\n\nabstract class UpdateDownloader : UpdatePreparer {\n    data class PreparedUpdateFile(\n        val file: File\n    ) : UpdatePreparer.PreparedUpdate {\n        override fun isValid(): Boolean {\n            return file.exists()\n        }\n\n    }\n\n    override fun accept(updateSource: UpdateSource): Boolean {\n        return updateSource is UpdateSource.DirectDownloadLink\n    }\n\n    override suspend fun prepareUpdate(source: UpdateSource): UpdatePreparer.PreparedUpdate {\n        return PreparedUpdateFile(\n            downloadUpdateFile(source as UpdateSource.DirectDownloadLink)\n        )\n    }\n\n    override suspend fun disposeUpdate(updateSource: UpdateSource) {\n        removeUpdateFiles(updateSource as UpdateSource.DirectDownloadLink)\n    }\n\n    override suspend fun disposeAllUpdates() {\n        return removeAllUpdateFiles()\n    }\n\n\n    abstract suspend fun downloadUpdateFile(updateDirectDownloadLink: UpdateSource.DirectDownloadLink): File\n    abstract suspend fun removeUpdateFiles(updateDirectDownloadLink: UpdateSource.DirectDownloadLink)\n    abstract suspend fun removeAllUpdateFiles()\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updateapplier/UpdateInstaller.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\ninterface UpdateInstaller {\n    fun installUpdate()\n}\n\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updatechecker/DummyUpdateChecker.kt",
    "content": "package com.abdownloadmanager.updatechecker\n\nimport com.abdownloadmanager.InstallableArch\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\nimport kotlinx.coroutines.delay\n\nclass DummyUpdateChecker(currentVersion: Version) : UpdateChecker(currentVersion) {\n    override suspend fun getMyPlatformLatestVersion(): UpdateInfo {\n        val newVersion = currentVersion.copy(\n            major = currentVersion.minor + 1,\n            preRelease = null,\n            buildMetadata = null,\n        )\n        delay(1000)\n//        error(\"Something wrong\")\n        return UpdateInfo(\n            version = newVersion,\n            platform = Platform.getCurrentPlatform(),\n            arch = Arch.getCurrentArch(),\n            updateSource = listOf(\n                UpdateSource.DirectDownloadLink(\n                    link = \"http://127.0.0.1:8080/ABDownloadManager_1.4.4_windows_x64.zip\",\n                    name = \"ABDownloadManager_1.4.4_windows_x64.zip\",\n                    hash = \"md5:0123456789abcdef\",\n                    installableArch = InstallableArch.fromString(\"x64\")\n                )\n            ),\n            changeLog = \"\"\"\n                1. there is an improve on download engine.\n                2. fix known bugs.\n            \"\"\".trimIndent()\n        )\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updatechecker/GithubUpdateChecker.kt",
    "content": "package com.abdownloadmanager.updatechecker\n\nimport com.abdownloadmanager.github.GithubApi\nimport com.abdownloadmanager.ArtifactUtil\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\n\nclass GithubUpdateChecker(\n    currentVersion: Version,\n    private val githubApi: GithubApi,\n) : UpdateChecker(currentVersion) {\n    override suspend fun getMyPlatformLatestVersion(): UpdateInfo {\n        return getLatestVersionsForThisDevice()\n    }\n\n    private suspend fun getLatestVersionsForThisDevice(): UpdateInfo {\n        val release = githubApi.getLatestReleases()\n        val currentPlatform = Platform.getCurrentPlatform()\n        val currentArch = Arch.getCurrentArch()\n        val updateSources = mutableListOf<UpdateSource>()\n        var foundVersion: Version? = null\n        var initializedVersionFromAssetNames = false\n        for (asset in release.assets) {\n            val v = ArtifactUtil.getArtifactInfo(asset.name) ?: continue\n            if (v.platform != currentPlatform) continue\n            // universal builds should be installed on any arch\n            if (!v.arch.isCompatible(currentArch)) continue\n            if (!initializedVersionFromAssetNames) {\n                foundVersion = v.version\n                initializedVersionFromAssetNames = true\n            }\n            val isHashFile = asset.name.endsWith(\".md5\")\n            if (isHashFile) {\n                // nothing for now!\n            } else {\n                updateSources.add(\n                    UpdateSource.DirectDownloadLink(\n                        link = asset.downloadLink,\n                        name = asset.name,\n                        hash = null,\n                        installableArch = v.arch,\n                    )\n                )\n            }\n        }\n        return UpdateInfo(\n            version = foundVersion\n                ?: Version.parse(release.version.substring(\"v\".length)),\n            platform = currentPlatform,\n            arch = currentArch,\n            changeLog = release.body ?: \"\",\n            updateSource = updateSources\n        )\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updatechecker/UpdateChecker.kt",
    "content": "package com.abdownloadmanager.updatechecker\n\nimport io.github.z4kn4fein.semver.Version\n\n\nabstract class UpdateChecker(\n    protected val currentVersion: Version,\n) {\n    abstract suspend fun getMyPlatformLatestVersion(): UpdateInfo\n    suspend fun check(): UpdateInfo? {\n        val latest = getMyPlatformLatestVersion()\n        require(latest.updateSource.isNotEmpty()) { \"There is no release for this platform\" }\n        return latest.takeIf {\n            it.version > currentVersion\n        }\n    }\n}"
  },
  {
    "path": "shared/updater/src/commonMain/kotlin/com/abdownloadmanager/updatechecker/UpdateInfo.kt",
    "content": "package com.abdownloadmanager.updatechecker\n\nimport com.abdownloadmanager.InstallableArch\nimport io.github.z4kn4fein.semver.Version\nimport ir.amirab.util.platform.Arch\nimport ir.amirab.util.platform.Platform\n\ndata class UpdateInfo(\n    val version: Version,\n    val platform: Platform,\n    val arch: Arch,\n    val updateSource: List<UpdateSource>,\n    val changeLog: String,\n)\n\nsealed interface UpdateSource {\n    data class DirectDownloadLink(\n        val link: String,\n        val name: String,\n        val hash: String?,\n        val installableArch: InstallableArch?,\n    ) : UpdateSource\n}\n"
  },
  {
    "path": "shared/updater/src/desktopMain/kotlin/com/abdownloadmanager/updateapplier/DesktopDirectLinkUpdateApplier.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport com.abdownloadmanager.updatechecker.UpdateInfo\nimport com.abdownloadmanager.updatechecker.UpdateSource\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.isMac\nimport java.io.File\n\nclass DesktopDirectLinkUpdateApplier(\n    private val installationFolder: String?,\n    private val appName: String,\n    private val updateFolder: String,\n    private val logDir: String,\n    private val updatePreparer: UpdateDownloader,\n) : BaseUpdateApplier() {\n    override fun getUpdatePreparer(): UpdatePreparer {\n        return updatePreparer\n    }\n    override fun updateSupported(): Boolean {\n        val installationFolder = installationFolder ?: return false\n        return File(installationFolder).canWrite()\n    }\n\n    override fun validateAppStateOnApplyUpdate() {\n        requireNotNull(installationFolder) {\n            \"update applier can only apply update if installation folder is not null\"\n        }\n    }\n\n    override fun getBestDownloadSource(updateInfo: UpdateInfo): UpdateSource {\n        val downloadableSources =\n            updateInfo.updateSource.filterIsInstance<UpdateSource.DirectDownloadLink>()\n        var downloadSource = downloadableSources.find {\n            isArchiveFile(it.name)\n        }\n        if (Platform.getCurrentPlatform() == Platform.Desktop.Windows) {\n            val exeDirectDownloadLink = downloadableSources.find {\n                isExeFile(it.name)\n            }\n            if (isAppInstalledWithNSIS() && exeDirectDownloadLink != null) {\n                downloadSource = exeDirectDownloadLink\n            }\n        }\n        return requireNotNull(downloadSource) {\n            \"Can't find proper download link for your platform! Please update it manually\"\n        }\n    }\n\n    override fun getUpdateInstaller(preparedUpdate: UpdatePreparer.PreparedUpdate): UpdateInstaller {\n        requireNotNull(preparedUpdate is UpdateDownloader.PreparedUpdateFile)\n        val downloadedFile = (preparedUpdate as UpdateDownloader.PreparedUpdateFile).file\n        return when {\n            isArchiveFile(downloadedFile.name) -> {\n                val appFolderInArchive = when {\n                    Platform.isMac() -> \"$appName.app\"\n                    else -> appName\n                }\n                UpdateInstallerFromArchiveFile(\n                    archiveFile = downloadedFile,\n                    installationFolder = installationFolder!!, // validated\n                    appFolderInArchive = appFolderInArchive,\n                    folderToExtractUpdate = File(updateFolder).resolve(\"extracted\"),\n                    logDir = logDir,\n                )\n            }\n\n            isExeFile(downloadedFile.name) -> {\n                UpdateInstallerByWindowsExecutable(downloadedFile)\n            }\n\n            else -> {\n                // should not happen btw\n                error(\"can't install ${extension(downloadedFile.name)} format automatically! please update it manually!\")\n            }\n        }\n    }\n\n    private fun isAppInstalledWithNSIS(): Boolean {\n        return File(installationFolder, \"uninstall.exe\").exists()\n    }\n\n\n    private fun isArchiveFile(name: String): Boolean {\n        return name.endsWith(\".tar.gz\") || name.endsWith(\".zip\")\n    }\n\n    private fun isExeFile(name: String): Boolean {\n        return name.endsWith(\".exe\")\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/desktopMain/kotlin/com/abdownloadmanager/updateapplier/UpdateInstallerByWindowsExecutable.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport java.io.File\n\nclass UpdateInstallerByWindowsExecutable(\n    private val executable: File,\n) : UpdateInstaller {\n    override fun installUpdate() {\n        val file = executable.absolutePath\n        ProcessBuilder()\n            .command(\"cmd\", \"/c\", file, \"/S\")\n            .start()\n    }\n}"
  },
  {
    "path": "shared/updater/src/desktopMain/kotlin/com/abdownloadmanager/updateapplier/UpdateInstallerFromArchiveFile.kt",
    "content": "package com.abdownloadmanager.updateapplier\n\nimport ir.amirab.util.platform.Platform\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\nimport okio.buffer\nimport okio.use\nimport java.io.File\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipInputStream\n\n/**\n * the duty of the script is\n * it accepts [folderToExtractUpdate], [installationFolder]\n * 1. stop the app\n * 2. remove the installed app files\n * 3. copy [folderToExtractUpdate] into [installationFolder]\n * 4. remove [folderToExtractUpdate]\n * 5. start the app again\n */\nclass UpdateInstallerFromArchiveFile(\n    private val archiveFile: File,\n    private val installationFolder: String,\n    private val folderToExtractUpdate: File,\n    private val appFolderInArchive: String,\n    private val logDir: String,\n) : UpdateInstaller {\n    private fun getScriptPath(logFile: String): String {\n        val platform = Platform.getCurrentPlatform()\n        val updaterPath = \"com/abdownloadmanager/updater\"\n        val scriptForPlatform = when (platform) {\n            Platform.Desktop.Linux -> \"$updaterPath/updater_linux.sh\"\n            Platform.Desktop.MacOS -> \"$updaterPath/updater_macos.sh\"\n            Platform.Desktop.Windows -> \"$updaterPath/updater_windows.bat\"\n\n            else -> error(\"script for this platform not found\")\n        }.toPath()\n        extractTo(archiveFile, folderToExtractUpdate)\n        val updateFolder = folderToExtractUpdate.resolve(appFolderInArchive)\n        require(updateFolder.exists()) {\n            \"Can't find required files for this update please update it manually\"\n        }\n        val scriptExtension = scriptForPlatform.toString().substringAfterLast('.', \"\")\n        val scriptContent = FileSystem.RESOURCES.source(scriptForPlatform).buffer().use {\n            it.readUtf8()\n        }\n        val scriptPathInTempFolder = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(\n            \"abdm-updater.$scriptExtension\"\n        )\n        scriptPathInTempFolder.toFile().writeText(scriptContent)\n        val scriptContentFile = scriptPathInTempFolder.toString()\n        val commandToRun = when (platform) {\n            Platform.Desktop.Linux -> execInBash(\n                scriptPath = scriptContentFile,\n                updateFolder = updateFolder.path,\n                installationFolder = installationFolder,\n                logFile = logFile,\n            )\n\n            Platform.Desktop.MacOS -> execInBash(\n                scriptPath = scriptContentFile,\n                updateFolder = updateFolder.path,\n                installationFolder = installationFolder,\n                logFile = logFile,\n            )\n\n            Platform.Desktop.Windows -> execInCMD(\n                scriptPath = scriptContentFile,\n                updateFolder = updateFolder.path,\n                installationFolder = installationFolder,\n                logFile = logFile,\n            )\n\n            else -> error(\"platform $platform not supported\")\n        }\n        val scriptToRun =\n            FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(\"abdm-updater.run.$scriptExtension\")\n        scriptToRun.toFile().writeText(commandToRun)\n        return scriptToRun.toString()\n    }\n\n    private fun executeScript() {\n        val logFile = File(logDir, \"update_log.txt\")\n            .apply {\n                parentFile.mkdirs()\n            }.path\n        val scriptPath = getScriptPath(logFile)\n\n\n        val command = when (val p = Platform.getCurrentPlatform()) {\n            Platform.Desktop.Linux -> arrayOf(\"bash\", scriptPath)\n            Platform.Desktop.MacOS -> arrayOf(\"bash\", scriptPath)\n            Platform.Desktop.Windows -> arrayOf(\"cmd\", \"/c\", scriptPath)\n            else -> error(\"platform: $p not supported for updating by script\")\n        }\n//        println(\"execute script $command\")\n        ProcessBuilder()\n            .command(*command)\n            .apply {\n                // in linux if I don't remove it the program won't restart\n                environment().remove(\"_JPACKAGE_LAUNCHER\")\n            }\n            .start()\n    }\n\n    private fun execInCMD(\n        scriptPath: String,\n        updateFolder: String,\n        installationFolder: String,\n        logFile: String,\n    ): String {\n        return \"\"\"\n            cmd /c \"\"$scriptPath\" \"$updateFolder\" \"$installationFolder\" > \"$logFile\" 2>&1\"\n        \"\"\".trimIndent()\n    }\n\n    private fun execInBash(\n        scriptPath: String,\n        updateFolder: String,\n        installationFolder: String,\n        logFile: String,\n    ): String {\n        return \"\"\"\n            bash \"$scriptPath\" \"$updateFolder\" \"$installationFolder\" > \"$logFile\" 2>&1 &\n        \"\"\".trimIndent()\n    }\n\n    override fun installUpdate() {\n        executeScript()\n    }\n}\n\nprivate fun extractTo(archiveFile: File, destinationFolder: File) {\n    val name = archiveFile.name\n    require(!destinationFolder.isFile) {\n        \"destination folder is a file!\"\n    }\n    destinationFolder.mkdirs()\n    require(destinationFolder.isDirectory) {\n        \"destination folder is not created!\"\n    }\n    when {\n        name.endsWith(\".zip\") -> extractZip(archiveFile, destinationFolder)\n        name.endsWith(\"tar.gz\") -> extractTarGzUsingTar(archiveFile, destinationFolder)\n        else -> error(\"archive file not detected for this file name: $name\")\n    }\n}\n\nprivate fun extractZip(zipFile: File, outputDirPath: File) {\n    ZipInputStream(zipFile.inputStream()).use { zis ->\n        var entry: ZipEntry? = null\n        while (true) {\n            entry = zis.nextEntry\n            if (entry == null) break\n            val outputFile = outputDirPath.resolve(entry.name)\n            if (entry.isDirectory) {\n                outputFile.mkdirs()\n            } else {\n                outputFile.parentFile.mkdirs()\n                outputFile.outputStream().use { fileOutputStream ->\n                    zis.copyTo(fileOutputStream)\n                }\n            }\n        }\n    }\n}\n\nprivate fun extractTarGzUsingTar(tarGzFilePath: File, outputDirPath: File) {\n    val tarCommand = listOf(\"tar\", \"-xzvf\", tarGzFilePath.path, \"-C\", outputDirPath.path)\n    try {\n        val process = ProcessBuilder(tarCommand)\n            .start()\n\n        val exitCode = process.waitFor()\n        if (exitCode == 0) {\n            println(\"Extraction completed successfully.\")\n        } else {\n            println(\"Error during extraction. Exit code: $exitCode\")\n        }\n    } catch (e: Exception) {\n        println(\"Failed to execute tar command: ${e.message}\")\n    }\n}\n"
  },
  {
    "path": "shared/updater/src/desktopMain/resources/com/abdownloadmanager/updater/updater_linux.sh",
    "content": "APP_NAME=\"ABDownloadManager\"\nawaitTermination(){\n  local processName=\"${1:?}\"\n  local count=0\n  while true; do\n    local pids=$(pidof \"$processName\")\n    if [ -z \"$pids\" ]; then\n      break\n    fi\n    if [ $count -eq 10 ]; then\n      echo \"timeout waiting for $processName to terminate\"\n      break\n    fi\n    echo \"waiting for $processName to terminate\"\n    sleep 1\n  done\n}\nstopApp(){\n  echo \"stopping the app\"\n  local pids=$(pidof \"$APP_NAME\")\n  if [ -z \"$pids\" ]; then\n    echo \"no process found with name $APP_NAME\"\n    return\n  fi\n  kill -9 \"$pids\"\n  awaitTermination \"$APP_NAME\"\n  if [ $? -ne 0 ]; then\n    echo \"failed to stop $APP_NAME\"\n    return 1\n  fi\n  echo \"process $APP_NAME stopped\"\n}\nremoveCurrentInstallation(){\n  local installationFolder=\"${1:?}\"\n  filesToRemove=(\n    \"bin\"\n    \"lib\"\n  )\n  echo \"removing current installation\"\n  for filesToRemove in \"${filesToRemove[@]}\" ; do\n      echo \"executing rm -rf \\\"$installationFolder/$filesToRemove\\\"\"\n      rm -rf \"$installationFolder/$filesToRemove\"\n  done\n}\ncopyUpdateToInstallationFolder(){\n    local updateFile=\"$1\"\n    local installationFolder=\"${2:?\"installationFolder not passed\"}\"\n    echo \"copying update files to installation folder\"\n    echo \"executing: cp -a \\\"$updateFile/.\\\" $installationFolder\"\n    cp -a \"$updateFile/.\" \"$installationFolder\"\n}\n\nremoveUpdateFiles(){\n    local updateFile=\"$1\"\n    echo \"removing update folder\"\n    echo \"executing: rm -rf \\\"$updateFile\\\"\"\n    rm -rf \"$updateFile\"\n}\nexecutablePath(){\n  local installationFolder=\"${1:?}\"\n  echo \"$installationFolder/bin/$APP_NAME\"\n}\nexecuteProgram(){\n  local installationFolder=$1\n  local path=$(executablePath \"$installationFolder\")\n  echo \"starting $APP_NAME...\"\n  echo \"executing: \\\"$path\\\"\"\n  \"$path\"\n}\nmain(){\n  local updateFile=\"$1\"\n  local installationFolder=\"$2\"\n\n  stopApp \"$installationFolder\"\n  if [ $? -ne 0 ]; then\n      echo \"returning back to program\"\n      executeProgram \"$installationFolder\"\n      exit 1\n  fi\n  removeCurrentInstallation \"$installationFolder\"\n  copyUpdateToInstallationFolder \"$updateFile\" \"$installationFolder\"\n  removeUpdateFiles \"$updateFile\"\n  executeProgram \"$installationFolder\"\n}\n\nmain \"$@\""
  },
  {
    "path": "shared/updater/src/desktopMain/resources/com/abdownloadmanager/updater/updater_macos.sh",
    "content": "APP_NAME=\"ABDownloadManager\"\n\nawaitTermination(){\n  local processName=\"${1:?}\"\n  local count=0\n  while true; do\n    local pids=$(pgrep -x \"$processName\")\n    if [ -z \"$pids\" ]; then\n      break\n    fi\n    if [ $count -eq 10 ]; then\n      echo \"timeout waiting for $processName to terminate\"\n      break\n    fi\n    echo \"waiting for $processName to terminate\"\n    sleep 1\n    count=$((count + 1))\n  done\n}\n\nstopApp(){\n  echo \"stopping the app\"\n  local pids=$(pgrep -x \"$APP_NAME\")\n  if [ -z \"$pids\" ]; then\n    echo \"no process found with name $APP_NAME\"\n    return\n  fi\n  kill -9 $pids\n  awaitTermination \"$APP_NAME\"\n  if [ $? -ne 0 ]; then\n    echo \"failed to stop $APP_NAME\"\n    return 1\n  fi\n  echo \"process $APP_NAME stopped\"\n}\n\nremoveCurrentInstallation(){\n  local installationFolder=\"${1:?}\"\n  echo \"removing current installation\"\n  echo \"executing rm -rf \\\"$installationFolder\\\"\"\n  rm -rf \"$installationFolder\"\n}\n\ncopyUpdateToInstallationFolder(){\n  local updateFile=\"$1\"\n  local installationFolder=\"${2:?}\"\n  echo \"copying update files to installation folder\"\n  echo \"executing: cp -Rp \\\"$updateFile\\\" \\\"$installationFolder\\\"\"\n  cp -Rp \"$updateFile\" \"$installationFolder\"\n}\n\nremoveUpdateFiles(){\n  local updateFile=\"$1\"\n  echo \"removing update folder\"\n  echo \"executing: rm -rf \\\"$updateFile\\\"\"\n  rm -rf \"$updateFile\"\n}\n\nexecuteProgram(){\n  local installationFolder=$1\n  echo \"starting $APP_NAME...\"\n  echo \"executing: open \\\"$installationFolder\\\"\"\n  open \"$installationFolder\"\n}\n\nmain(){\n  local updateFile=\"$1\"\n  local installationFolder=\"$2\"\n\n  stopApp \"$APP_NAME\"\n  if [ $? -ne 0 ]; then\n    echo \"returning back to program\"\n    executeProgram \"$installationFolder\"\n    exit 1\n  fi\n\n  removeCurrentInstallation \"$installationFolder\"\n  copyUpdateToInstallationFolder \"$updateFile\" \"$installationFolder\"\n  removeUpdateFiles \"$updateFile\"\n  executeProgram \"$installationFolder\"\n}\n\nmain \"$@\""
  },
  {
    "path": "shared/updater/src/desktopMain/resources/com/abdownloadmanager/updater/updater_windows.bat",
    "content": "@echo off\r\n\r\nset APP_NAME=ABDownloadManager\r\ncall :main \"%1\" \"%2\"\r\ngoto :eof\r\n\r\n:stopApp\r\necho execute: taskkill /IM %APP_NAME%.exe /F\r\ntaskkill /IM %APP_NAME%.exe /F\r\ncall :wait_for_termination\r\necho %APP_NAME% is terminated\r\ngoto :eof\r\n\r\n:wait_for_termination\r\necho checking for termination of %APP_NAME%\r\ntasklist /FI \"IMAGENAME eq %APP_NAME%.exe\" | find /I \"%APP_NAME%.exe\" >nul 2>&1\r\nif errorlevel 1 (\r\n    goto :eof\r\n) else (\r\n    ping 127.0.0.1 -n 2 >nul 2>&1\r\n    goto wait_for_termination\r\n)\r\n\r\n\r\n:removeCurrentInstallation\r\nsetlocal\r\n    set installationFolder=%~1\r\n    set filesToRemove=(\"app\" \"runtime\" \"ABDownloadManager.exe\" \"ABDownloadManager.ico\")\r\n    for %%f in %filesToRemove% do (\r\n        if exist %installationFolder%\\%%f (\r\n            if exist %installationFolder%\\%%f\\* (\r\n                echo executing rmdir /S /Q %installationFolder%\\%%f\r\n                rmdir /S /Q \"%installationFolder%\\%%f\"\r\n            ) else (\r\n                echo executing del /F /Q %installationFolder%\\%%f\r\n                del /F /Q \"%installationFolder%\\%%f\"\r\n            )\r\n        )\r\n    )\r\n    endlocal\r\ngoto :eof\r\n\r\n:copyUpdateToInstallationFolder\r\n    setlocal\r\n    set updateFile=%1\r\n    set installationFolder=%2\r\n    echo executing: xcopy /E /I /Y %updateFile% %installationFolder%\r\n    xcopy /E /I /Y %updateFile% %installationFolder%\r\n    endlocal\r\ngoto :eof\r\n\r\n:removeUpdateFolder\r\n    setlocal\r\n    set updateFolder=%1\r\n    echo executing rmdir /S /Q \"%updateFolder%\"\r\n    rmdir /S /Q \"%updateFolder%\"\r\n    endlocal\r\ngoto :eof\r\n\r\n:executeProgram\r\n    setlocal\r\n    set installationFolder=%~1\r\n    set code=%2\r\n    set message=%3\r\n    echo executing %installationFolder%\\%APP_NAME%.exe\r\n    start \"\" %installationFolder%\\%APP_NAME%.exe\r\n    endlocal\r\ngoto :eof\r\n\r\n:main\r\n    setlocal\r\n    set updateFile=%1\r\n    set installationFolder=%2\r\n    call :stopApp\r\n    call :removeCurrentInstallation %installationFolder%\r\n    call :copyUpdateToInstallationFolder %updateFile% %installationFolder%\r\n    call :removeUpdateFolder %updateFile%\r\n    call :executeProgram %installationFolder%\r\n    endlocal\r\ngoto :eof\r\n\r\n\r\n"
  },
  {
    "path": "shared/utils/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id(MyPlugins.kotlinMultiplatform)\n    id(Plugins.Kotlin.serialization)\n    id(Plugins.Android.library)\n}\nkotlin {\n    jvm(\"desktop\")\n    androidTarget(\"android\") {\n        compilerOptions {\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n    sourceSets {\n        commonMain.dependencies {\n            implementation(libs.kotlin.serialization.json)\n            api(libs.okio.okio)\n            api(libs.okhttp.okhttp)\n            api(libs.kotlin.coroutines.core)\n            api(libs.kotlin.datetime)\n            api(libs.semver)\n            api(libs.arrow.optics)\n            api(\"ir.amirab.util:platform:1\")\n        }\n        val desktopMain by getting\n        desktopMain.dependencies {\n            api(libs.jna.platform)\n        }\n        androidMain.dependencies {\n            implementation(libs.koin.core)\n            implementation(libs.androidx.core.ktx)\n        }\n    }\n}\nandroid {\n    compileSdk = 36\n    namespace = \"ir.amirab.util\"\n    defaultConfig {\n        minSdk = 26\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/androidMain/kotlin/ir/amirab/util/openUrl.android.kt",
    "content": "package ir.amirab.util\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\n\nactual object URLOpener : KoinComponent {\n    val context: Context by inject()\n    actual fun openUrl(url: String) {\n        val intent = Intent(\n            Intent.ACTION_VIEW,\n        )\n        intent.data = Uri.parse(url)\n        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n        runCatching {\n            context.startActivity(intent)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/androidMain/kotlin/ir/amirab/util/osfileutil/AndroidFileUtil.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.provider.Settings\nimport android.util.Log\nimport android.webkit.MimeTypeMap\nimport android.widget.Toast\nimport androidx.core.content.FileProvider\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport java.io.File\n\nclass AndroidFileUtil : FileUtilsBase(), KoinComponent {\n    val context: Context by inject()\n    override fun openFileInternal(file: File): Boolean {\n        val mimeType = MimeTypeMap\n            .getSingleton()\n            .getMimeTypeFromExtension(file.extension.lowercase())\n            ?: \"*/*\"\n\n\n        val uri = FileProvider.getUriForFile(context, \"${context.packageName}.provider\", file)\n        val intent = Intent(Intent.ACTION_VIEW).apply {\n            setDataAndType(uri, mimeType)\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        }\n        return runCatching {\n            context.startActivity(intent)\n            true\n        }\n            .onFailure {\n                it.printStackTrace()\n                (it.localizedMessage ?: it::class.qualifiedName)?.let { message ->\n                    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()\n                }\n            }\n            .getOrElse { false }\n    }\n\n    override fun openFolderOfFileInternal(file: File): Boolean {\n        return file.parentFile?.let {\n            openFolderInternal(it)\n        } ?: false\n    }\n\n    override fun openFolderInternal(folder: File): Boolean {\n        throw UnsupportedOperationException(\n            \"Android doesn't support open folder\"\n        )\n    }\n\n    override fun isRemovableStorage(path: String): Boolean {\n        return false\n    }\n\n}\n"
  },
  {
    "path": "shared/utils/src/androidMain/kotlin/ir/amirab/util/osfileutil/FileUtils.android.kt",
    "content": "package ir.amirab.util.osfileutil\n\nactual fun getPlatformFileUtil(): FileUtils {\n    return AndroidFileUtil()\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/SelectionUtil.kt",
    "content": "package ir.amirab\n\nobject SelectionUtil {\n    inline fun <T, ID> invertSelection(\n        selectionList: List<ID>,\n        all: List<T>,\n        getId: (T) -> ID\n    ): List<ID> {\n        return all\n            .filterNot { getId(it) in selectionList }\n            .map { getId(it) }\n    }\n\n    inline fun <T, ID> toggleSelectInside(\n        selectionList: List<ID>,\n        fullSortedList: List<T>,\n        getId: (T) -> ID,\n    ): List<ID>? {\n        val selectionSet = selectionList.toSet()\n        val startIndex = fullSortedList.indexOfFirst {\n            getId(it) in selectionSet\n        }\n        val endIndex = fullSortedList.indexOfLast {\n            getId(it) in selectionSet\n        }\n        if (startIndex == -1 || endIndex == -1) {\n            return null\n        }\n        val startItem = getId(fullSortedList[startIndex])\n        val endItem = getId(fullSortedList[endIndex])\n        return if ((endIndex - startIndex + 1) == selectionSet.size) {\n            listOf(startItem, endItem)\n        } else {\n            selectInside(\n                sortedList = fullSortedList,\n                startItem = startItem,\n                endItem = endItem,\n                getID = getId,\n            )\n        }\n    }\n\n    // ONLY PASS SORTED LIST!\n    inline fun <Item, ID> getARangeOfItems(\n        sortedList: List<Item>,\n        id: (Item) -> ID,\n        fromItem: ID,\n        toItem: ID,\n    ): List<ID> {\n        return sortedList.map(id).dropWhile {\n            it != fromItem && it != toItem\n        }.dropLastWhile {\n            it != fromItem && it != toItem\n        }\n    }\n\n    inline fun <T, ID> selectInside(\n        sortedList: List<T>,\n        startItem: ID,\n        endItem: ID,\n        getID: (T) -> ID\n    ): List<ID> {\n        val ids: List<ID> = getARangeOfItems(\n            sortedList = sortedList,\n            id = getID,\n            fromItem = startItem,\n            toItem = endItem,\n        )\n        return ids\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/AppVersionTracker.kt",
    "content": "package ir.amirab.util\n\nimport io.github.z4kn4fein.semver.Version\n\nclass AppVersionTracker(\n    val previousVersion: () -> Version?,\n    val currentVersion: Version,\n) {\n    fun isNewInstall(): Boolean {\n        return previousVersion() == null\n    }\n\n    fun isUpgraded(): Boolean {\n        val previousVersion = previousVersion() ?: return false\n        return previousVersion < currentVersion\n    }\n\n    fun isDowngraded(): Boolean {\n        val previousVersion = previousVersion() ?: return false\n        return previousVersion > currentVersion\n    }\n\n    fun isNewOrUpdated() = isNewInstall() || isUpgraded()\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/CallAwait.kt",
    "content": "package ir.amirab.util\n\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport okhttp3.Call\nimport okhttp3.Callback\nimport okhttp3.Response\nimport java.io.IOException\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\nsuspend fun Call.await(): Response {\n    return suspendCancellableCoroutine { continuation ->\n        continuation.invokeOnCancellation {\n            try {\n                cancel()\n            } catch (_: Throwable) {\n            }\n        }\n        enqueue(object : Callback {\n            override fun onResponse(call: Call, response: Response) {\n                continuation.resume(response)\n            }\n\n            override fun onFailure(call: Call, e: IOException) {\n                // Don't bother with resuming the continuation if it is already cancelled.\n                if (continuation.isCancelled) return\n                continuation.resumeWithException(e)\n            }\n        })\n\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/CollectionUtils.kt",
    "content": "package ir.amirab.util\n\nfun <T> MutableList<T>.swap(\n    index: Int, toPosition: Int\n): MutableList<T> = apply {\n    val p = set(toPosition, this[index])\n    set(index, p)\n}\n\nfun <T> List<T>.swapped(index: Int, toPosition: Int): List<T> {\n    val l = toMutableList()\n    l.swap(index, toPosition)\n    return l.toList()\n}\n\nfun <T> Set<T>.swapped(a: T, b: T): Set<T> {\n    val l = toMutableList()\n    val indexA = indexOf(a)\n    val indexB = indexOf(b)\n    val tmp = l.set(indexB, l[indexA])\n    l.set(indexA, tmp)\n    return l.toList().toSet()\n}\n\nfun <T> List<T>.shifted(index: Int, delta: Int): List<T> {\n    val indices = indices\n    require(index in indices)\n    val newPosition = index + delta\n    require(newPosition in indices)\n    val l = toMutableList()\n    l.add(newPosition, l.removeAt(index))\n    return l.toList()\n}\nfun <T> MutableList<T>.shift(index: Int, delta: Int): List<T> {\n    val indices = indices\n    require(index in indices)\n    val newPosition = index + delta\n    require(newPosition in indices)\n    add(newPosition, removeAt(index))\n    return this\n}\n\nfun <T> MutableList<T>.shiftToLast(index: Int): List<T> {\n    return shift(index, lastIndex - index)\n}\n\nfun <T> MutableList<T>.shiftToFirst(index: Int): List<T> {\n    return shift(index, -index)\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/Exec.kt",
    "content": "package ir.amirab.util\n\nimport java.util.concurrent.TimeUnit\n\n/**\n * this helper function is here to execute a command and waits for the process to finish and return the result based on exit code\n * @param command the command\n * @param waitFor maximum time allowed process finish ( in milliseconds )\n * @return `true` when process exits with `0` exit code, `false` if the process fails with non-zero exit code or execution time exceeds the [waitFor]\n */\nfun execAndWait(\n    command: Array<String>,\n    waitFor: Long = 2_000,\n): Boolean {\n    return runCatching {\n        val p = Runtime.getRuntime().exec(command)\n        val exited = p.waitFor(waitFor, TimeUnit.MILLISECONDS)\n        if (exited) {\n            p.exitValue() == 0\n        } else {\n            false\n        }\n    }.getOrElse { false }\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/FileExtensions.kt",
    "content": "package ir.amirab.util\n\nimport okio.FileSystem\nimport okio.IOException\nimport okio.Path.Companion.toOkioPath\nimport java.io.File\n\n\nfun File.toUpUntil(\n    condition: (File) -> Boolean\n): File? {\n    var file: File? = this\n    while (true) {\n        if (file == null) {\n            return null\n        }\n        if (condition(file)) {\n            return file\n        }\n        file = file.parentFile\n    }\n}\n\nfun File.tryAtomicMove(destination: File) {\n    val target = destination.toOkioPath()\n    val source = toOkioPath()\n    try {\n        // this should replace existing target in java.nio file system\n        // however if on some target we have to use java.io we should delete the file first\n        FileSystem.SYSTEM.atomicMove(source, target)\n    } catch (e: IOException) {\n        if (!e.message.orEmpty().contains(\"atomic move\")) {\n            throw e\n        }\n        FileSystem.SYSTEM.delete(target, false)\n        FileSystem.SYSTEM.copy(source, target)\n        FileSystem.SYSTEM.delete(source)\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/FileNameValidator.kt",
    "content": "package ir.amirab.util\n\nimport java.io.File\n\nobject FileNameValidator {\n    fun isValidFileName(name: String): Boolean {\n        if (name.isEmpty()) return false\n        return runCatching {\n            File(name).canonicalFile\n        }.getOrNull()?.let {\n            it.name == name\n        } ?: false\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/FilenameDecoder.kt",
    "content": "package ir.amirab.util\n\nimport java.nio.charset.Charset\n\n/**\n * this is very similar to URLDecoder however it doesn't replace \"+\" with \" \"\n * RFC 5987\n */\nobject FilenameDecoder {\n    fun decode(\n        encoded: String,\n        charset: Charset = Charsets.UTF_8,\n    ): String {\n        var strIndex = 0\n        val stringBuilder = StringBuilder()\n        // we only initiate it when we visit %\n        var bytes: ByteArray? = null\n        while (strIndex < encoded.length) {\n            var ch = encoded[strIndex]\n            if (ch == '%') {\n                var byteIndex = 0\n                if (bytes == null) {\n                    // maximum required size\n                    bytes = ByteArray((encoded.length - strIndex) / 3)\n                }\n                while (true) {\n                    if ((strIndex + 2) >= encoded.length) {\n                        throw IllegalArgumentException(\"Incomplete percent encoding at position $strIndex\")\n                    }\n                    bytes[byteIndex++] = Integer.parseInt(\n                        encoded,\n                        // after % take two chars\n                        strIndex + 1,\n                        strIndex + 3,\n                        16\n                    ).toByte()\n                    strIndex += 3 // %ab (3 chars)\n                    if (strIndex < encoded.length) {\n                        ch = encoded[strIndex]\n                        if (ch == '%') {\n                            continue\n                        }\n                    }\n                    break\n                }\n                stringBuilder.append(\n                    String(bytes, 0, byteIndex, charset)\n                )\n            } else {\n                stringBuilder.append(ch)\n                strIndex++\n            }\n        }\n        val modified = bytes != null\n        return if (modified) {\n            stringBuilder.toString()\n        } else {\n            encoded\n        }\n    }\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/GuardedEntry.kt",
    "content": "package ir.amirab.util\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\ninterface BaseGuardedEntry {\n    suspend fun awaitDone()\n    fun isDone(): Boolean\n}\n\ninterface GuardedEntry : BaseGuardedEntry {\n    fun <T> action(block: () -> T): T?\n}\n\ninterface SuspendGuardedEntry : BaseGuardedEntry {\n    suspend fun <T> action(block: suspend () -> T): T?\n}\n\nprivate abstract class BaseGuardedEntryImpl : BaseGuardedEntry {\n    private val _isBooted = MutableStateFlow(false)\n    protected fun setIsDone() {\n        _isBooted.value = true\n    }\n\n    override fun isDone(): Boolean {\n        return _isBooted.value\n    }\n\n    override suspend fun awaitDone() {\n        if (isDone()) return\n        _isBooted.first { it }\n    }\n}\n\n\nprivate class GuardedActionImpl : BaseGuardedEntryImpl(), GuardedEntry {\n    private val mutex = Any()\n    override fun <T> action(block: () -> T): T? {\n        if (isDone()) {\n            return null\n        }\n        return synchronized(mutex) {\n            if (isDone()) {\n                return null\n            }\n            val result = block()\n            setIsDone()\n            result\n        }\n    }\n}\n\nprivate class SuspendGuardedActionImpl : BaseGuardedEntryImpl(), SuspendGuardedEntry {\n    private val mutex = Mutex()\n    override suspend fun <T> action(block: suspend () -> T): T? {\n        if (isDone()) {\n            return null\n        }\n        return mutex.withLock {\n            if (isDone()) {\n                return null\n            }\n            val result = block()\n            setIsDone()\n            result\n        }\n    }\n}\n\n/**\nprevent multiple threads call something. for example some object might require booting once. and calling boot again can lead to undefined behavior\n```kt\nval entry = guardedEntry()\nthread {\nentry.action { print(\"1\") }\n}\nthread {\nentry.action { print(\"2\") }\n}\n```\nonly one of these prints will be printed!\n */\n\nfun guardedEntry(): GuardedEntry = GuardedActionImpl()\nfun suspendGuardedEntry(): SuspendGuardedEntry = SuspendGuardedActionImpl()\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/HttpUrlUtils.kt",
    "content": "package ir.amirab.util\n\nimport okhttp3.HttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nobject HttpUrlUtils {\n    fun createURL(url: String): HttpUrl {\n        return url.toHttpUrl()\n    }\n\n    fun isValidUrl(link: String): Boolean {\n        return runCatching { createURL(link) }.isSuccess\n    }\n\n    fun extractNameFromLink(link: String): String? {\n        return runCatching {\n            createURL(link)\n        }.map { url ->\n            val foundName = url.pathSegments\n                .lastOrNull { it.isNotBlank() }\n                ?.let {\n                    kotlin.runCatching {\n                        FilenameDecoder.decode(it, Charsets.UTF_8)\n                    }.getOrNull()\n                }\n            if (foundName != null) {\n                return@map foundName\n            }\n            url.host.replace('.', '_')\n        }\n            .getOrNull()\n    }\n\n    fun getHost(url: String): String? {\n        return kotlin.runCatching {\n            createURL(url).host\n        }.getOrNull()\n    }\n\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/IfThen.kt",
    "content": "@file:OptIn(ExperimentalContracts::class)\n\npackage ir.amirab.util\n\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.ExperimentalExtendedContracts\nimport kotlin.contracts.InvocationKind\nimport kotlin.contracts.contract\n\n@OptIn(ExperimentalExtendedContracts::class)\ninline fun <Base, T : Base> T.ifThen(condition: Boolean, block: T.() -> Base): Base {\n    contract {\n        callsInPlace(block, InvocationKind.AT_MOST_ONCE)\n        condition holdsIn block\n    }\n    return if (condition) {\n        this.block()\n    } else {\n        this\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/NullCheck.kt",
    "content": "@file:OptIn(ExperimentalContracts::class)\n@file:Suppress(\"NOTHING_TO_INLINE\")\n\npackage ir.amirab.util\n\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.contract\n\ninline fun <T> isNull(value: T): Boolean {\n    contract {\n        returns(true) implies (value == null)\n        returns(false) implies (value != null)\n    }\n    return value == null\n}\n\ninline fun <T> isNotNull(value: T): Boolean {\n    contract {\n        returns(true) implies (value != null)\n        returns(false) implies (value == null)\n    }\n    return value != null\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/OkioUtils.kt",
    "content": "package ir.amirab.util\n\nimport okio.FileSystem\nimport okio.Path\nimport okio.Path.Companion.toPath\nimport java.nio.file.FileAlreadyExistsException\nimport java.nio.file.NotDirectoryException\nimport kotlin.io.path.createDirectories\nimport kotlin.io.path.isDirectory\n\nfun Path.writeText(\n    text: String,\n    fileSystem: FileSystem = FileSystem.SYSTEM\n) {\n    fileSystem.write(this, false) {\n        writeUtf8(text)\n    }\n}\n\nfun Path.readText(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n): String {\n    return fileSystem.read(this) {\n        readUtf8()\n    }\n}\n\nfun Path.exists(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n): Boolean {\n    return fileSystem.exists(this)\n}\n\nfun Path.isFile(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n): Boolean {\n    return fileSystem.metadataOrNull(this)?.isRegularFile == true\n}\n\nfun Path.isDirectory(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n): Boolean {\n    return fileSystem.metadataOrNull(this)?.isDirectory == true\n}\n\nfun Path.toAbsolute(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n): Path {\n    return ifThen(!isAbsolute) {\n        fileSystem.canonicalize(\"\".toPath()) / this\n    }\n}\n\nfun Path.listFiles(fileSystem: FileSystem = FileSystem.SYSTEM): List<Path> {\n    return fileSystem.list(this)\n}\nfun Path.listFilesOrNull(fileSystem: FileSystem = FileSystem.SYSTEM): List<Path>? {\n    return fileSystem.listOrNull(this)\n}\n\nfun Path.pathString(): String = toString()\n\nfun Path.createDirectories(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n) {\n    fileSystem.createDirectories(\n        dir = this,\n        mustCreate = false\n    )\n}\n\nfun Path.createParentDirectories(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n) {\n    parent\n        ?.takeIf { !it.isDirectory(fileSystem) }\n        ?.createDirectories(fileSystem)\n}\n\nfun Path.deleteIfExists(\n    fileSystem: FileSystem = FileSystem.SYSTEM\n) {\n    fileSystem.delete(this, false)\n}\n\nfun Path.startsWith(other: Path) = normalized().run {\n    other.normalized().let { normalizedOther ->\n        normalizedOther.segments.size <= segments.size &&\n                segments\n                    .slice(0 until normalizedOther.segments.size)\n                    .filterIndexed { index, s -> normalizedOther.segments[index] != s }\n                    .isEmpty()\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/PathValidator.kt",
    "content": "package ir.amirab.util\n\nimport ir.amirab.util.osfileutil.FileUtils\nimport java.io.File\n\nobject PathValidator {\n    fun canWriteToThisPath(path: String): Boolean {\n        return FileUtils.canWriteInThisFolder(path)\n    }\n\n    fun isValidPath(path: String): Boolean {\n        if (path.isEmpty()) return false\n        return runCatching {\n            File(path).canonicalFile\n            true\n        }.getOrElse { false }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/StringUtil.kt",
    "content": "package ir.amirab.util\n\nfun wildcardMatch(\n    pattern: String,\n    input: String,\n): Boolean {\n    return pattern\n        .split(\"*\")\n        .joinToString(\".*\") { Regex.escape(it) }\n        .toRegex(RegexOption.IGNORE_CASE)\n        .containsMatchIn(input)\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/ValueHolder.kt",
    "content": "package ir.amirab.util\n\nimport kotlin.reflect.KProperty\n\ndata class ValueHolder<T>(\n    var value: T\n)\n\n@Suppress(\"NOTHING_TO_INLINE\")\ninline operator fun <T> ValueHolder<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {\n    this.value = value\n}\n\n@Suppress(\"NOTHING_TO_INLINE\")\ninline operator fun <T> ValueHolder<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/coroutines/CombineFlows.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage ir.amirab.util.coroutines\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.combine\n\nfun <T1, T2, T3, T4, T5, T6, R> combine(\n    flow: Flow<T1>,\n    flow2: Flow<T2>,\n    flow3: Flow<T3>,\n    flow4: Flow<T4>,\n    flow5: Flow<T5>,\n    flow6: Flow<T6>,\n    transform: suspend (T1, T2, T3, T4, T5, T6) -> R\n): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->\n    transform(\n        args[0] as T1,\n        args[1] as T2,\n        args[2] as T3,\n        args[3] as T4,\n        args[4] as T5,\n        args[5] as T6,\n    )\n}\n\nfun <T1, T2, T3, T4, T5, T6, T7, R> combine(\n    flow: Flow<T1>,\n    flow2: Flow<T2>,\n    flow3: Flow<T3>,\n    flow4: Flow<T4>,\n    flow5: Flow<T5>,\n    flow6: Flow<T6>,\n    flow7: Flow<T7>,\n    transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R\n): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->\n    transform(\n        args[0] as T1,\n        args[1] as T2,\n        args[2] as T3,\n        args[3] as T4,\n        args[4] as T5,\n        args[5] as T6,\n        args[6] as T7,\n    )\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/coroutines/CoroutineUtils.kt",
    "content": "package ir.amirab.util.coroutines\n\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.launch\nimport kotlin.coroutines.cancellation.CancellationException\n\n/**\n * a launch will be used for a suspend task and a deferred\n * the difference with [async] is that exceptions will be thrown immediately without calling [Deferred.await]\n */\n\nfun <T> CoroutineScope.launchWithDeferred(\n    block: suspend CoroutineScope.() -> T\n): Deferred<T> {\n    val deferred = CompletableDeferred<T>()\n    val job = launch {\n        try {\n            deferred.complete(block())\n        } catch (e: Exception) {\n            deferred.completeExceptionally(e)\n            throw e\n        }\n    }\n    // cancell the job if caller request cancellation\n    deferred.invokeOnCompletion {\n        if (it is CancellationException) {\n            job.cancel(it)\n        }\n    }\n    return deferred\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/coroutines/debounce.kt",
    "content": "package ir.amirab.util.coroutines\n\nimport ir.amirab.util.ValueHolder\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\nfun CoroutineScope.debounce(\n    fn: () -> Unit,\n    delayMillis: Long,\n): () -> Unit {\n    var lastRun: Job? = null\n    return {\n        lastRun?.cancel()\n        lastRun = launch {\n            delay(delayMillis)\n            fn.invoke()\n        }\n    }\n}\n\nfun <T> CoroutineScope.debounce(\n    fn: (T) -> Unit,\n    delayMillis: Long,\n    previousValueMerge: ((previous: T, current: T) -> T)? = null,\n): (T) -> Unit {\n    var lastRun: Job? = null\n    val previousValueHolder = ValueHolder(null as T?)\n    return { v ->\n        val previousValue = previousValueHolder.value\n        val param = if (previousValueMerge != null && previousValue != null) {\n            previousValueMerge(previousValue, v)\n        } else {\n            v\n        }\n        previousValueHolder.value = v\n        lastRun?.cancel()\n        lastRun = launch {\n            delay(delayMillis)\n            fn.invoke(param)\n        }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/BaseSize.kt",
    "content": "package ir.amirab.util.datasize\n\nsealed class BaseSize(\n    val size: Long,\n) {\n    abstract fun longString(): String\n    fun scaleInto(baseSize: BaseSize): Double {\n        return when {\n            baseSize == this -> 1.0\n            else -> size / baseSize.size.toDouble()\n        }\n    }\n\n    data object Bits : BaseSize(1) {\n        override fun toString(): String {\n            return \"b\"\n        }\n\n        override fun longString(): String {\n            return \"Bits\"\n        }\n    }\n\n    data object Bytes : BaseSize(8) {\n        override fun toString(): String {\n            return \"B\"\n        }\n\n        override fun longString(): String {\n            return \"Bytes\"\n        }\n    }\n\n\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/CommonSizeConvertConfigs.kt",
    "content": "package ir.amirab.util.datasize\n\nobject CommonSizeConvertConfigs {\n    val BinaryBytes\n        get() = ConvertSizeConfig(\n            baseSize = BaseSize.Bytes,\n            factors = SizeFactors.BinarySizeFactors,\n        )\n    val BinaryBits\n        get() = ConvertSizeConfig(\n            baseSize = BaseSize.Bits,\n            factors = SizeFactors.BinarySizeFactors,\n        )\n    val DecimalBytes\n        get() = ConvertSizeConfig(\n            baseSize = BaseSize.Bytes,\n            factors = SizeFactors.DecimalSizeFactors,\n        )\n    val DecimalBits\n        get() = ConvertSizeConfig(\n            baseSize = BaseSize.Bits,\n            factors = SizeFactors.DecimalSizeFactors,\n        )\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/CommonSizeUnits.kt",
    "content": "package ir.amirab.util.datasize\n\nobject CommonSizeUnits {\n    val BinaryBytes = SizeUnit(\n        factorValue = SizeFactors.FactorValue.None,\n        baseSize = BaseSize.Bytes,\n        factors = SizeFactors.BinarySizeFactors,\n    )\n    val BinaryBits = SizeUnit(\n        factorValue = SizeFactors.FactorValue.None,\n        baseSize = BaseSize.Bits,\n        factors = SizeFactors.BinarySizeFactors,\n    )\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/ConvertSizeConfig.kt",
    "content": "package ir.amirab.util.datasize\n\ndata class ConvertSizeConfig(\n    val baseSize: BaseSize,\n    val factors: SizeFactors,\n    // default to auto\n    val acceptedFactors: List<SizeFactors.FactorValue> = SizeFactors.FactorValue.entries,\n)"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/SizeConverter.kt",
    "content": "package ir.amirab.util.datasize\n\nobject SizeConverter {\n    fun sizeToBytes(\n        sizeWithUnit: SizeWithUnit,\n    ): Long {\n        return convert(\n            sizeWithUnit,\n            CommonSizeConvertConfigs\n                .BinaryBytes\n                .fixedFactor(SizeFactors.FactorValue.None)\n        ).value.toLong()\n    }\n\n    fun bytesToSize(\n        bytes: Long,\n        target: ConvertSizeConfig,\n    ): SizeWithUnit {\n        return convert(\n            SizeWithUnit(\n                bytes.toDouble(),\n                CommonSizeUnits.BinaryBytes,\n            ),\n            target\n        )\n    }\n\n    fun convert(\n        src: SizeWithUnit,\n        target: ConvertSizeConfig,\n    ): SizeWithUnit {\n        val valueWithoutFactor = src.unit.factors.removeFactor(\n            src.value, src.unit.factorValue\n        )\n        val valueWithBaseSize = valueWithoutFactor * src.unit.baseSize.scaleInto(target.baseSize)\n        val factorValue = target.factors.bestFactor(\n            valueWithBaseSize.toLong(),\n            target.acceptedFactors,\n        )\n        val finalValue = target.factors.withFactor(valueWithBaseSize, factorValue)\n        return SizeWithUnit(\n            value = finalValue,\n            SizeUnit(\n                factorValue = factorValue,\n                factors = target.factors,\n                baseSize = target.baseSize,\n            )\n        )\n    }\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/SizeFactors.kt",
    "content": "package ir.amirab.util.datasize\n\nimport kotlin.math.absoluteValue\nimport kotlin.math.pow\n\nsealed class SizeFactors(\n    val baseValue: Long,\n) {\n    enum class FactorValue {\n        None,\n        Kilo,\n        Mega,\n        Giga,\n        Tera,\n//        Peta,\n//        Exa,\n    }\n\n    operator fun get(factorValue: FactorValue): Long {\n        return getFactorSize(factorValue)\n    }\n\n    private fun getFactorSize(factorValue: FactorValue): Long {\n        return factors[factorValue.ordinal]\n    }\n\n    private val factors = FactorValue.entries.map {\n        baseValue.toDouble().pow(it.ordinal).toLong()\n    }\n\n    fun bestFactor(\n        value: Long,\n        acceptedFactors: List<FactorValue> = FactorValue.entries,\n    ): FactorValue {\n        require(acceptedFactors.isNotEmpty()) {\n            \"acceptedFactors must not be empty\"\n        }\n        // we need lowest\n        if (value == 0L) {\n            return acceptedFactors.first()\n        }\n        // no other choice\n        if (acceptedFactors.size == 1) return acceptedFactors.first()\n        // find in range\n        val inRange = acceptedFactors.lastOrNull {\n            getFactorSize(it) <= value\n        }\n        if (inRange != null) {\n            return inRange\n        }\n        // find rearrest\n        return acceptedFactors.minBy {\n            (value - getFactorSize(it)).absoluteValue\n        }\n    }\n\n    fun removeFactor(value: Double, factorValue: FactorValue): Long {\n        return (value * getFactorSize(factorValue)).toLong()\n    }\n\n    fun withFactor(value: Double, factorValue: FactorValue): Double {\n        if (factorValue == FactorValue.None) return value\n        return value / getFactorSize(factorValue)\n    }\n\n    abstract fun toString(factorValue: FactorValue): String\n    abstract fun toLongString(factorValue: FactorValue): String\n\n    data object DecimalSizeFactors : SizeFactors(baseValue = 1000) {\n        override fun toString(factorValue: FactorValue): String {\n            return when (factorValue) {\n                FactorValue.None -> \"\"\n                FactorValue.Kilo -> \"K\"\n                FactorValue.Mega -> \"M\"\n                FactorValue.Giga -> \"G\"\n                FactorValue.Tera -> \"T\"\n//                FactorValue.Peta -> \"P\"\n//                FactorValue.Exa -> \"E\"\n            }\n        }\n\n        override fun toLongString(factorValue: FactorValue): String {\n            return when (factorValue) {\n                FactorValue.None -> \"\"\n                FactorValue.Kilo -> \"Kilo\"\n                FactorValue.Mega -> \"Mega\"\n                FactorValue.Giga -> \"Giga\"\n                FactorValue.Tera -> \"Tera\"\n//                FactorValue.Peta -> \"Peta\"\n//                FactorValue.Exa -> \"Exa\"\n            }\n        }\n\n    }\n\n    data object BinarySizeFactors : SizeFactors(baseValue = 1024) {\n        override fun toString(factorValue: FactorValue): String {\n            return when (factorValue) {\n                FactorValue.None -> \"\"\n                FactorValue.Kilo -> \"Ki\"\n                FactorValue.Mega -> \"Mi\"\n                FactorValue.Giga -> \"Gi\"\n                FactorValue.Tera -> \"Ti\"\n//                FactorValue.Peta -> \"Pi\"\n//                FactorValue.Exa -> \"Ei\"\n            }\n        }\n\n        override fun toLongString(factorValue: FactorValue): String {\n            return when (factorValue) {\n                FactorValue.None -> \"\"\n                FactorValue.Kilo -> \"Kibi\"\n                FactorValue.Mega -> \"Mebi\"\n                FactorValue.Giga -> \"Gibi\"\n                FactorValue.Tera -> \"Tebi\"\n//                FactorValue.Peta -> \"Pebi\"\n//                FactorValue.Exa -> \"Exbi\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/SizeUnit.kt",
    "content": "package ir.amirab.util.datasize\n\ndata class SizeUnit(\n    val factorValue: SizeFactors.FactorValue = SizeFactors.FactorValue.None,\n    val baseSize: BaseSize,\n    val factors: SizeFactors,\n) {\n    override fun toString(): String {\n        val factor = factors.toString(factorValue)\n        return \"$factor$baseSize\"\n    }\n}"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/SizeWithUnit.kt",
    "content": "package ir.amirab.util.datasize\n\nimport java.text.DecimalFormat\nimport java.text.DecimalFormatSymbols\nimport java.text.NumberFormat\nimport java.util.*\n\ndata class SizeWithUnit(\n    val value: Double,\n    val unit: SizeUnit,\n) {\n    fun toString(format: NumberFormat?): String {\n        val formattedValue = formatedValue(format)\n        return \"$formattedValue $unit\"\n    }\n\n    fun formatedValue(format: NumberFormat? = DefaultFormat) = format\n        ?.format(value)\n        ?: value.toString()\n\n    override fun toString(): String {\n        return toString(DefaultFormat)\n    }\n\n    companion object {\n        val DefaultFormat = DecimalFormat(\n            \"#.##\", DecimalFormatSymbols(\n                Locale.US,\n            )\n        )\n        val SmallFormat = DecimalFormat(\n            \"#.#\", DecimalFormatSymbols(\n                Locale.US,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/datasize/extensions.kt",
    "content": "package ir.amirab.util.datasize\n\nfun SizeUnit.asConverterConfig(\n    acceptedFactors: List<SizeFactors.FactorValue> = listOf(\n        factorValue\n    ),\n): ConvertSizeConfig {\n    return ConvertSizeConfig(\n        factors = factors,\n        baseSize = baseSize,\n        acceptedFactors = acceptedFactors\n    )\n}\n\nfun ConvertSizeConfig.bits() = copy(\n    baseSize = BaseSize.Bits\n)\n\nfun ConvertSizeConfig.bytes() = copy(\n    baseSize = BaseSize.Bytes\n)\n\nfun ConvertSizeConfig.decimal() = copy(\n    factors = SizeFactors.DecimalSizeFactors\n)\n\nfun ConvertSizeConfig.binary() = copy(\n    factors = SizeFactors.BinarySizeFactors\n)\n\nfun ConvertSizeConfig.autoSelectFactors() = copy(\n    acceptedFactors = SizeFactors.FactorValue.entries\n)\n\nfun ConvertSizeConfig.fixedFactor(factorValue: SizeFactors.FactorValue) = copy(\n    acceptedFactors = listOf(factorValue)\n)"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/enumValueOrNull.kt",
    "content": "package ir.amirab.util\n\ninline fun <reified T : Enum<T>> String.enumValueOrNull(): T? {\n    return runCatching { enumValueOf<T>(this) }.getOrNull()\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/flow/FlowOperators.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\", \"unused\")\n\npackage ir.amirab.util.flow\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.*\nimport java.util.*\nimport kotlin.time.Duration\n\nprivate val NULL = Any()\n\n/**\n * this is like simple but emits last emission\n * after last period\n */\n\n\nfun <T> Flow<T>.rest(time: Long, emitLastEmissionWithoutRest: Boolean = false): Flow<T> {\n    return channelFlow {\n        var upStreamFinished = false\n        var lastValue: Any? = NULL\n        suspend fun pushValue() {\n            val value = lastValue\n            if (lastValue !== NULL) {\n                lastValue = NULL\n                send(value as T)\n            }\n        }\n\n        val ticker = launch {\n            while (isActive) {\n                delay(time)\n                pushValue()\n                if (upStreamFinished) {\n                    break\n                }\n            }\n            close()\n        }\n        launch {\n            collect {\n                lastValue = it\n            }\n            if (emitLastEmissionWithoutRest) {\n                pushValue()\n                ticker.cancel()\n            }\n            upStreamFinished = true\n        }\n\n    }\n}\n\nfun <T, R> Flow<T>.concurrentMap(\n    capacity: Int = Channel.BUFFERED,\n    transformBlock: suspend (T) -> R,\n): Flow<R> {\n    return flow {\n        coroutineScope {\n            map {\n                async(start = CoroutineStart.LAZY) {\n                    transformBlock(\n                        it\n                    )\n                }\n            }\n                .buffer(capacity)\n                .map {\n                    it.start()\n                    it.await()\n                }\n                .let {\n                    emitAll(it)\n                }\n        }\n    }\n}\n\nfun <T> Flow<T>.throttle(waitMillis: Int) = flow {\n    coroutineScope {\n        val context = coroutineContext\n        var nextTime = 0L\n        var delayPost: Deferred<Unit>? = null\n        collect {\n            val current = System.currentTimeMillis()\n            if (nextTime < current) {\n                nextTime = current + waitMillis\n                emit(it)\n                delayPost?.cancel()\n            } else {\n                val delayNext = nextTime\n                delayPost?.cancel()\n                delayPost = async(Dispatchers.Default) {\n                    delay(nextTime - current)\n                    if (delayNext == nextTime) {\n                        nextTime = System.currentTimeMillis() + waitMillis\n                        withContext(context) {\n                            emit(it)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n\nfun <T> Flow<T>.rateLimit(limit: Long, per: Duration): Flow<T> {\n    return rateLimit(limit, per.inWholeMilliseconds)\n}\n\nfun <T> Flow<T>.rateLimit(limit: Long, per: Long) = flow<T> {\n    coroutineScope {\n        val context = coroutineContext\n        var lastStartTime = System.currentTimeMillis()\n        var remainingInDuration = limit\n        val items = LinkedList<T>()\n        var isDone = false\n        launch(context) {\n            collect {\n                items.add(it)\n            }\n            isDone = true\n        }\n        launch(Dispatchers.Default) {\n            while (isActive) {\n                yield()\n                if (remainingInDuration > 0) {\n                    val removeFirst = items.removeFirstOrNull()\n                    if (removeFirst != null) {\n                        withContext(context) {\n                            emit(removeFirst)\n                        }\n                        remainingInDuration--\n                    } else {\n                        if (isDone) {\n                            break\n                        }\n                    }\n\n                } else {\n                    val waitUntil = lastStartTime + per\n                    delay(waitUntil - System.currentTimeMillis())\n                    lastStartTime = System.currentTimeMillis()\n                    remainingInDuration = limit\n                }\n            }\n        }\n    }\n}\n\nfun <T> interval(time: Long, initialValue: T, newValue: (T) -> T): Flow<T> {\n    var value = initialValue\n    return interval(time)\n        .map {\n            value.apply {\n                value = newValue(this)\n            }\n        }\n}\n\nfun interval(time: Long, timeOut: Long = time) = flow {\n    if (timeOut > 0) {\n        delay(timeOut)\n    }\n    emit(Unit)\n    while (true) {\n        delay(time)\n        emit(Unit)\n    }\n}\n\nfun <T> Flow<T>.saved(count: Int): Flow<List<T>> {\n    require(count >= 0)\n    return when (count) {\n        0 -> emptyFlow()\n        else -> scan<T, List<T>>(\n            listOf()\n        ) { l, v ->\n            if (l.size < count) {\n                l.plus(v)\n            } else {\n                l.drop(1).plus(v)\n            }\n        }.drop(1) // scan emits an initial value (emptyList)\n    }\n}\n\nfun <T> Flow<List<T>>.pad(capacity: Int, fillAfter: Boolean) = map { actual ->\n    val size = actual.size\n    if (capacity > size) {\n        val pad = List(capacity - size) { null }\n        if (fillAfter) actual + pad\n        else pad + actual\n    } else actual\n}\n\nfun <T> Flow<T>.takeFirstEmitInEvery(millis: Long) = flow<T> {\n    var lastEmitTime = 0L\n    collect {\n        val now = System.currentTimeMillis()\n        if (now - lastEmitTime >= millis) {\n            lastEmitTime = now\n            emit(it)\n        }\n    }\n}\n\nfun <T> Flow<T>.chunked(count: Int): Flow<List<T>> = flow {\n    val list = mutableListOf<T>()\n    collect {\n        if (list.size == count) {\n            emit(list.toList())\n            list.clear()\n        } else {\n            list.add(it)\n        }\n    }\n    if (list.isNotEmpty()) {\n        emit(list)\n    }\n}\n\nfun <T> Flow<T>.onEachLatest(block: suspend (T) -> Unit) = transformLatest {\n    block(it)\n    emit(it)\n}\n\nfun <T, R> Flow<T>.withPrevious(\n    transform: (previous: T?, current: T) -> R,\n): Flow<R> {\n    return saved(2)\n        .pad(2, false)\n        .map {\n            val previous = it[0]\n            val current = it[1] as T\n            transform(previous, current)\n        }\n}\n\nfun <T> Flow<T>.withPrevious(): Flow<Pair<T?, T>> = withPrevious { previous, current -> previous to current }"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/flow/FlowUtils.kt",
    "content": "package ir.amirab.util.flow\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.*\n\nfun Flow<Unit>.withStartEmit(): Flow<Unit> {\n    return flow {\n        emit(Unit)\n        emitAll(this@withStartEmit)\n    }\n}\n\nfun <T> createMutableStateFlowFromFlow(\n    flow: Flow<T>,\n    initialValue: T,\n    updater: (T) -> Unit,\n    scope: CoroutineScope,\n): MutableStateFlow<T> {\n    val downStream = MutableStateFlow(initialValue)\n    flow.onEach { newFromUpStream ->\n        downStream.update { newFromUpStream }\n    }.launchIn(scope)\n    downStream.onEach {\n        updater(it)\n    }.launchIn(scope)\n    return downStream\n}\n\nfun <T> createMutableStateFlowFromStateFlow(\n    flow: StateFlow<T>,\n    updater: suspend (T) -> Unit,\n    scope: CoroutineScope,\n): MutableStateFlow<T> {\n    val downStream = MutableStateFlow(flow.value)\n    flow.onEach { newFromUpStream ->\n        downStream.update { newFromUpStream }\n    }.launchIn(scope)\n    downStream.onEach {\n        updater(it)\n    }.launchIn(scope)\n    return downStream\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/flow/StateFlowUtil.kt",
    "content": "package ir.amirab.util.flow\n\nimport arrow.optics.Lens\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.*\n\nclass DerivedStateFlow<T>(\n    private val getValue: () -> T,\n    private val flow: Flow<T>,\n) : StateFlow<T> {\n    override val replayCache: List<T>\n        get() = listOf(value)\n    override val value: T\n        get() = getValue()\n\n    override suspend fun collect(collector: FlowCollector<T>): Nothing {\n        coroutineScope {\n            flow\n                .distinctUntilChanged()\n                .stateIn(this)\n                .collect(collector)\n        }\n    }\n}\n\n@PublishedApi\ninternal class TwoWayDerivedStateFlow<T, R>(\n    private val upStream: MutableStateFlow<T>,\n    private val map: (T) -> R,\n    private val unMap: (R) -> T,\n) : MutableStateFlow<R> {\n\n    override var value: R\n        get() {\n            return map(upStream.value)\n        }\n        set(v) {\n            upStream.value = unMap(v)\n        }\n\n    override val replayCache: List<R>\n        get() = listOf(value)\n\n    private val _sc = MutableStateFlow(0)\n\n    override val subscriptionCount: StateFlow<Int>\n        get() = _sc.asStateFlow()\n\n    private val _mappedStream = upStream.mapStateFlow(map)\n\n    override suspend fun collect(collector: FlowCollector<R>): Nothing {\n        try {\n            _sc.update { it + 1 }\n            _mappedStream.collect(collector)\n        } finally {\n            _sc.update { it - 1 }\n        }\n    }\n\n    override fun compareAndSet(expect: R, update: R): Boolean {\n        return upStream.compareAndSet(\n            expect = unMap(expect),\n            update = unMap(update),\n        )\n    }\n\n    @ExperimentalCoroutinesApi\n    override fun resetReplayCache() {\n        upStream.resetReplayCache()\n    }\n\n    override fun tryEmit(value: R): Boolean {\n        this.value = value\n        return true\n    }\n\n    override suspend fun emit(value: R) {\n        this.value = value\n    }\n}\n\n/**\n * NOTE :\n * DON\"T USE MutableStateFlow::update\n * If I use the map and unmap does not return equally it will cause to infinite loop\n *\n */\nfun <T, R> MutableStateFlow<T>.mapTwoWayStateFlow(\n    map: (T) -> R,\n    unMap: T.(R) -> T,\n): MutableStateFlow<R> {\n    return TwoWayDerivedStateFlow(\n        upStream = this,\n        map = map,\n        unMap = {\n            unMap(value, it)\n        },\n    )\n}\n\nfun <T, R> MutableStateFlow<T>.mapTwoWayStateFlow(\n    lens: Lens<T, R>\n): MutableStateFlow<R> {\n    return TwoWayDerivedStateFlow(\n        upStream = this,\n        map = lens::get,\n        unMap = {\n            lens.set(value, it)\n        },\n    )\n}\n\n\nfun <T, R> StateFlow<T>.mapStateFlow(\n    transform: (T) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = { transform(value) },\n        flow = this.map(transform)\n    )\n}\n\nfun <T1, T2, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    transform: (a: T1, b: T2) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value)\n        },\n        flow = combine(a, b) { a, b ->\n            transform(a, b)\n        }\n    )\n}\n\nfun <T1, T2, T3, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    c: StateFlow<T3>,\n    transform: (a: T1, b: T2, c: T3) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value, c.value)\n        },\n        flow = combine(a, b, c) { a, b, c ->\n            transform(a, b, c)\n        }\n    )\n}\n\nfun <T1, T2, T3, T4, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    c: StateFlow<T3>,\n    d: StateFlow<T4>,\n    transform: (a: T1, b: T2, c: T3, d: T4) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value, c.value, d.value)\n        },\n        flow = combine(a, b, c, d) { a, b, c, d ->\n            transform(a, b, c, d)\n        }\n    )\n}\n\nfun <T1, T2, T3, T4, T5, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    c: StateFlow<T3>,\n    d: StateFlow<T4>,\n    e: StateFlow<T5>,\n    transform: (a: T1, b: T2, c: T3, d: T4, e: T5) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value, c.value, d.value, e.value)\n        },\n        flow = combine(a, b, c, d, e) { a, b, c, d, e ->\n            transform(a, b, c, d, e)\n        }\n    )\n}\n\nfun <T1, T2, T3, T4, T5, T6, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    c: StateFlow<T3>,\n    d: StateFlow<T4>,\n    e: StateFlow<T5>,\n    f: StateFlow<T6>,\n    transform: (a: T1, b: T2, c: T3, d: T4, e: T5, f: T6) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value, c.value, d.value, e.value, f.value)\n        },\n        flow = combine(a, b, c, d, e, f) { array ->\n            @Suppress(\"UNCHECKED_CAST\")\n            transform(array[0] as T1, array[1] as T2, array[2] as T3, array[3] as T4, array[4] as T5, array[5] as T6)\n        }\n    )\n}\n\nfun <T1, T2, T3, T4, T5, T6, T7, R> combineStateFlows(\n    a: StateFlow<T1>,\n    b: StateFlow<T2>,\n    c: StateFlow<T3>,\n    d: StateFlow<T4>,\n    e: StateFlow<T5>,\n    f: StateFlow<T6>,\n    g: StateFlow<T7>,\n    transform: (a: T1, b: T2, c: T3, d: T4, e: T5, f: T6, g: T7) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(a.value, b.value, c.value, d.value, e.value, f.value, g.value)\n        },\n        flow = combine(a, b, c, d, e, f, g) { array ->\n            @Suppress(\"UNCHECKED_CAST\")\n            transform(\n                array[0] as T1,\n                array[1] as T2,\n                array[2] as T3,\n                array[3] as T4,\n                array[4] as T5,\n                array[5] as T6,\n                array[6] as T7,\n            )\n        }\n    )\n}\n\n\ninline fun <reified T, R> combineStateFlows(\n    flows: Iterable<StateFlow<T>>,\n    noinline transform: (list: Array<T>) -> R\n): StateFlow<R> {\n    return DerivedStateFlow(\n        getValue = {\n            transform(\n                flows\n                    .map { it.value }\n                    .toTypedArray()\n            )\n        },\n        flow = combine(flows) {\n            transform(it)\n        }\n    )\n}\n\ninline fun <reified T, R> combineStateFlows(\n    vararg flows: StateFlow<T>,\n    noinline transform: (list: Array<T>) -> R\n): StateFlow<R> {\n    return combineStateFlows(listOf(*flows), transform)\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/lock.kt",
    "content": "@file:OptIn(ExperimentalContracts::class)\n\npackage ir.amirab.util\n\nimport arrow.core.Either\nimport arrow.core.left\nimport arrow.core.right\nimport kotlinx.coroutines.sync.Mutex\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.InvocationKind\nimport kotlin.contracts.contract\n\nobject MutexIsLocked\n\ninline fun <T> Mutex.tryLocked(block: () -> T): Either<MutexIsLocked, T> {\n    contract {\n        callsInPlace(block, InvocationKind.AT_MOST_ONCE)\n    }\n    if (tryLock()) {\n        try {\n            return block().right()\n        } finally {\n            unlock()\n        }\n    }\n    return MutexIsLocked.left()\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/openUrl.kt",
    "content": "package ir.amirab.util\n\nexpect object URLOpener {\n    fun openUrl(url: String)\n}\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/osfileutil/FileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport java.io.File\n\ninterface FileUtils {\n    fun openFile(file: File): Boolean\n    fun openFolderOfFile(file: File): Boolean\n    fun openFolder(folder: File): Boolean\n    fun canWriteInThisFolder(folder: String): Boolean\n    fun isRemovableStorage(path: String): Boolean\n\n    companion object : FileUtils by getPlatformFileUtil()\n}\n\nexpect fun getPlatformFileUtil(): FileUtils\n"
  },
  {
    "path": "shared/utils/src/commonMain/kotlin/ir/amirab/util/osfileutil/FileUtilsBase.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport java.io.File\nimport java.io.FileNotFoundException\n\nabstract class FileUtilsBase : FileUtils {\n    override fun openFile(file: File): Boolean {\n        return openFileInternal(\n            file = preparedFile(file)\n        )\n    }\n\n    override fun openFolderOfFile(file: File): Boolean {\n        return openFolderOfFileInternal(\n            file = preparedFile(file)\n        )\n    }\n\n    override fun openFolder(folder: File): Boolean {\n        return openFolderInternal(\n            folder = preparedFile(folder)\n        )\n    }\n\n    override fun canWriteInThisFolder(folder: String): Boolean {\n        return runCatching {\n            File(folder).canUseThisAsFolder()\n        }.getOrElse { false }\n    }\n\n    private fun File.canUseThisAsFolder(): Boolean {\n        var current: File? = this\n        while (true) {\n            if (current == null) break\n            if (current.exists()) {\n                return current.isDirectory\n            }\n            current = current.parentFile\n        }\n        return false\n    }\n\n    private fun preparedFile(file: File): File {\n        val file = file.canonicalFile.absoluteFile\n        if (!file.exists()) {\n            throw FileNotFoundException(\"$file not found\")\n        }\n        return file\n    }\n\n    protected abstract fun openFileInternal(file: File): Boolean\n    protected abstract fun openFolderOfFileInternal(file: File): Boolean\n    protected abstract fun openFolderInternal(folder: File): Boolean\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/openUrl.desktop.kt",
    "content": "package ir.amirab.util\n\nimport java.awt.Desktop\nimport java.net.URI\n\nactual object URLOpener {\n    actual fun openUrl(url: String) {\n        runCatching {\n            val desktop = Desktop.getDesktop()\n            if (desktop.isSupported(Desktop.Action.BROWSE)) {\n                desktop.browse(URI(url))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/DesktopFileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport kotlin.io.path.Path\nimport kotlin.io.path.absolute\nimport kotlin.io.path.fileStore\n\nabstract class DesktopFileUtils : FileUtilsBase() {\n    override fun isRemovableStorage(path: String): Boolean {\n        runCatching {\n            val store = Path(path).absolute().fileStore()\n            if (store.supportsFileAttributeView(\"basic\")) {\n                val isRemovable = store.getAttribute(\"volume:isRemovable\")\n                if (isRemovable is Boolean) {\n                    return isRemovable\n                }\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/FileUtils.desktop.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport ir.amirab.util.platform.Platform\nimport ir.amirab.util.platform.asDesktop\n\nactual fun getPlatformFileUtil(): FileUtils {\n    return when (Platform.asDesktop()) {\n        Platform.Desktop.Windows -> WindowsFileUtils()\n        Platform.Desktop.Linux -> LinuxFileUtils()\n        Platform.Desktop.MacOS -> MacOsFileUtils()\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/JVMFileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport java.awt.Desktop\nimport java.io.File\n\n/**\n * it uses the jvm default.\n */\ninternal class JVMFileUtils : DesktopFileUtils() {\n    override fun openFileInternal(file: File): Boolean {\n        runCatching {\n            Desktop.getDesktop().open(file)\n            return true\n        }\n        return false\n    }\n\n    override fun openFolderOfFileInternal(file: File): Boolean {\n        runCatching {\n            Desktop.getDesktop().browseFileDirectory(file)\n            return true\n        }\n        return false\n    }\n\n    override fun openFolderInternal(folder: File): Boolean {\n        kotlin.runCatching {\n            Desktop.getDesktop().open(folder)\n            return true\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/LinuxFileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport ir.amirab.util.execAndWait\nimport java.io.File\nimport java.net.URLEncoder\n\ninternal class LinuxFileUtils : DesktopFileUtils() {\n    override fun openFileInternal(file: File): Boolean {\n        return execAndWait(arrayOf(\"xdg-open\", file.path))\n    }\n\n    override fun openFolderOfFileInternal(file: File): Boolean {\n        val uri = \"file://\" + encodePath(file.path)\n        val dbusSendResult = execAndWait(\n            arrayOf(\n                \"dbus-send\",\n                \"--print-reply\",\n                \"--dest=org.freedesktop.FileManager1\",\n                \"/org/freedesktop/FileManager1\",\n                \"org.freedesktop.FileManager1.ShowItems\",\n                \"array:string:$uri\",\n                \"string:\"\n            )\n        )\n        if (dbusSendResult) {\n            return true\n        }\n        val xdgOpenResult = execAndWait(\n            arrayOf(\"xdg-open\", file.parent)\n        )\n        return xdgOpenResult\n    }\n\n    override fun openFolderInternal(folder: File): Boolean {\n        return execAndWait(arrayOf(\"xdg-open\", folder.parent))\n    }\n\n    private fun encodePath(path: String): String {\n        return path\n            .split('/')\n            .joinToString(\"/\") {\n                URLEncoder\n                    .encode(it, Charsets.UTF_8)\n                    .replace(\"+\", \"%20\")\n            }\n    }\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/MacOsFileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport ir.amirab.util.execAndWait\nimport java.io.File\n\ninternal class MacOsFileUtils : DesktopFileUtils() {\n    override fun openFileInternal(file: File): Boolean {\n        return execAndWait(arrayOf(\"open\", file.path))\n    }\n\n    override fun openFolderOfFileInternal(file: File): Boolean {\n        return execAndWait(arrayOf(\"open\", \"-R\", file.path))\n    }\n\n    override fun openFolderInternal(folder: File): Boolean {\n        return execAndWait(arrayOf(\"open\", folder.path))\n    }\n\n}\n"
  },
  {
    "path": "shared/utils/src/desktopMain/kotlin/ir/amirab/util/osfileutil/WindowsFileUtils.kt",
    "content": "package ir.amirab.util.osfileutil\n\nimport com.sun.jna.Native\nimport com.sun.jna.Pointer\nimport com.sun.jna.platform.win32.*\nimport com.sun.jna.win32.StdCallLibrary\nimport com.sun.jna.win32.W32APIOptions\nimport ir.amirab.util.execAndWait\nimport java.io.File\nimport kotlin.io.path.Path\nimport kotlin.io.path.absolute\n\ninternal class WindowsFileUtils : DesktopFileUtils() {\n    override fun openFileInternal(file: File): Boolean {\n        return execAndWait(arrayOf(\"cmd\", \"/c\", \"start\", \"/B\", \"\", file.path.quoted()))\n    }\n\n    override fun openFolderOfFileInternal(file: File): Boolean {\n        val nativeSuccess = showFileInFolderViaNative(file.path)\n        if (nativeSuccess) {\n            return true\n        }\n        //fallback to use explorer\n        return execAndWait(arrayOf(\"cmd\", \"/c\", \"explorer.exe\", \"/select,\", file.path.quoted()))\n    }\n\n    override fun openFolderInternal(folder: File): Boolean {\n        val nativeSuccess = openFolderViaNative(folder.path)\n        if (nativeSuccess) {\n            return true\n        }\n        //fallback to use explorer\n        return execAndWait(arrayOf(\"cmd\", \"/c\", \"explorer.exe\", folder.path.quoted()))\n    }\n\n    override fun isRemovableStorage(path: String): Boolean {\n        return try {\n            isRemovableStorageViaNative(path)\n        } catch (e: Exception) {\n            e.printStackTrace()\n            super.isRemovableStorage(path)\n        }\n    }\n\n    private fun isRemovableStorageViaNative(path: String): Boolean {\n        val rootPath = Path(path).absolute().root.toString()\n        val driveType = Kernel32.INSTANCE.GetDriveType(rootPath)\n        return driveType == WinBase.DRIVE_REMOVABLE\n    }\n\n    private fun showFileInFolderViaNative(\n        file: String,\n    ): Boolean {\n        try {\n            Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_APARTMENTTHREADED)\n            val path = Shell32Ex.INSTANCE.ILCreateFromPath(File(file).parent)\n            val selectedFiles = arrayOf(Shell32Ex.INSTANCE.ILCreateFromPath(file))\n            val cidl = WinDef.UINT(selectedFiles.size.toLong())\n            try {\n                val res = Shell32Ex.INSTANCE.SHOpenFolderAndSelectItems(\n                    pIdlFolder = path,\n                    cIdl = cidl,\n                    apIdl = selectedFiles,\n                    dwFlags = WinDef.DWORD(0)\n                )\n                return WinError.S_OK == res\n            } finally {\n                Shell32Ex.INSTANCE.ILFree(path)\n                selectedFiles.forEach {\n                    Shell32Ex.INSTANCE.ILFree(it)\n                }\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n            return false\n        } finally {\n            Ole32.INSTANCE.CoUninitialize()\n        }\n    }\n\n    private fun openFolderViaNative(folder: String): Boolean {\n        try {\n            val result = Shell32.INSTANCE.ShellExecute(\n                null, \"explore\", folder, null, null, WinUser.SW_NORMAL,\n            ).toInt()\n            return result > 32\n        } catch (e: Exception) {\n            e.printStackTrace()\n            return false\n        }\n    }\n\n    private fun String.quoted() = \"\\\"$this\\\"\"\n\n}\n\n\nprivate interface Shell32Ex : StdCallLibrary {\n    fun ILCreateFromPath(path: String?): Pointer?\n    fun ILFree(pIdl: Pointer?)\n    fun SHOpenFolderAndSelectItems(\n        pIdlFolder: Pointer?,\n        cIdl: WinDef.UINT?,\n        apIdl: Array<Pointer?>?,\n        dwFlags: WinDef.DWORD?,\n    ): WinNT.HRESULT?\n\n    companion object {\n        val INSTANCE: Shell32Ex = Native.load(\"shell32\", Shell32Ex::class.java, W32APIOptions.DEFAULT_OPTIONS)\n    }\n}\n"
  }
]