Repository: sheikhhaziq/vibemusic Branch: main Commit: 2e08142f8f5e Files: 216 Total size: 13.5 MB Directory structure: gitextract_k51tdnmq/ ├── .fvmrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── config.yml │ └── workflows/ │ ├── auto-label-issues.yml │ ├── beta-release.yml │ ├── branch-protection-check.yml │ └── stable-release.yml ├── .gitignore ├── .metadata ├── .vscode/ │ ├── launch.json │ └── settings.json ├── CONTRIBUTING.md ├── GyawunMusic-2.0.16.flatpak ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── jhelum/ │ │ │ │ └── gyawun/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── anim/ │ │ │ │ ├── in_animation.xml │ │ │ │ └── out_animation.xml │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-hdpi/ │ │ │ │ └── audio_service_stop.xml │ │ │ ├── drawable-mdpi/ │ │ │ │ └── audio_service_stop.xml │ │ │ ├── drawable-night/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi/ │ │ │ │ └── audio_service_stop.xml │ │ │ ├── drawable-xxhdpi/ │ │ │ │ └── audio_service_stop.xml │ │ │ ├── drawable-xxxhdpi/ │ │ │ │ └── audio_service_stop.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── raw/ │ │ │ │ └── keep.xml │ │ │ ├── values/ │ │ │ │ ├── attrs.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── themes.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v31/ │ │ │ │ └── styles.xml │ │ │ ├── values-v31/ │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ └── automotive_app_desc.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build/ │ │ └── reports/ │ │ └── problems/ │ │ └── problems-report.html │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle.kts ├── build-flatpak.sh ├── devtools_options.yaml ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── flutter_native_splash.yaml ├── lib/ │ ├── core/ │ │ ├── extensions/ │ │ │ ├── random_material_shape.dart │ │ │ └── string_extensions.dart │ │ ├── models/ │ │ │ └── app_config.dart │ │ ├── utils/ │ │ │ ├── expressive_sheet.dart │ │ │ └── service_locator.dart │ │ └── widgets/ │ │ ├── expressive_app_bar.dart │ │ ├── expressive_list_group.dart │ │ ├── expressive_list_tile.dart │ │ ├── expressive_switch_list_tile.dart │ │ ├── internet_guard.dart │ │ ├── library_tile.dart │ │ ├── rounded_polygon_icon.dart │ │ ├── section_item.dart │ │ ├── sections/ │ │ │ ├── section_multi_column.dart │ │ │ └── section_row.dart │ │ ├── song_tile.dart │ │ └── tiles/ │ │ ├── section_list_tile.dart │ │ └── section_row_tile.dart │ ├── generated/ │ │ ├── intl/ │ │ │ ├── messages_all.dart │ │ │ ├── messages_en.dart │ │ │ ├── messages_es.dart │ │ │ ├── messages_fr.dart │ │ │ ├── messages_hi.dart │ │ │ ├── messages_it.dart │ │ │ ├── messages_tr.dart │ │ │ └── messages_ur.dart │ │ └── l10n.dart │ ├── l10n/ │ │ ├── intl_en.arb │ │ ├── intl_es.arb │ │ ├── intl_fr.arb │ │ ├── intl_hi.arb │ │ ├── intl_it.arb │ │ ├── intl_tr.arb │ │ └── intl_ur.arb │ ├── main.dart │ ├── screens/ │ │ ├── browse/ │ │ │ ├── browse_page.dart │ │ │ └── cubit/ │ │ │ ├── browse_cubit.dart │ │ │ └── browse_state.dart │ │ ├── chip/ │ │ │ ├── chip_page.dart │ │ │ └── cubit/ │ │ │ ├── chip_cubit.dart │ │ │ └── chip_state.dart │ │ ├── home/ │ │ │ ├── cubit/ │ │ │ │ ├── home_cubit.dart │ │ │ │ └── home_state.dart │ │ │ ├── home_page.dart │ │ │ └── widgets/ │ │ │ └── chips_row.dart │ │ ├── library/ │ │ │ ├── cubit/ │ │ │ │ ├── library_cubit.dart │ │ │ │ └── library_state.dart │ │ │ ├── downloads/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── downloads_cubit.dart │ │ │ │ │ └── downloads_state.dart │ │ │ │ ├── downloading/ │ │ │ │ │ ├── cubit/ │ │ │ │ │ │ ├── downloading_cubit.dart │ │ │ │ │ │ └── downloading_state.dart │ │ │ │ │ ├── downloading_page.dart │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── downloading_section_tile.dart │ │ │ │ │ └── downloading_song_tile.dart │ │ │ │ ├── downloads_page.dart │ │ │ │ └── playlist/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── download_playlist_cubit.dart │ │ │ │ │ └── download_playlist_state.dart │ │ │ │ ├── download_playlist_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── download_playlist_header.dart │ │ │ │ └── download_song_tile.dart │ │ │ ├── favourites/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── favourites_cubit.dart │ │ │ │ │ └── favourites_state.dart │ │ │ │ └── favourites_page.dart │ │ │ ├── history/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── history_cubit.dart │ │ │ │ │ └── history_state.dart │ │ │ │ └── history_page.dart │ │ │ ├── library_page.dart │ │ │ ├── playlist/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── playlist_details_cubit.dart │ │ │ │ │ └── playlist_details_state.dart │ │ │ │ └── playlist_details_page.dart │ │ │ └── widgets/ │ │ │ └── my_playlist_header.dart │ │ ├── player/ │ │ │ ├── player_page.dart │ │ │ └── widgets/ │ │ │ ├── lyrics_box.dart │ │ │ ├── play_pause_button.dart │ │ │ └── queue_list.dart │ │ ├── search/ │ │ │ ├── cubit/ │ │ │ │ ├── search_cubit.dart │ │ │ │ └── search_state.dart │ │ │ └── search_page.dart │ │ ├── settings/ │ │ │ ├── about/ │ │ │ │ └── about_page.dart │ │ │ ├── appearance/ │ │ │ │ ├── appearance_page.dart │ │ │ │ └── cubit/ │ │ │ │ ├── appearance_cubit.dart │ │ │ │ └── appearance_state.dart │ │ │ ├── backup_storage/ │ │ │ │ ├── backup_storage_page.dart │ │ │ │ └── cubit/ │ │ │ │ ├── backup_storage_cubit.dart │ │ │ │ └── backup_storage_state.dart │ │ │ ├── cubit/ │ │ │ │ ├── settings_system_cubit.dart │ │ │ │ └── settings_system_state.dart │ │ │ ├── player/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── player_settings_cubit.dart │ │ │ │ │ └── player_settings_state.dart │ │ │ │ ├── equalizer/ │ │ │ │ │ ├── cubit/ │ │ │ │ │ │ ├── equalizer_cubit.dart │ │ │ │ │ │ ├── equalizer_state.dart │ │ │ │ │ │ ├── loudness_cubit.dart │ │ │ │ │ │ └── loudness_state.dart │ │ │ │ │ └── equalizer_page.dart │ │ │ │ └── player_settings_page.dart │ │ │ ├── privacy/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── privacy_cubit.dart │ │ │ │ │ └── privacy_state.dart │ │ │ │ └── privacy_page.dart │ │ │ ├── services/ │ │ │ │ └── yt_music/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── ytmusic_cubit.dart │ │ │ │ │ └── ytmusic_state.dart │ │ │ │ └── yt_music_page.dart │ │ │ ├── settings_page.dart │ │ │ └── widgets/ │ │ │ ├── color_icon.dart │ │ │ └── setting_item.dart │ │ └── shell/ │ │ ├── app_shell.dart │ │ └── widgets/ │ │ └── bottom_player.dart │ ├── services/ │ │ ├── bottom_message.dart │ │ ├── custom_audio_stream.dart │ │ ├── download_manager.dart │ │ ├── favourites_manager.dart │ │ ├── file_storage.dart │ │ ├── history_manager.dart │ │ ├── library.dart │ │ ├── lyrics.dart │ │ ├── media_player.dart │ │ ├── settings_manager.dart │ │ ├── stream_client.dart │ │ ├── update_service/ │ │ │ ├── models/ │ │ │ │ └── update_info.dart │ │ │ ├── update_service.dart │ │ │ └── widgets/ │ │ │ ├── update_checking.dart │ │ │ └── update_dialog.dart │ │ └── yt_audio_stream.dart │ ├── themes/ │ │ ├── colors.dart │ │ ├── dark.dart │ │ ├── light.dart │ │ ├── text_styles.dart │ │ ├── theme.dart │ │ └── typography.dart │ └── utils/ │ ├── adaptive_widgets/ │ │ ├── adaptive_widgets.dart │ │ ├── appbar.dart │ │ ├── buttons.dart │ │ ├── card.dart │ │ ├── dropdown_button.dart │ │ ├── icons.dart │ │ ├── inkwell.dart │ │ ├── listtile.dart │ │ ├── no_splash_factory.dart │ │ ├── progress_ring.dart │ │ ├── scaffold.dart │ │ ├── slider.dart │ │ ├── switch.dart │ │ ├── text_field.dart │ │ └── theme.dart │ ├── add_history.dart │ ├── bottom_modals.dart │ ├── check_update.dart │ ├── enhanced_image.dart │ ├── extensions.dart │ ├── format_duration.dart │ ├── internet_guard.dart │ ├── playlist_icon.dart │ ├── playlist_icon_widget.dart │ ├── playlist_icons.dart │ ├── playlist_thumbnail.dart │ ├── pprint.dart │ ├── router.dart │ ├── song_thumbnail.dart │ └── text_controller_builder.dart ├── pubspec.yaml └── test/ └── widget_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .fvmrc ================================================ { "flutter": "3.41.4" } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Report a bug in the app (stable or beta) labels: ["bug"] body: - type: dropdown id: channel attributes: label: Release channel description: Which version are you using? options: - Stable - Beta validations: required: true - type: input id: version attributes: label: App version placeholder: e.g. 2.0.11 validations: required: true - type: dropdown id: source attributes: label: Install source options: - GitHub Releases - IzzyOnDroid (F-Droid) validations: required: true - type: input id: android attributes: label: Android version placeholder: e.g. Android 14 validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce validations: required: true - type: textarea id: expected attributes: label: Expected behavior validations: required: true - type: textarea id: actual attributes: label: Actual behavior validations: required: true - type: textarea id: regression attributes: label: Regression information (beta only) description: Did this work correctly in the latest stable version? placeholder: Yes / No / Not sure ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Questions and support url: https://github.com/sheikhhaziq/gyawun_music/discussions about: Please use Discussions for questions and general support. ================================================ FILE: .github/workflows/auto-label-issues.yml ================================================ name: Auto-label issues by release channel on: issues: types: [opened] permissions: issues: write jobs: label: runs-on: ubuntu-latest steps: - name: Auto-label by release channel uses: actions/github-script@v7 with: script: | const body = context.payload.issue.body || ""; function getSectionValue(title) { const regex = new RegExp( `###\\s+${title}[\\s\\S]*?\\n\\n([^#]+)`, "i" ); const match = body.match(regex); return match ? match[1].trim() : null; } const channel = getSectionValue("Release channel"); if (!channel) return; const labels = []; if (channel.toLowerCase().startsWith("stable")) { labels.push("stable"); } if (channel.toLowerCase().startsWith("beta")) { labels.push("beta"); } if (!labels.length) return; await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels, }); ================================================ FILE: .github/workflows/beta-release.yml ================================================ name: Beta Release on: push: tags: - 'v*-beta.*' jobs: beta-release: if: "contains(github.ref, '-beta.')" runs-on: ubuntu-latest outputs: release_tag: ${{ steps.app_version.outputs.version }} release_channel: 'beta' steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate tag is on beta branch run: | if ! git branch -r --contains "${{ github.ref_name }}" | grep -q 'origin/beta'; then echo "❌ Tag ${{ github.ref_name }} is not on the beta branch" exit 1 fi echo "✅ Tag is on beta branch" - name: Read FVM version id: fvm_version run: | FVM_VERSION=$(jq -r '.flutterSdkVersion' .fvm/fvm_config.json) echo "version=$FVM_VERSION" >> $GITHUB_OUTPUT echo "Flutter version from FVM: $FVM_VERSION" - name: Read and validate beta version id: app_version run: | TAG="${{ github.ref_name }}" # e.g. v1.2.0-beta.1 VERSION="${TAG#v}" # e.g. 1.2.0-beta.1 if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then echo "❌ Tag must follow vX.Y.Z-beta.N format, got: $TAG" exit 1 fi VERSION_CODE=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f2) if [[ -z "$VERSION_CODE" || "$VERSION_CODE" == "0" ]]; then echo "❌ pubspec.yaml must have a valid build number (not 0)" exit 1 fi sed -i "s/^version:.*/version: $VERSION+$VERSION_CODE/" pubspec.yaml echo "version=$VERSION" >> $GITHUB_OUTPUT echo "build=$VERSION_CODE" >> $GITHUB_OUTPUT echo "✅ Beta version: $VERSION+$VERSION_CODE" - name: Setup Flutter with FVM uses: kuhnroyal/flutter-fvm-config-action/setup@v3 - name: Decode keystore run: | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks - name: Create key.properties run: | cat > android/key.properties << EOF storePassword=${{ secrets.STORE_PASSWORD }} keyPassword=${{ secrets.KEY_PASSWORD }} keyAlias=${{ secrets.KEY_ALIAS }} storeFile=keystore.jks EOF - name: Install dependencies run: flutter pub get - name: Generate Splash Screen run: dart run flutter_native_splash:create - name: Build Android APK (Beta - Universal) run: flutter build apk --flavor beta --release - name: Build Android APK (Beta - Split per ABI) run: flutter build apk --flavor beta --release --split-per-abi - name: Rename build artifacts run: | mkdir -p releases V=${{ steps.app_version.outputs.version }} cp build/app/outputs/flutter-apk/app-beta-release.apk \ releases/gyawun-beta-v${V}-universal.apk cp build/app/outputs/flutter-apk/app-arm64-v8a-beta-release.apk \ releases/gyawun-beta-v${V}-arm64-v8a.apk cp build/app/outputs/flutter-apk/app-armeabi-v7a-beta-release.apk \ releases/gyawun-beta-v${V}-armeabi-v7a.apk cp build/app/outputs/flutter-apk/app-x86_64-beta-release.apk \ releases/gyawun-beta-v${V}-x86_64.apk - name: Create Beta Pre-Release uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.ref_name }} name: Beta v${{ steps.app_version.outputs.version }} prerelease: true draft: false body: | ## 🧪 Beta Release v${{ steps.app_version.outputs.version }} ⚠️ **This is a BETA release.** For testing purposes – may contain bugs. --- ### 🔖 Build Information - **Channel:** Beta - **Version:** `${{ steps.app_version.outputs.version }}` - **Build Number:** `${{ steps.app_version.outputs.build }}` - **Flutter SDK:** `${{ steps.fvm_version.outputs.version }}` - **Branch:** `beta` --- ### 🧪 Testing Notes - Beta quality – expect some bugs - Please report issues on GitHub - Backup your data before testing files: | releases/gyawun-beta-v${{ steps.app_version.outputs.version }}-universal.apk releases/gyawun-beta-v${{ steps.app_version.outputs.version }}-arm64-v8a.apk releases/gyawun-beta-v${{ steps.app_version.outputs.version }}-armeabi-v7a.apk releases/gyawun-beta-v${{ steps.app_version.outputs.version }}-x86_64.apk env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/branch-protection-check.yml ================================================ name: Branch Protection Check on: pull_request: branches: - stable - beta jobs: check-pr-source: runs-on: ubuntu-latest steps: - name: Check PR to stable is from beta if: github.base_ref == 'stable' run: | if [ "${{ github.head_ref }}" != "beta" ]; then echo "❌ Error: Pull requests to 'stable' branch must come from 'beta' branch only." echo "Current source: ${{ github.head_ref }}" exit 1 fi echo "✅ PR is from beta branch - validation passed" - name: Info for beta PRs if: github.base_ref == 'beta' run: | echo "✅ Pull request to beta branch from: ${{ github.head_ref }}" echo "This will trigger a beta pre-release on merge." ================================================ FILE: .github/workflows/stable-release.yml ================================================ name: Stable Release on: push: tags: - 'v[0-9]*.[0-9]*.[0-9]*' jobs: stable-release: if: "!contains(github.ref, '-beta.')" runs-on: ubuntu-latest outputs: release_tag: ${{ steps.app_version.outputs.version }} release_channel: 'stable' steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate tag is on stable branch run: | if ! git branch -r --contains "${{ github.ref_name }}" | grep -q 'origin/stable'; then echo "❌ Tag ${{ github.ref_name }} is not on the stable branch" exit 1 fi echo "✅ Tag is on stable branch" - name: Read FVM version id: fvm_version run: | FVM_VERSION=$(jq -r '.flutterSdkVersion' .fvm/fvm_config.json) echo "version=$FVM_VERSION" >> $GITHUB_OUTPUT echo "Flutter version from FVM: $FVM_VERSION" - name: Read and validate production version id: app_version run: | TAG="${{ github.ref_name }}" # e.g. v1.2.0 VERSION="${TAG#v}" # e.g. 1.2.0 if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "❌ Tag must follow vX.Y.Z format (no beta suffix), got: $TAG" exit 1 fi VERSION_CODE=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f2) if [[ -z "$VERSION_CODE" || "$VERSION_CODE" == "0" ]]; then echo "❌ pubspec.yaml must have a valid build number (not 0)" exit 1 fi sed -i "s/^version:.*/version: $VERSION+$VERSION_CODE/" pubspec.yaml echo "version=$VERSION" >> $GITHUB_OUTPUT echo "build=$VERSION_CODE" >> $GITHUB_OUTPUT echo "✅ Stable version: $VERSION+$VERSION_CODE" - name: Setup Flutter with FVM uses: kuhnroyal/flutter-fvm-config-action/setup@v3 - name: Decode keystore run: | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks - name: Create key.properties run: | cat > android/key.properties << EOF storePassword=${{ secrets.STORE_PASSWORD }} keyPassword=${{ secrets.KEY_PASSWORD }} keyAlias=${{ secrets.KEY_ALIAS }} storeFile=keystore.jks EOF - name: Install dependencies run: flutter pub get - name: Generate Splash Screen run: dart run flutter_native_splash:create - name: Build Android APK (Stable - Universal) run: flutter build apk --flavor stable --release - name: Build Android APK (Stable - Split per ABI) run: flutter build apk --flavor stable --release --split-per-abi - name: Rename build artifacts run: | mkdir -p releases V=${{ steps.app_version.outputs.version }} cp build/app/outputs/flutter-apk/app-stable-release.apk \ releases/gyawun-v${V}-universal.apk cp build/app/outputs/flutter-apk/app-arm64-v8a-stable-release.apk \ releases/gyawun-v${V}-arm64-v8a.apk cp build/app/outputs/flutter-apk/app-armeabi-v7a-stable-release.apk \ releases/gyawun-v${V}-armeabi-v7a.apk cp build/app/outputs/flutter-apk/app-x86_64-stable-release.apk \ releases/gyawun-v${V}-x86_64.apk - name: Create Stable Release uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.ref_name }} name: v${{ steps.app_version.outputs.version }} prerelease: false draft: false body: | ## ✅ Stable Release v${{ steps.app_version.outputs.version }} **This is a stable production release.** --- ### 🔖 Build Information - **Channel:** Stable - **Version:** `${{ steps.app_version.outputs.version }}` - **Build Number:** `${{ steps.app_version.outputs.build }}` - **Flutter SDK:** `${{ steps.fvm_version.outputs.version }}` - **Branch:** `stable` --- ### 📝 Release Notes _See commit history for changes._ files: | releases/gyawun-v${{ steps.app_version.outputs.version }}-universal.apk releases/gyawun-v${{ steps.app_version.outputs.version }}-arm64-v8a.apk releases/gyawun-v${{ steps.app_version.outputs.version }}-armeabi-v7a.apk releases/gyawun-v${{ steps.app_version.outputs.version }}-x86_64.apk env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ **/Gyawun **/dist # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/key.properties **/android/**/GeneratedPluginRegistrant.java /linux /macos /windows /ios /web .flatpak-builder .secrets # FVM Version Cache .fvm/ repo ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - platform: linux create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Flutter (Beta Flavor)", "request": "launch", "type": "dart", "args": [ "--flavor", "beta" ] }, { "name": "Flutter (Production Flavor)", "request": "launch", "type": "dart", "args": [ "--flavor", "production" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "cmake.sourceDirectory": "/home/sheikh-haziq/Development/Jhelum/gyawun-app/linux", "dart.flutterSdkPath": ".fvm/versions/3.41.4" } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guide Thank you for your interest in contributing! This repository follows a **strict, staged branch workflow** to maintain stability while allowing controlled development and testing. Please read this document fully before opening a pull request. --- ## 📌 Repository Overview - **Project type:** Flutter application - **Primary maintainer:** Solo maintainer - **Default branch:** `main` --- ## 🌿 Branching Model This project uses **three long-lived branches**: ### `main` — Development - All **active development** happens here - Used by the maintainer for **manual testing** - May be unstable or incomplete - **All pull requests must target `main`** ✅ This is the **only branch contributors should use**. --- ### `beta` — Pre-release - Receives changes **only from `main`** - Used for wider testing and early access - Merging into `beta`: - Automatically publishes a **beta GitHub release** - Version format: `x.y.z-beta.n` 🚫 Contributors should **not** open PRs directly to `beta`. --- ### `stable` — Stable - Production-ready code only - Receives changes **only from `beta`** - Merging into `stable`: - Automatically publishes a **stable GitHub release** - Version format: `x.y.z` 🚫 Pull requests to `stable` are not accepted. --- ## 🔁 Contribution Workflow ### 1. Fork the Repository ```bash git clone https://github.com//.git cd git checkout main ``` --- ### 2. Create a Feature or Fix Branch ```bash git checkout -b feature/short-description # or git checkout -b fix/short-description ``` --- ### 3. Make Your Changes - Keep changes **focused and minimal** - Follow the existing project structure - Avoid unrelated refactors or dependency changes --- ### 4. Basic Checks Before opening a PR, ensure: ```bash flutter analyze flutter test ``` > Final testing is performed **manually by the maintainer** on the `main` branch. --- ### 5. Open a Pull Request - **Base branch:** `main` - **Compare branch:** your feature/fix branch - Clearly explain: - What the change does - Why it is needed - Any user-facing or breaking changes 🚫 PRs targeting `beta` or `stable` will be **closed without review**. --- ## 🚀 Release Flow (For Reference) | Action | Result | |------|-------| | Merge PR → `main` | Change queued for manual testing | | Merge `main` → `beta` | Beta release published | | Merge `beta` → `stable` | Stable release published | Contributors do **not** need to manage versions, tags, or releases. --- ## 🐞 Issues & Discussions - **Bug reports & feature requests:** GitHub Issues - **Questions & general discussion:** GitHub Discussions - Old or unstructured issues may be closed during maintenance cleanups --- ## 📐 Code Guidelines - Follow Flutter and Dart best practices - Prefer clarity over cleverness - Avoid introducing new dependencies without discussion - Platform-specific logic should be well-isolated --- ## 🔒 Maintainer Notes - This project is mantained by **Two maintainers** - Reviews and merges may take time - Large or architectural changes should be discussed **before** implementation --- ## ❤️ Thank You Every contribution—code, issues, or documentation—helps improve the project. Happy contributing! 🚀 ================================================ FILE: GyawunMusic-2.0.16.flatpak ================================================ [File too large to display: 12.5 MB] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================
![Gyawun Music Banner](cover.png) # 🎵 Gyawun Music **Where Music Knows No Bounds** *Experience the freedom of unlimited, ad-free music streaming from YouTube Music* [![GitHub release](https://img.shields.io/github/v/release/sheikhhaziq/gyawun_music?style=for-the-badge)](https://github.com/sheikhhaziq/gyawun_music/releases/latest) [![License](https://img.shields.io/badge/license-GPL--3.0-blue?style=for-the-badge)](LICENSE) [![Stars](https://img.shields.io/github/stars/sheikhhaziq/gyawun_music?style=for-the-badge)](https://github.com/sheikhhaziq/gyawun_music/stargazers) [![Downloads](https://img.shields.io/github/downloads/sheikhhaziq/gyawun_music/total?style=for-the-badge)](https://github.com/sheikhhaziq/gyawun_music/releases) [![Join us on Telegram](https://img.shields.io/badge/Join_us_on_Telegram-0088cc?style=for-the-badge&logo=telegram)](https://t.me/jhelumcorp) [Download](https://github.com/sheikhhaziq/gyawun_music/releases/latest) • [Website](https://gyawunmusic.vercel.app/) • [Contribute](CONTRIBUTING.md) • [Report Bug](https://github.com/sheikhhaziq/gyawun_music/issues) ---
## ✨ What Makes Gyawun Special Gyawun isn't just another music player—it's your gateway to an uninterrupted musical journey. Built with Flutter and powered by YouTube Music's vast library, Gyawun brings you millions of songs without the noise of advertisements, subscription walls, or unnecessary restrictions. Immerse yourself in crystal-clear audio quality, discover artists from every corner of the globe, and let the rhythm carry you wherever you go. With Gyawun, music flows freely, as it should. ## 🚀 Features ### Core Experience - 🎧 **Unlimited Streaming** - Access millions of songs from YouTube Music - 🚫 **Ad-Free Playback** - Enjoy uninterrupted listening, always - 🎼 **High-Quality Audio** - Adjustable audio quality to suit your preference - 🌙 **Background Playback** - Keep the music playing while you multitask - 💾 **Offline Downloads** - Save your favorite tracks for offline listening - ⏰ **Sleep Timer** - Fall asleep to your favorite music with auto-stop ### Discovery & Organization - 🔍 **Smart Search** - Find songs, artists, and playlists effortlessly - ❤️ **Favorites** - Build your personal collection of beloved tracks - 📝 **Custom Playlists** - Curate and organize music your way - 🔄 **Flexible Queue** - Reorder songs on the fly - 📜 **Listening History** - Never lose track of what you've played - 📝 **Lyrics** - Sing along with synchronized lyrics powered by LRCLib ### Personalization - 🎨 **Material You** - Dynamic theming that adapts to your style - 🌓 **Dark Mode** - Easy on the eyes, day or night - 🎚️ **Audio Enhancement** - Built-in equalizer and loudness enhancer - 🌍 **Multi-Language Support** - Available in multiple languages - 🔄 **Cross-Device Sync** - Sync recommendations across devices using visitor ID - 🎙️ **Podcast Support** - Stream your favorite podcasts alongside music ## 📱 Installation ### Android Download the latest APK from our [releases page](https://github.com/sheikhhaziq/gyawun_music/releases/latest) and install it on your device. ### Building from Source ```bash # Clone the repository git clone https://github.com/sheikhhaziq/gyawun_music.git cd gyawun_music # This project uses FVM for Flutter version management # Install FVM if you haven't already: https://fvm.app/docs/getting_started/installation # The Flutter version is specified in .fvmrc # Install the correct Flutter version fvm install # Use the project's Flutter version fvm use # Install dependencies fvm flutter pub get # Run the app fvm flutter run ``` ## 🎯 Roadmap We're constantly evolving. Here's what we're working on: - [ ] iOS Support - [ ] Desktop Applications (Windows, macOS, Linux) - [ ] Advanced Audio Controls - [ ] Social Features (Share playlists, collaborative playlists) - [ ] Import/Export Playlists - [ ] Enhanced Podcast Features ## 🤝 Contributing Gyawun is built by music lovers, for music lovers. We welcome contributions from developers, designers, translators, and enthusiasts of all skill levels. Whether you want to fix bugs, add features, improve documentation, or translate the app into your language, your contributions make Gyawun better for everyone. Please read our [Contributing Guidelines](CONTRIBUTING.md) to get started. ## 👥 Community Join our growing community of music enthusiasts: - 💬 [Telegram Group](https://t.me/jhelumcorp) - Chat, share music, and get support - 🐛 [Issue Tracker](https://github.com/sheikhhaziq/gyawun_music/issues) - Report bugs - 💡 [Discussions](https://github.com/sheikhhaziq/gyawun_music/discussions) - Request features, share ideas and feedback ## 🙏 Acknowledgments A heartfelt thank you to our incredible contributors who have helped shape Gyawun: [![Contributors](https://contrib.rocks/image?repo=sheikhhaziq/gyawun_music)](https://github.com/sheikhhaziq/gyawun_music/graphs/contributors) ## ⚖️ Legal **Disclaimer:** This project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way associated with YouTube, Google LLC, or any of its affiliates and subsidiaries. Any trademark, service mark, trade name, or other intellectual property rights used in this project are owned by the respective owners. Gyawun is an open-source project created for educational and personal use. Users are responsible for ensuring their usage complies with YouTube's Terms of Service and applicable laws in their jurisdiction. ## 📄 License This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. ---
**Made with ❤️ by the Gyawun community** *Let the music play on* [⬆ Back to Top](#-gyawun-music)
================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java .cxx/ # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle.kts ================================================ import java.io.FileInputStream import java.util.Properties plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("dev.flutter.flutter-gradle-plugin") } /* ---------- local.properties ---------- */ val localProperties = Properties() val localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { localPropertiesFile.inputStream().use { localProperties.load(it) } } /* ---------- key.properties ---------- */ val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("key.properties") if (keystorePropertiesFile.exists()) { FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } android { namespace = "com.jhelum.gyawun" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } defaultConfig { applicationId = "com.jhelum.gyawun" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } /* ---------- Flavors ---------- */ flavorDimensions += "default" productFlavors { create("beta") { dimension = "default" applicationIdSuffix = ".beta" resValue("string", "app_name", "Gyawun Music Beta") } create("stable") { dimension = "default" resValue("string", "app_name", "Gyawun Music") } } /* ---------- Signing ---------- */ signingConfigs { create("release") { keyAlias = keystoreProperties["keyAlias"] as String? keyPassword = keystoreProperties["keyPassword"] as String? storeFile = keystoreProperties["storeFile"]?.let { file(it) } storePassword = keystoreProperties["storePassword"] as String? } } buildTypes { getByName("release") { signingConfig = signingConfigs.getByName("release") ndk { debugSymbolLevel = "none" } } } packaging { jniLibs { useLegacyPackaging = true } } } flutter { source = "../.." } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/jhelum/gyawun/MainActivity.kt ================================================ package com.jhelum.gyawun import io.flutter.embedding.android.FlutterActivity class MainActivity : FlutterActivity() ================================================ FILE: android/app/src/main/res/anim/in_animation.xml ================================================ ================================================ FILE: android/app/src/main/res/anim/out_animation.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-hdpi/audio_service_stop.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-mdpi/audio_service_stop.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-night/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-xhdpi/audio_service_stop.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-xxhdpi/audio_service_stop.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-xxxhdpi/audio_service_stop.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/raw/keep.xml ================================================ ================================================ FILE: android/app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: android/app/src/main/res/values/colors.xml ================================================ #FFE1F5FE #FF81D4FA #FF039BE5 #FF01579B ================================================ FILE: android/app/src/main/res/values/dimens.xml ================================================ 0dp ================================================ FILE: android/app/src/main/res/values/ic_launcher_background.xml ================================================ #000000 ================================================ FILE: android/app/src/main/res/values/strings.xml ================================================ Play Add widget BlackHole Music ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values/themes.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night-v31/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-v31/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/automotive_app_desc.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build/reports/problems/problems-report.html ================================================ Gradle Configuration Cache
Loading...
================================================ FILE: android/build.gradle.kts ================================================ allprojects { repositories { google() mavenCentral() } } val newBuildDir: Directory = rootProject.layout.buildDirectory .dir("../../build") .get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean") { delete(rootProject.layout.buildDirectory) } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip org.gradle.jvmargs=--enable-native-access=ALL-UNNAMED ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true ================================================ FILE: android/settings.gradle.kts ================================================ pluginManagement { val flutterSdkPath = run { val properties = java.util.Properties() file("local.properties").inputStream().use { properties.load(it) } val flutterSdkPath = properties.getProperty("flutter.sdk") require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } flutterSdkPath } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.11.1" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") ================================================ FILE: build-flatpak.sh ================================================ #!/usr/bin/env bash set -euo pipefail APP_ID="com.gyawun.music" APP_NAME="GyawunMusic" VERSION="2.0.16" BUNDLE_DIR="build/linux/x64/release/bundle" OLD_BIN="gyawun" NEW_BIN="gmusic" MANIFEST="com.gyawun.music.yml" BUILD_DIR="build-dir" REPO_DIR="repo" OUT_FILE="${APP_NAME}-${VERSION}.flatpak" echo "▶ Building Flutter Linux release…" flutter build linux --release echo "▶ Verifying Flutter bundle…" if [[ ! -d "$BUNDLE_DIR" ]]; then echo "ERROR: Flutter bundle not found at $BUNDLE_DIR" exit 1 fi if [[ ! -f "$BUNDLE_DIR/$OLD_BIN" && ! -f "$BUNDLE_DIR/$NEW_BIN" ]]; then echo "ERROR: Neither '$OLD_BIN' nor '$NEW_BIN' found in bundle" exit 1 fi if [[ -f "$BUNDLE_DIR/$OLD_BIN" ]]; then echo "▶ Renaming binary: $OLD_BIN → $NEW_BIN" mv -f "$BUNDLE_DIR/$OLD_BIN" "$BUNDLE_DIR/$NEW_BIN" fi chmod +x "$BUNDLE_DIR/$NEW_BIN" echo "▶ Building Flatpak (clean)…" flatpak-builder --force-clean "$BUILD_DIR" "$MANIFEST" echo "▶ Exporting Flatpak repository…" rm -rf "$REPO_DIR" flatpak-builder --force-clean --repo="$REPO_DIR" "$BUILD_DIR" "$MANIFEST" echo "▶ Creating .flatpak bundle…" flatpak build-bundle \ "$REPO_DIR" \ "$OUT_FILE" \ "$APP_ID" \ --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo echo "✔ Done." echo "✔ Bundle created: $OUT_FILE" ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ Immerse yourself in the world of Gyawun, where music knows no bounds. Enjoy uninterrupted, ad-free streaming with an extensive library of songs spanning genres and artists from every corner of the globe. With Gyawun, the rhythm never fades. Download now and let the music carry you away. ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ Stream music from YouTube Music ================================================ FILE: fastlane/metadata/android/en-US/title.txt ================================================ Gyawun Music ================================================ FILE: flutter_native_splash.yaml ================================================ flutter_native_splash: image: assets/images/splash_icon.png background_image: "assets/images/background.png" branding: assets/images/branding.png android_12: image: assets/images/icon_512.png branding: assets/images/branding.png ================================================ FILE: lib/core/extensions/random_material_shape.dart ================================================ import 'dart:math' as math; import 'package:m3e_collection/m3e_collection.dart'; extension RandomMaterialShape on MaterialShapes { static List randomshapes = [ MaterialShapes.slanted, MaterialShapes.pill, MaterialShapes.arrow, MaterialShapes.fan, MaterialShapes.clover4Leaf, ]; static RoundedPolygon get random { final random = math.Random(); final index = random.nextInt(randomshapes.length); return randomshapes[index]; } } ================================================ FILE: lib/core/extensions/string_extensions.dart ================================================ extension StringCapitalization on String { String capitalize() { if (isEmpty) return this; return this[0].toUpperCase() + substring(1); } } ================================================ FILE: lib/core/models/app_config.dart ================================================ class AppConfig { final bool isBeta; final Uri stableReleasesUri; final Uri allReleasesUri; final String codeName; // e.g. 2.0.16 or 2.0.16-beta.3 AppConfig({ required this.isBeta, required this.stableReleasesUri, required this.allReleasesUri, required this.codeName, }); } ================================================ FILE: lib/core/utils/expressive_sheet.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; class ExpressiveSheetOption { final String label; final IconData? icon; final T value; final bool selected; const ExpressiveSheetOption({ required this.label, required this.value, this.icon, this.selected = false, }); } class ExpressiveSheet { /// Shows a modal bottom sheet with a list of options. /// Returns the value of the selected option, or null if dismissed. static Future showSelection( BuildContext context, { required String title, required List> options, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), builder: (context) { return DraggableScrollableSheet( expand: false, builder: (context, controller) { return ListView( controller: controller, padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), children: [ Padding( padding: const EdgeInsets.only(bottom: 24, left: 8, right: 8), child: Text( title, style: theme.textTheme.headlineSmall, textAlign: TextAlign.center, ), ), ...options.map((option) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: ExpressiveListTile( title: Text(option.label), leading: option.icon != null ? Icon(option.icon) : null, trailing: option.selected ? Icon(FluentIcons.checkmark_24_filled) : null, onTap: () { Navigator.pop(context, option.value); }, ), ); }), ], ); }, ); }, ); } /// Shows a modal bottom sheet with a color selection grid. /// Returns the selected color, or null if dismissed/cancelled. static Future showColorSelection( BuildContext context, { required String title, Color? currentColor, VoidCallback? onReset, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; // Preset M3-style colors final List presets = [ Colors.red, Colors.pink, Colors.purple, Colors.deepPurple, Colors.indigo, Colors.blue, Colors.lightBlue, Colors.cyan, Colors.teal, Colors.green, Colors.lightGreen, Colors.lime, Colors.yellow, Colors.amber, Colors.orange, Colors.deepOrange, Colors.brown, Colors.grey, Colors.blueGrey, const Color(0xFF000000), ]; Color? selectedColor = currentColor; return showModalBottomSheet( context: context, isScrollControlled: false, showDragHandle: true, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), builder: (context) { return StatefulBuilder( builder: (context, setState) { return Column( children: [ Padding( padding: const EdgeInsets.only( bottom: 16, left: 16, right: 16, ), child: Text( title, style: theme.textTheme.headlineSmall, textAlign: TextAlign.center, ), ), Expanded( child: GridView.builder( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 8, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, crossAxisSpacing: 16, mainAxisSpacing: 16, ), itemCount: presets.length, itemBuilder: (context, index) { final color = presets[index]; final isSelected = selectedColor?.value == color.value; return GestureDetector( onTap: () { setState(() => selectedColor = color); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: isSelected ? Border.all( color: colorScheme.onSurface, width: 2.5, ) : Border.all( color: colorScheme.outline.withValues( alpha: 0.1, ), width: 1, ), boxShadow: isSelected ? [ BoxShadow( color: color.withValues(alpha: 0.4), blurRadius: 8, spreadRadius: 1, ), ] : null, ), child: isSelected ? Icon( Icons.check, size: 20, color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, ) : null, ), ); }, ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (onReset != null) TextButton( onPressed: () { onReset(); Navigator.pop(context, null); }, child: const Text("Reset"), ), const Spacer(), TextButton( onPressed: () => Navigator.pop(context), child: const Text("Cancel"), ), const SizedBox(width: 12), FilledButton( onPressed: () => Navigator.pop(context, selectedColor), child: const Text("Done"), ), ], ), ), ], ); }, ); }, ); } } ================================================ FILE: lib/core/utils/service_locator.dart ================================================ import 'package:get_it/get_it.dart'; final sl = GetIt.I; ================================================ FILE: lib/core/widgets/expressive_app_bar.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; class ExpressiveAppBar extends StatelessWidget { const ExpressiveAppBar({ super.key, this.title, this.child, this.hasLeading = false, this.actions, }); final bool hasLeading; final String? title; final Widget? child; final List? actions; @override Widget build(BuildContext context) { return SliverAppBar( pinned: true, expandedHeight: 120, actions: actions, flexibleSpace: hasLeading ? LayoutBuilder( builder: (context, constraints) { final maxHeight = 120.0; final t = (constraints.maxHeight / (maxHeight + 30)).clamp( 0.0, 1.0, ); final paddingLeft = lerpDouble(100, 16, t)!; return _ExpressiveFlexSpaceBar( paddingLeft: paddingLeft, title: title, child: child, ); }, ) : _ExpressiveFlexSpaceBar( paddingLeft: 16, title: title, child: child, ), ); } } class _ExpressiveFlexSpaceBar extends StatelessWidget { const _ExpressiveFlexSpaceBar({ required this.paddingLeft, this.title, this.child, }); final double paddingLeft; final String? title; final Widget? child; @override Widget build(BuildContext context) { return FlexibleSpaceBar( titlePadding: EdgeInsets.only(left: paddingLeft, bottom: 12), title: child ?? Text( title ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.headlineSmall?.copyWith(fontWeight: .w600), ), ); } } ================================================ FILE: lib/core/widgets/expressive_list_group.dart ================================================ import 'package:flutter/material.dart'; class ExpressiveListGroupScope extends InheritedWidget { const ExpressiveListGroupScope({super.key, required super.child}); static ExpressiveListGroupScope? of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType(); } @override bool updateShouldNotify(ExpressiveListGroupScope oldWidget) => false; } class ExpressiveListGroup extends StatelessWidget { final List children; final String? title; final Widget? header; const ExpressiveListGroup({ super.key, required this.children, this.title, this.header, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final backgroundColor = colorScheme.surfaceContainerHigh; final borderRadius = BorderRadius.circular(24); Widget? headerWidget = header; if (headerWidget == null && title != null) { headerWidget = Padding( padding: const EdgeInsets.only(left: 16, bottom: 8, top: 8), child: Text( title!, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (headerWidget != null) headerWidget, ExpressiveListGroupScope( child: Container( decoration: BoxDecoration( color: backgroundColor.withValues(alpha: 0.5), borderRadius: borderRadius, ), clipBehavior: Clip.hardEdge, child: Column(children: _buildChildrenWithDividers(context)), ), ), ], ); } List _buildChildrenWithDividers(BuildContext context) { final List items = []; final colorScheme = Theme.of(context).colorScheme; for (int i = 0; i < children.length; i++) { items.add(children[i]); // Add Divider if not the last item if (i < children.length - 1) { items.add( Divider( height: 1, thickness: 1, indent: 76, endIndent: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.4), ), ); } } return items; } } ================================================ FILE: lib/core/widgets/expressive_list_tile.dart ================================================ import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; class ExpressiveListTile extends StatelessWidget { final Widget title; final Widget? subtitle; final Widget? leading; final Widget? trailing; final VoidCallback? onTap; final VoidCallback? onLongPress; final bool selected; final bool enableFeedback; final BorderRadiusGeometry? borderRadius; final Color? fillColor; const ExpressiveListTile({ super.key, required this.title, this.subtitle, this.leading, this.trailing, this.onTap, this.onLongPress, this.selected = false, this.enableFeedback = true, this.borderRadius, this.fillColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; // Check if we are inside an ExpressiveListGroup final isInGroup = ExpressiveListGroupScope.of(context) != null; // Determine default values based on context (Standalone vs Grouped) final effectiveBorderRadius = borderRadius ?? (isInGroup ? BorderRadius.zero : BorderRadius.circular(24)); final effectiveFillColor = fillColor ?? (isInGroup ? Colors.transparent : colorScheme.surfaceContainerHigh); // Colors final selectedColor = colorScheme.secondaryContainer; final baseColor = selected ? selectedColor : effectiveFillColor; // Overlay colors for InkWell (M3 specs) final hoverColor = colorScheme.onSurface.withValues(alpha: 0.08); final highlightColor = colorScheme.onSurface.withValues(alpha: 0.12); final splashColor = colorScheme.onSurface.withValues(alpha: 0.12); return AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeOut, decoration: BoxDecoration( color: baseColor, borderRadius: effectiveBorderRadius, ), child: Material( type: MaterialType.transparency, child: InkWell( onTap: onTap, onLongPress: onLongPress, borderRadius: effectiveBorderRadius.resolve( Directionality.of(context), ), hoverColor: hoverColor, highlightColor: highlightColor, splashColor: splashColor, enableFeedback: enableFeedback, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ // Leading if (leading != null) ...[ IconTheme( data: IconThemeData( color: selected ? colorScheme.onSecondaryContainer : colorScheme.onSurfaceVariant, size: 24, ), child: leading!, ), const SizedBox(width: 16), ], // Text Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ DefaultTextStyle( style: theme.textTheme.bodyLarge!.copyWith( color: selected ? colorScheme.onSecondaryContainer : colorScheme.onSurface, fontWeight: FontWeight.w600, ), child: title, ), if (subtitle != null) ...[ const SizedBox(height: 2), DefaultTextStyle( style: theme.textTheme.bodyMedium!.copyWith( color: selected ? colorScheme.onSecondaryContainer.withValues( alpha: 0.8, ) : colorScheme.onSurfaceVariant, fontSize: 13, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, child: subtitle!, ), ], ], ), ), // Trailing if (trailing != null) ...[ const SizedBox(width: 16), IconTheme( data: IconThemeData( color: colorScheme.onSurfaceVariant, size: 24, ), child: trailing!, ), ], ], ), ), ), ), ); } } ================================================ FILE: lib/core/widgets/expressive_switch_list_tile.dart ================================================ import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; class ExpressiveSwitchListTile extends StatelessWidget { final bool value; final ValueChanged? onChanged; final Widget title; final Widget? subtitle; final Widget? leading; final bool enableFeedback; final VoidCallback? onLongPress; final bool selected; const ExpressiveSwitchListTile({ super.key, required this.value, required this.onChanged, required this.title, this.subtitle, this.leading, this.enableFeedback = true, this.onLongPress, this.selected = false, }); @override Widget build(BuildContext context) { return ExpressiveListTile( title: title, subtitle: subtitle, leading: leading, onTap: onChanged != null ? () => onChanged!(!value) : null, onLongPress: onLongPress, enableFeedback: enableFeedback, selected: selected, trailing: Switch(value: value, onChanged: onChanged), ); } } ================================================ FILE: lib/core/widgets/internet_guard.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/themes/colors.dart'; import 'package:gyawun/utils/adaptive_widgets/buttons.dart'; import 'package:gyawun/utils/adaptive_widgets/icons.dart'; class InternetGuard extends StatefulWidget { final Widget child; final VoidCallback? onConnectivityRestored; const InternetGuard({ super.key, required this.child, this.onConnectivityRestored, }); @override State createState() => _InternetGuardState(); } class _InternetGuardState extends State { final Connectivity _connectivity = Connectivity(); bool _isOnline = true; bool _wasOffline = false; StreamSubscription? _subscription; @override void initState() { super.initState(); _initConnectivity(); } @override void dispose() { _subscription?.cancel(); super.dispose(); } Future _initConnectivity() async { // Initial check final result = await _connectivity.checkConnectivity(); _updateStatus(result); // Listen for changes _subscription = _connectivity.onConnectivityChanged.listen(_updateStatus); } void _updateStatus(dynamic value) { final bool online = !_isOffline(value); if (online != _isOnline && mounted) { setState(() { if (!online) { _wasOffline = true; } if (online && _wasOffline) { _wasOffline = false; widget.onConnectivityRestored?.call(); } _isOnline = online; }); } } bool _isOffline(dynamic value) { if (value is ConnectivityResult) { return value == ConnectivityResult.none; } if (value is List) { return value.contains(ConnectivityResult.none); } return true; } Future _retry() async { final result = await _connectivity.checkConnectivity(); _updateStatus(result); } @override Widget build(BuildContext context) { if (_isOnline) { return widget.child; } return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( AdaptiveIcons.wifi_off_rounded, size: 80, color: greyColor, ), const SizedBox(height: 20), Text( S.of(context).No_Internet_Connection, textAlign: TextAlign.center, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), AdaptiveFilledButton( onPressed: () => context.go('/library/downloads'), child: Text(S.of(context).Go_To_Downloads), ), const SizedBox(height: 20), OutlinedButton.icon( icon: const Icon(Icons.refresh), label: Text(S.of(context).Retry), onPressed: _retry, ), ], ), ), ); } } ================================================ FILE: lib/core/widgets/library_tile.dart ================================================ import 'package:flutter/material.dart'; class LibraryTile extends StatelessWidget { const LibraryTile({ this.title, this.leading, this.subtitle, this.trailing, this.onTap, this.onLongPress, this.isFirst = true, this.isLast = true, super.key, }); final Widget? title; final Widget? leading; final Widget? subtitle; final Widget? trailing; final void Function()? onTap; final void Function()? onLongPress; final bool isFirst; final bool isLast; @override Widget build(BuildContext context) { return Material( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadiusGeometry.only( topLeft: Radius.circular(isFirst ? 20 : 4), topRight: Radius.circular(isFirst ? 20 : 4), bottomLeft: Radius.circular(isLast ? 20 : 4), bottomRight: Radius.circular(isLast ? 20 : 4), ), child: InkWell( onTap: onTap, onLongPress: onLongPress, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ if (leading != null) ...[leading!, const SizedBox(width: 16)], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (title != null) DefaultTextStyle( style: Theme.of(context).textTheme.bodyLarge!, child: title!, ), if (subtitle != null) ...[ const SizedBox(height: 4), DefaultTextStyle( style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).textTheme.bodySmall!.color, ), child: subtitle!, ), ], ], ), ), if (trailing != null) ...[const SizedBox(width: 16), trailing!], ], ), ), ), ); } } ================================================ FILE: lib/core/widgets/rounded_polygon_icon.dart ================================================ import 'package:flutter/material.dart'; import 'package:m3e_collection/m3e_collection.dart'; class RoundedPolygonIcon extends StatelessWidget { final RoundedPolygon polygon; final Color? color; final double size; const RoundedPolygonIcon({ super.key, required this.polygon, this.color, this.size = 24.0, }); @override Widget build(BuildContext context) { final iconColor = color ?? Theme.of(context).iconTheme.color ?? Colors.black; return CustomPaint( size: Size(size, size), painter: _RoundedPolygonPainter( polygon: polygon, color: iconColor, ), ); } } class _RoundedPolygonPainter extends CustomPainter { final RoundedPolygon polygon; final Color color; _RoundedPolygonPainter({ required this.polygon, required this.color, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; final path = polygon.toPath(); // Get the bounds of the polygon final bounds = path.getBounds(); // Scale and center the polygon to fit the icon size final scaleX = size.width / bounds.width; final scaleY = size.height / bounds.height; final scale = scaleX < scaleY ? scaleX : scaleY; canvas.save(); canvas.translate( size.width / 2 - bounds.center.dx * scale, size.height / 2 - bounds.center.dy * scale, ); canvas.scale(scale); canvas.drawPath(path, paint); canvas.restore(); } @override bool shouldRepaint(_RoundedPolygonPainter oldDelegate) { return oldDelegate.polygon != polygon || oldDelegate.color != color; } } ================================================ FILE: lib/core/widgets/section_item.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/sections/section_multi_column.dart'; import 'package:gyawun/core/widgets/sections/section_row.dart'; import 'package:gyawun/core/widgets/tiles/section_list_tile.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:yt_music/ytmusic.dart'; import '../../generated/l10n.dart'; import '../../services/bottom_message.dart'; import '../../utils/adaptive_widgets/adaptive_widgets.dart'; import '../../services/media_player.dart'; class SectionItem extends StatefulWidget { const SectionItem({required this.section, this.isMore = false, super.key}); final Map section; final bool isMore; @override State createState() => _SectionItemState(); } class _SectionItemState extends State { final ScrollController horizontalScrollController = ScrollController(); PageController horizontalPageController = PageController(); bool loadingMore = false; @override void initState() { super.initState(); } @override void dispose() { horizontalPageController.dispose(); horizontalScrollController.dispose(); super.dispose(); } void loadMoreItems() { if (widget.section['continuation'] != null) { setState(() { loadingMore = true; }); GetIt.I() .getMoreItems(continuation: widget.section['continuation']) .then((value) { setState(() { widget.section['contents'].addAll(value['items']); widget.section['continuation'] = value['continuation']; loadingMore = false; }); }); } } @override Widget build(BuildContext context) { horizontalPageController = PageController( viewportFraction: 350 / MediaQuery.of(context).size.width, ); return widget.section['contents'].isEmpty ? const SizedBox() : Column( children: [ if (widget.section['title'] != null) SectionHeader( title: widget.section['title'], trailing: widget.section['trailing'], contents: widget.section['contents'], ), if (widget.section['viewType'] == 'COLUMN' && !widget.isMore) SectionMultiColumn(items: widget.section['contents']) else if (widget.section['viewType'] == 'SINGLE_COLUMN' || widget.isMore) SingleColumnList(songs: widget.section['contents']) else SectionRow(items: widget.section['contents']), if (loadingMore) const ExpressiveLoadingIndicator(), if (widget.section['continuation'] != null && !loadingMore) AdaptiveButton( onPressed: loadMoreItems, child: const Text("Load More"), ), ], ); } } class SectionHeader extends StatelessWidget { const SectionHeader({ super.key, required this.title, required this.trailing, required this.contents, }); final String title; final Map? trailing; final List contents; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( title, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w700, fontSize: 18, ), ), ), if (trailing != null) TextButton.icon( iconAlignment: IconAlignment.end, label: Text(trailing!['text']), icon: const Icon(FluentIcons.play_24_filled), onPressed: () async { if (trailing!['playable'] == false && trailing!['endpoint'] != null) { context.push( '/browse', extra: {'endpoint': trailing!['endpoint'], 'isMore': true}, ); } else { BottomMessage.showText( context, S.of(context).Songs_Will_Start_Playing_Soon, ); if (trailing!['endpoint'] != null) { await GetIt.I().startPlaylistSongs( trailing!['endpoint'], ); } else { await GetIt.I().playAll(contents); } } }, ), ], ), ); } } class SingleColumnList extends StatelessWidget { const SingleColumnList({required this.songs, super.key}); final List songs; @override Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: songs.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 4), child: SectionListTile( item: songs[index], isFirst: index == 0, isLast: index == (songs.length - 1), ), ); }, ); } } ================================================ FILE: lib/core/widgets/sections/section_multi_column.dart ================================================ import 'dart:math'; import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/tiles/section_list_tile.dart'; class SectionMultiColumn extends StatefulWidget { const SectionMultiColumn({super.key, required this.items, this.maxItem}); final List items; final int? maxItem; @override State createState() => _SectionMultiColumnState(); } class _SectionMultiColumnState extends State { late PageController controller; @override void didChangeDependencies() { super.didChangeDependencies(); controller = PageController( viewportFraction: min(450, MediaQuery.sizeOf(context).width - 50) / MediaQuery.sizeOf(context).width, ); } @override Widget build(BuildContext context) { final num = widget.maxItem ?? (widget.items.length <= 5 ? widget.items.length : 4); var pages = widget.items.length ~/ num; pages = (pages * num) < widget.items.length ? pages + 1 : pages; return ExpandablePageView.builder( controller: controller, padEnds: false, itemCount: pages, itemBuilder: (context, index) { int start = index * num; int end = start + num; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: widget.items.sublist(start, min(end, widget.items.length)).indexed.map(( entry, ) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 4), child: SectionListTile( item: entry.$2, isFirst: entry.$1 == 0, isLast: entry.$1 == (num - 1) || entry.$2 == widget.items.last, ), ); }).toList(), ), ); }, ); } } ================================================ FILE: lib/core/widgets/sections/section_row.dart ================================================ import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/tiles/section_row_tile.dart'; class SectionRow extends StatelessWidget { const SectionRow({super.key,required this.items}); final List items; @override Widget build(BuildContext context) { return SizedBox( // height: context.isWideScreen ? 270 : 216, height: 216, child: ListView.separated( addAutomaticKeepAlives: false, padding: const EdgeInsets.symmetric(horizontal: 8), scrollDirection: Axis.horizontal, itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(width: 4), itemBuilder: (context, index) { final item = items[index]; return SectionRowTile(item: item); }, ), ); } } ================================================ FILE: lib/core/widgets/song_tile.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/library_tile.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/bottom_modals.dart'; import '../../utils/song_thumbnail.dart'; class SongTile extends StatelessWidget { const SongTile({ required this.song, this.playlistId, this.onTap, this.onLongPress, this.icon, this.onIconPress, this.isFirst = true, this.isLast = true, super.key, }); final Map song; final String? playlistId; final Function? onTap; final Function? onLongPress; final IconData? icon; final Function? onIconPress; final bool isFirst; final bool isLast; void _onTap(BuildContext context, Map song) async { if (song['endpoint'] != null && song['videoId'] == null) { context.push('/browse', extra: {'endpoint': song['endpoint']}); } else { await GetIt.I().playSong(Map.from(song)); } } void _onLongPress(BuildContext context, Map song) { if (song['videoId'] != null) { Modals.showSongBottomModal(context, song); } } void _onIconPress(BuildContext context, Map song) { if (song['videoId'] != null) { Modals.showSongBottomModal(context, song); } } @override Widget build(BuildContext context) { double height = (song['aspectRatio'] != null ? 50 / song['aspectRatio'] : 50) .toDouble(); return LibraryTile( onTap: () => (onTap ?? _onTap)(context, song), onLongPress: () => (onLongPress ?? _onLongPress)(context, song), title: Text( song['title'] ?? "", maxLines: 1, style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), ), leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: SongThumbnail( song: song, height: height, width: 50, fit: BoxFit.cover, ), ), subtitle: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (song['explicit'] == true) Padding( padding: const EdgeInsets.only(right: 2), child: Icon( Icons.explicit, size: 18, color: Colors.grey.withValues(alpha: 0.9), ), ), Expanded( child: Text( song['subtitle'] ?? song['artists']?.map((e) => e['name'])?.join(',') ?? '', maxLines: 1, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface.withAlpha(150), ), overflow: TextOverflow.ellipsis, ), ), ], ), trailing: song['videoId'] == null ? null : IconButton.filledTonal( onPressed: () => (onIconPress ?? _onIconPress)(context, song), icon: Icon(icon ?? FluentIcons.more_vertical_24_filled), ), isFirst: isFirst, isLast: isLast, ); } } ================================================ FILE: lib/core/widgets/tiles/section_list_tile.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/bottom_modals.dart'; class SectionListTile extends StatelessWidget { const SectionListTile({ super.key, required this.item, this.onTap, this.items, this.isFirst = false, this.isLast = false, }); final Map item; final List? items; final void Function()? onTap; final bool isFirst; final bool isLast; @override Widget build(BuildContext context) { final pixelRatio = MediaQuery.devicePixelRatioOf(context); final thumbnail = item['thumbnails'][0]; return Material( color: Colors.transparent, elevation: 0, shadowColor: Colors.transparent, surfaceTintColor: Colors.transparent, child: ListTile( onTap: () async { if (item['endpoint'] != null && item['videoId'] == null) { context.push('/browse', extra: {'endpoint': item['endpoint']}); } else { await GetIt.I().playSong(Map.from(item)); } }, onLongPress: () { if (item['videoId'] != null) { Modals.showSongBottomModal(context, item); } }, enableFeedback: true, shape: RoundedRectangleBorder( borderRadius: BorderRadiusGeometry.only( topLeft: Radius.circular(isFirst ? 20 : 4), topRight: Radius.circular(isFirst ? 20 : 4), bottomLeft: Radius.circular(isLast ? 20 : 4), bottomRight: Radius.circular(isLast ? 20 : 4), ), ), tileColor: Theme.of(context).colorScheme.surfaceContainer, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: thumbnail?['url'] == null ? null : SizedBox( width: 50, height: 50, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular( item['type'] == 'ARTIST' ? ((50 * pixelRatio).round() / 2) : 8, ), image: DecorationImage( image: CachedNetworkImageProvider( thumbnail!['url'], maxHeight: (50 * pixelRatio).round(), maxWidth: (50 * pixelRatio).round(), ), fit: BoxFit.fitWidth, ), ), ), ), title: Text( item['title'], maxLines: 1, style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), ), subtitle: Text( item['subtitle'] ?? item['artists']?.map((e) => e['name'])?.join(',') ?? '', maxLines: 1, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface.withAlpha(150), ), overflow: TextOverflow.ellipsis, ), trailing: item['videoId'] != null ? IconButton( onPressed: () { Modals.showSongBottomModal(context, item); }, icon: const Icon(Icons.more_vert_rounded), ) : Icon(FluentIcons.chevron_right_24_filled), ), ); } } ================================================ FILE: lib/core/widgets/tiles/section_row_tile.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/bottom_modals.dart'; class SectionRowTile extends StatelessWidget { const SectionRowTile({super.key, required this.item}); final Map item; @override Widget build(BuildContext context) { final pixelRatio = MediaQuery.devicePixelRatioOf(context); final imageHeight = 150; final isHorizontal = item['aspectRatio'] != null && item['aspectRatio'] != 1; final imageWidth = (isHorizontal ? imageHeight * (16 / 9) : imageHeight) .toInt(); final thumbnail = (item['thumbnails'] as List).length > 2 ? item['thumbnails'][1]['url'] : item['thumbnails'][0]['url']; return Material( color: Colors.transparent, elevation: 0, shadowColor: Colors.transparent, surfaceTintColor: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), enableFeedback: true, onTap: () async { if (item['endpoint'] != null && item['videoId'] == null) { context.push('/browse', extra: {'endpoint': item['endpoint']}); } else { await GetIt.I().playSong(Map.from(item)); } }, onLongPress: () { if (item['videoId'] != null) { Modals.showSongBottomModal(context, item); } else if (item['playlistId'] != null) { Modals.showPlaylistBottomModal(context, item); } }, onSecondaryTap: () { if (item['videoId'] != null) { Modals.showSongBottomModal(context, item); } else if (item['playlistId'] != null) { Modals.showPlaylistBottomModal(context, item); } }, child: RepaintBoundary( child: SizedBox( height: 216, width: imageWidth.toDouble() + 16, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Ink( height: imageHeight.toDouble(), width: imageWidth.toDouble(), decoration: BoxDecoration( borderRadius: BorderRadius.circular( (item['type'] == 'ARTIST') ? (imageWidth * pixelRatio) : 8, ), image: DecorationImage( image: CachedNetworkImageProvider( thumbnail, maxHeight: (imageHeight * pixelRatio).round(), maxWidth: (imageWidth * pixelRatio).round(), ), fit: BoxFit.fill, ), ), ), Text( item['title'], maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w700, ), ), if (item['subtitle'] != null && item['subtitle']!.isNotEmpty) Text( item['subtitle']!, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Theme.of( context, ).colorScheme.onSurface.withAlpha(150), ), ), ], ), ), ), ), ), ); } } ================================================ FILE: lib/generated/intl/messages_all.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that looks up messages for specific locales by // delegating to the appropriate library. // Ignore issues from commonly used lints in this file. // ignore_for_file:implementation_imports, file_names, unnecessary_new // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering // ignore_for_file:argument_type_not_assignable, invalid_assignment // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases // ignore_for_file:comment_references import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; import 'messages_en.dart' as messages_en; import 'messages_es.dart' as messages_es; import 'messages_fr.dart' as messages_fr; import 'messages_hi.dart' as messages_hi; import 'messages_it.dart' as messages_it; import 'messages_tr.dart' as messages_tr; import 'messages_ur.dart' as messages_ur; typedef Future LibraryLoader(); Map _deferredLibraries = { 'en': () => new SynchronousFuture(null), 'es': () => new SynchronousFuture(null), 'fr': () => new SynchronousFuture(null), 'hi': () => new SynchronousFuture(null), 'it': () => new SynchronousFuture(null), 'tr': () => new SynchronousFuture(null), 'ur': () => new SynchronousFuture(null), }; MessageLookupByLibrary? _findExact(String localeName) { switch (localeName) { case 'en': return messages_en.messages; case 'es': return messages_es.messages; case 'fr': return messages_fr.messages; case 'hi': return messages_hi.messages; case 'it': return messages_it.messages; case 'tr': return messages_tr.messages; case 'ur': return messages_ur.messages; default: return null; } } /// User programs should call this before using [localeName] for messages. Future initializeMessages(String localeName) { var availableLocale = Intl.verifiedLocale( localeName, (locale) => _deferredLibraries[locale] != null, onFailure: (_) => null, ); if (availableLocale == null) { return new SynchronousFuture(false); } var lib = _deferredLibraries[availableLocale]; lib == null ? new SynchronousFuture(false) : lib(); initializeInternalMessageLookup(() => new CompositeMessageLookup()); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); return new SynchronousFuture(true); } bool _messagesExistFor(String locale) { try { return _findExact(locale) != null; } catch (e) { return false; } } MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { var actualLocale = Intl.verifiedLocale( locale, _messagesExistFor, onFailure: (_) => null, ); if (actualLocale == null) return null; return _findExact(actualLocale); } ================================================ FILE: lib/generated/intl/messages_en.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a en locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; static String m0(count) => "Queued (${count})"; static String m1(count) => "${Intl.plural(count, zero: 'No Songs', one: '1 Song', other: '${count} Songs')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("About"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "Add To Favourites", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage("Add To Library"), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage("Add To Playlist"), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("Add To Queue"), "Album": MessageLookupByLibrary.simpleMessage("Album"), "Albums": MessageLookupByLibrary.simpleMessage("Albums"), "App_Folder": MessageLookupByLibrary.simpleMessage("App Folder"), "Appearence": MessageLookupByLibrary.simpleMessage("Appearence"), "Artists": MessageLookupByLibrary.simpleMessage("Artists"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "Audio and Playback", ), "Autofetch_Songs": MessageLookupByLibrary.simpleMessage( "Autoplay Similar Songs", ), "Backup": MessageLookupByLibrary.simpleMessage("Backup"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "Backup and Restore", ), "Backup_Failed": MessageLookupByLibrary.simpleMessage( "Failed to back up Data", ), "Backup_Success": MessageLookupByLibrary.simpleMessage( "Backed up successfully at", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "Click here disable battery optimisation for Gyawun to work properly", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "Battery Optimisation Detected", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("Bug Report"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage("Buy me a Coffee"), "Cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "Check for Update", ), "Confirm": MessageLookupByLibrary.simpleMessage("Confirm"), "Confirm_Delete_All_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to delete them?", ), "Content": MessageLookupByLibrary.simpleMessage("Content"), "Contributors": MessageLookupByLibrary.simpleMessage("Contributors"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "Copied to Clipboard", ), "Country": MessageLookupByLibrary.simpleMessage("Country"), "Create": MessageLookupByLibrary.simpleMessage("Create"), "Create_Playlist": MessageLookupByLibrary.simpleMessage("Create Playlist"), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "Download Quality", ), "Delete_All_Songs": MessageLookupByLibrary.simpleMessage( "Delete All Songs", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to delete this item?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "Delete Playback History", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to delete Playback History.", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "Delete Search History", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to delete Search History.", ), "Deleting_Songs": MessageLookupByLibrary.simpleMessage("Deleting Songs..."), "Developer": MessageLookupByLibrary.simpleMessage("Developer"), "Donate": MessageLookupByLibrary.simpleMessage("Donate"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "Support the development of Gyawun", ), "Done": MessageLookupByLibrary.simpleMessage("Done"), "Download": MessageLookupByLibrary.simpleMessage("Download"), "Download_Started": MessageLookupByLibrary.simpleMessage( "Download started...", ), "Downloading": MessageLookupByLibrary.simpleMessage("Downloading"), "Downloads": MessageLookupByLibrary.simpleMessage("Downloads"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("Dynamic Colors"), "Edit": MessageLookupByLibrary.simpleMessage("Edit"), "Edit_Playlist": MessageLookupByLibrary.simpleMessage("Edit Playlist"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "Enable Equalizer", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "Enable Playback History", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "Enable Search History", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Enter Visitor Id", ), "Equalizer": MessageLookupByLibrary.simpleMessage("Equalizer"), "Favourites": MessageLookupByLibrary.simpleMessage("Favourites"), "Feature_Request": MessageLookupByLibrary.simpleMessage("Feature Request"), "File_Not_Found": MessageLookupByLibrary.simpleMessage("File not found"), "Go_To_Downloads": MessageLookupByLibrary.simpleMessage("Go to Downloads"), "Google_Account": MessageLookupByLibrary.simpleMessage("Google Account"), "Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun"), "High": MessageLookupByLibrary.simpleMessage("High"), "History": MessageLookupByLibrary.simpleMessage("History"), "Home": MessageLookupByLibrary.simpleMessage("Home"), "Import": MessageLookupByLibrary.simpleMessage("Import"), "Import_Playlist": MessageLookupByLibrary.simpleMessage("Import Playlist"), "In_Progress": MessageLookupByLibrary.simpleMessage("In Progress"), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("Jhelum Corp"), "Language": MessageLookupByLibrary.simpleMessage("Language"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "Loudness And Equalizer", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage( "Loudness Enhancer", ), "Low": MessageLookupByLibrary.simpleMessage("Low"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage("Made in Kashmir"), "Name": MessageLookupByLibrary.simpleMessage("Name"), "Next_Up": MessageLookupByLibrary.simpleMessage("Next Up"), "No": MessageLookupByLibrary.simpleMessage("No"), "No_Internet_Connection": MessageLookupByLibrary.simpleMessage( "No Internet Connection", ), "No_Offline_Songs": MessageLookupByLibrary.simpleMessage( "No offline songs available", ), "Organisation": MessageLookupByLibrary.simpleMessage("Organisation"), "Other_Results": MessageLookupByLibrary.simpleMessage("Other Results"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage("Pay with UPI"), "Payment_Methods": MessageLookupByLibrary.simpleMessage("Payment Methods"), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "Personalised Content", ), "Play_All": MessageLookupByLibrary.simpleMessage("Play All"), "Play_Next": MessageLookupByLibrary.simpleMessage("Play Next"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "Playback History Deleted", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("Playlist Name"), "Playlist_Not_Available": MessageLookupByLibrary.simpleMessage( "Playlist not available", ), "Playlists": MessageLookupByLibrary.simpleMessage("Playlists"), "Progress": MessageLookupByLibrary.simpleMessage("Progress"), "Queued": MessageLookupByLibrary.simpleMessage("Queued"), "Queued_Count": m0, "Remove": MessageLookupByLibrary.simpleMessage("Remove"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to clear all history?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "Remove From Favourites", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "Remove From Library", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to remove it from YTMusic?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "Are you sure you want to remove it?", ), "Rename": MessageLookupByLibrary.simpleMessage("Rename"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage("Rename Playlist"), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Reset Visitor Id", ), "Restore": MessageLookupByLibrary.simpleMessage("Restore"), "Restore_Failed": MessageLookupByLibrary.simpleMessage( "Failed to restore Data", ), "Restore_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Restore Missing Songs", ), "Restore_Success": MessageLookupByLibrary.simpleMessage( "Data successfully restored", ), "Restoring_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Restoring Missing Songs...", ), "Retry": MessageLookupByLibrary.simpleMessage("Retry"), "Rotate_Device": MessageLookupByLibrary.simpleMessage( "Rotate your device to type.", ), "Save": MessageLookupByLibrary.simpleMessage("Save"), "Saved": MessageLookupByLibrary.simpleMessage("Saved"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("Search Gyawun"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "Search History Deleted", ), "Search_Settings": MessageLookupByLibrary.simpleMessage("Search Settings"), "Select_Backup": MessageLookupByLibrary.simpleMessage("Select Backup"), "Select_Playlist_Icon": MessageLookupByLibrary.simpleMessage( "Select Playlist Icon", ), "Settings": MessageLookupByLibrary.simpleMessage("Settings"), "Share": MessageLookupByLibrary.simpleMessage("Share"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("Sheikh Haziq"), "Show_Less": MessageLookupByLibrary.simpleMessage("Show Less"), "Show_More": MessageLookupByLibrary.simpleMessage("Show More"), "Shuffle": MessageLookupByLibrary.simpleMessage("Shuffle"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("Skip Silence"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("Sleep Timer"), "Songs": MessageLookupByLibrary.simpleMessage("Songs"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "Songs will start playing soon.", ), "Source_Code": MessageLookupByLibrary.simpleMessage("Source Code"), "Start_Radio": MessageLookupByLibrary.simpleMessage("Start Radio"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage( "Streaming Quality", ), "Subscriptions": MessageLookupByLibrary.simpleMessage("Subscriptions"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "Support me on Ko-fi", ), "Telegram": MessageLookupByLibrary.simpleMessage("Telegram"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("Theme Mode"), "Top_Results": MessageLookupByLibrary.simpleMessage("Top Results"), "Translate_Lyrics": MessageLookupByLibrary.simpleMessage( "Translate Lyrics", ), "Version": MessageLookupByLibrary.simpleMessage("Version"), "View_Equalizer": MessageLookupByLibrary.simpleMessage( "Play a song to see the equalizer.", ), "Visitor_Id": MessageLookupByLibrary.simpleMessage("Visitor Id"), "Window_Effect": MessageLookupByLibrary.simpleMessage("Window Effect"), "YTMusic": MessageLookupByLibrary.simpleMessage("YTMusic"), "Yes": MessageLookupByLibrary.simpleMessage("Yes"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_es.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a es locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'es'; static String m1(count) => "${Intl.plural(count, zero: 'No hay canciones', one: '1 canción', other: '${count} canciones')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("Acerca de"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "Añadir a favoritos", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage( "Añadir a la biblioteca", ), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "Añadir a la lista de reproducción", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("Añadir a la cola"), "Album": MessageLookupByLibrary.simpleMessage("Álbum"), "Albums": MessageLookupByLibrary.simpleMessage("Álbumes"), "Appearence": MessageLookupByLibrary.simpleMessage("Apariencia"), "Artists": MessageLookupByLibrary.simpleMessage("Artistas"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "Audio y reproducción", ), "Backup": MessageLookupByLibrary.simpleMessage("Copia de seguridad"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "Copia de seguridad y restauración", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "Haz clic aquí para desactivar la optimización de batería para que Gyawun funcione correctamente", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "Optimización de batería detectada", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("Reporte de errores"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage("Cómprame un café"), "Cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "Buscar actualizaciones", ), "Confirm": MessageLookupByLibrary.simpleMessage("Confirmar"), "Content": MessageLookupByLibrary.simpleMessage("Contenido"), "Contributors": MessageLookupByLibrary.simpleMessage("Colaboradores"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "Copiado al portapapeles", ), "Country": MessageLookupByLibrary.simpleMessage("País"), "Create": MessageLookupByLibrary.simpleMessage("Crear"), "Create_Playlist": MessageLookupByLibrary.simpleMessage( "Crear lista de reproducción", ), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "Calidad de descarga", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres eliminar este elemento?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "Eliminar historial de reproducción", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres eliminar el historial de reproducción?", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "Eliminar historial de búsqueda", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres eliminar el historial de búsqueda?", ), "Developer": MessageLookupByLibrary.simpleMessage("Desarrollador"), "Donate": MessageLookupByLibrary.simpleMessage("Donar"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "Apoya el desarrollo de Gyawun", ), "Done": MessageLookupByLibrary.simpleMessage("Hecho"), "Download": MessageLookupByLibrary.simpleMessage("Descargar"), "Downloads": MessageLookupByLibrary.simpleMessage("Descargas"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("Colores dinámicos"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "Activar ecualizador", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "Activar historial de reproducción", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "Activar historial de búsqueda", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Introducir ID de visitante", ), "Equalizer": MessageLookupByLibrary.simpleMessage("Ecualizador"), "Favourites": MessageLookupByLibrary.simpleMessage("Favoritos"), "Feature_Request": MessageLookupByLibrary.simpleMessage( "Solicitud de características", ), "Google_Account": MessageLookupByLibrary.simpleMessage("Cuenta de Google"), "Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun"), "High": MessageLookupByLibrary.simpleMessage("Alto"), "History": MessageLookupByLibrary.simpleMessage("Historial"), "Home": MessageLookupByLibrary.simpleMessage("Inicio"), "Import": MessageLookupByLibrary.simpleMessage("Importar"), "Import_Playlist": MessageLookupByLibrary.simpleMessage( "Importar lista de reproducción", ), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("Jhelum Corp"), "Language": MessageLookupByLibrary.simpleMessage("Idioma"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "Volumen y ecualizador", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage( "Mejorador de volumen", ), "Low": MessageLookupByLibrary.simpleMessage("Bajo"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage( "Hecho en Cachemira", ), "Name": MessageLookupByLibrary.simpleMessage("Nombre"), "Next_Up": MessageLookupByLibrary.simpleMessage("Siguiente"), "No": MessageLookupByLibrary.simpleMessage("No"), "Organisation": MessageLookupByLibrary.simpleMessage("Organización"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage("Pagar con UPI"), "Payment_Methods": MessageLookupByLibrary.simpleMessage("Métodos de pago"), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "Contenido personalizado", ), "Play_Next": MessageLookupByLibrary.simpleMessage("Reproducir siguiente"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "Historial de reproducción eliminado", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("Nombre de la lista"), "Playlists": MessageLookupByLibrary.simpleMessage("Listas de reproducción"), "Progress": MessageLookupByLibrary.simpleMessage("Progreso"), "Remove": MessageLookupByLibrary.simpleMessage("Eliminar"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres borrar todo el historial?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "Eliminar de favoritos", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "Eliminar de la biblioteca", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres eliminarlo de YTMusic?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres eliminarlo?", ), "Rename": MessageLookupByLibrary.simpleMessage("Renombrar"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "Renombrar lista de reproducción", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Restablecer ID de visitante", ), "Restore": MessageLookupByLibrary.simpleMessage("Restaurar"), "Saved": MessageLookupByLibrary.simpleMessage("Guardado"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("Buscar Gyawun"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "Historial de búsqueda eliminado", ), "Search_Settings": MessageLookupByLibrary.simpleMessage( "Configuración de búsqueda", ), "Select_Backup": MessageLookupByLibrary.simpleMessage( "Seleccionar copia de seguridad", ), "Settings": MessageLookupByLibrary.simpleMessage("Configuraciones"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("Sheikh Haziq"), "Show_Less": MessageLookupByLibrary.simpleMessage("Mostrar menos"), "Show_More": MessageLookupByLibrary.simpleMessage("Mostrar más"), "Shuffle": MessageLookupByLibrary.simpleMessage("Aleatorio"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("Saltar silencio"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage( "Temporizador de apagado", ), "Songs": MessageLookupByLibrary.simpleMessage("Canciones"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "Las canciones comenzarán a reproducirse pronto.", ), "Source_Code": MessageLookupByLibrary.simpleMessage("Código fuente"), "Start_Radio": MessageLookupByLibrary.simpleMessage("Iniciar radio"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage( "Calidad de transmisión", ), "Subscriptions": MessageLookupByLibrary.simpleMessage("Suscripciones"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "Apóyame en Ko-fi", ), "Telegram": MessageLookupByLibrary.simpleMessage("Telegram"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("Modo de tema"), "Version": MessageLookupByLibrary.simpleMessage("Versión"), "Visitor_Id": MessageLookupByLibrary.simpleMessage("ID de visitante"), "Window_Effect": MessageLookupByLibrary.simpleMessage("Efecto de ventana"), "YTMusic": MessageLookupByLibrary.simpleMessage("YTMusic"), "Yes": MessageLookupByLibrary.simpleMessage("Sí"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_fr.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a fr locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'fr'; static String m1(count) => "${Intl.plural(count, zero: 'Pas de Titres', one: '1 Titre', other: '${count} Titres')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("À propos"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "Ajouter aux favoris", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage( "Ajouter à la bibliothèque", ), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "Ajouter à une playlist", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage( "Ajouter à la file d\'attente", ), "Album": MessageLookupByLibrary.simpleMessage("Album"), "Albums": MessageLookupByLibrary.simpleMessage("Albums"), "App_Folder": MessageLookupByLibrary.simpleMessage("Dossier Application"), "Appearence": MessageLookupByLibrary.simpleMessage("Apparence"), "Artists": MessageLookupByLibrary.simpleMessage("Artistes"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "Audio et Lecture", ), "Autofetch_Songs": MessageLookupByLibrary.simpleMessage( "Lecture automatique de titres similaires", ), "Backup": MessageLookupByLibrary.simpleMessage("Sauvegarde"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "Sauvegarde et Restauration", ), "Backup_Failed": MessageLookupByLibrary.simpleMessage( "Échec de la sauvegarde des données", ), "Backup_Success": MessageLookupByLibrary.simpleMessage( "Sauvegarde effectuée avec succès à", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "Cliquez ici pour désactiver l\'optimisation de la batterie afin que Gyawun fonctionne correctement.", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "Optimisation de batterie détectée", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("Rapport de bug"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage( "Offrez-moi un café", ), "Cancel": MessageLookupByLibrary.simpleMessage("Annuler"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "Vérifier les mises à jour", ), "Confirm": MessageLookupByLibrary.simpleMessage("Confirmer"), "Confirm_Delete_All_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir les supprimer ?", ), "Content": MessageLookupByLibrary.simpleMessage("Contenu"), "Contributors": MessageLookupByLibrary.simpleMessage("Contributeurs"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "Copié dans le presse-papiers", ), "Country": MessageLookupByLibrary.simpleMessage("Pays"), "Create": MessageLookupByLibrary.simpleMessage("Créer"), "Create_Playlist": MessageLookupByLibrary.simpleMessage( "Créer une playlist", ), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "Qualité du Téléchargement", ), "Delete_All_Songs": MessageLookupByLibrary.simpleMessage( "Supprimer tous les titres", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir supprimer cet élément ?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "Supprimer l\'historique de lecture", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir supprimer l\'historique de lecture ?", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "Supprimer l\'historique de recherche", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir supprimer l\'historique de recherche ?", ), "Deleting_Songs": MessageLookupByLibrary.simpleMessage( "Suppression de titres...", ), "Developer": MessageLookupByLibrary.simpleMessage("Développeur"), "Donate": MessageLookupByLibrary.simpleMessage("Faire un don"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "Soutenez le développement de Gyawun", ), "Done": MessageLookupByLibrary.simpleMessage("Terminé"), "Download": MessageLookupByLibrary.simpleMessage("Télécharger"), "Download_Started": MessageLookupByLibrary.simpleMessage( "Téléchargement en cours...", ), "Downloading": MessageLookupByLibrary.simpleMessage("Téléchargement"), "Downloads": MessageLookupByLibrary.simpleMessage("Téléchargements"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage( "Couleurs Dynamiques", ), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "Activer l\'égaliseur", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "Activer l\'historique de lecture", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "Activer l\'historique de recherche", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Saisir l\'identifiant du visiteur", ), "Equalizer": MessageLookupByLibrary.simpleMessage("Égaliseur"), "Favourites": MessageLookupByLibrary.simpleMessage("Favoris"), "Feature_Request": MessageLookupByLibrary.simpleMessage( "Demande de fonctionnalité", ), "Go_To_Downloads": MessageLookupByLibrary.simpleMessage( "Accéder aux téléchargements", ), "Google_Account": MessageLookupByLibrary.simpleMessage("Compte Google"), "Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun"), "High": MessageLookupByLibrary.simpleMessage("Haute"), "History": MessageLookupByLibrary.simpleMessage("Historique"), "Home": MessageLookupByLibrary.simpleMessage("Accueil"), "Import": MessageLookupByLibrary.simpleMessage("Importer"), "Import_Playlist": MessageLookupByLibrary.simpleMessage( "Importer une playlist", ), "In_Progress": MessageLookupByLibrary.simpleMessage("En cours"), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("Jhelum Corp"), "Language": MessageLookupByLibrary.simpleMessage("Langue"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "Volume et Égaliseur", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage( "Amplificateur de Volume", ), "Low": MessageLookupByLibrary.simpleMessage("Basse"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage( "Fabriqué au Cachemire", ), "Name": MessageLookupByLibrary.simpleMessage("Nom"), "Next_Up": MessageLookupByLibrary.simpleMessage("Suivant"), "No": MessageLookupByLibrary.simpleMessage("Non"), "No_Internet_Connection": MessageLookupByLibrary.simpleMessage( "Aucune connexion Internet", ), "Organisation": MessageLookupByLibrary.simpleMessage("Organisation"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage("Payer avec UPI"), "Payment_Methods": MessageLookupByLibrary.simpleMessage( "Modes de Paiement", ), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "Contenu Personnalisé", ), "Play_Next": MessageLookupByLibrary.simpleMessage("Lire ensuite"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "Historique de lecture supprimé", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("Nom de la playlist"), "Playlist_Not_Available": MessageLookupByLibrary.simpleMessage( "Playlist indisponible", ), "Playlists": MessageLookupByLibrary.simpleMessage("Playlists"), "Progress": MessageLookupByLibrary.simpleMessage("Progrès"), "Queued": MessageLookupByLibrary.simpleMessage("En attente"), "Remove": MessageLookupByLibrary.simpleMessage("Supprimer"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir effacer tout l\'historique ?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "Supprimer des favoris", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "Supprimer de la bibliothèque", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir le supprimer de YouTube Music ?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir le supprimer ?", ), "Rename": MessageLookupByLibrary.simpleMessage("Renommer"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "Renommer la playlist", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Réinitialiser l\'identifiant du visiteur", ), "Restore": MessageLookupByLibrary.simpleMessage("Restauration"), "Restore_Failed": MessageLookupByLibrary.simpleMessage( "Échec de la restauration des données", ), "Restore_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Restaurer les titres manquants", ), "Restore_Success": MessageLookupByLibrary.simpleMessage( "Données restaurées avec succès", ), "Restoring_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Restauration des titres manquants...", ), "Retry": MessageLookupByLibrary.simpleMessage("Réessayer"), "Save": MessageLookupByLibrary.simpleMessage("Enregistrer"), "Saved": MessageLookupByLibrary.simpleMessage("Enregistré"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage( "Rechercher sur Gyawun", ), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "Historique de recherche supprimé", ), "Search_Settings": MessageLookupByLibrary.simpleMessage( "Paramètres de recherche", ), "Select_Backup": MessageLookupByLibrary.simpleMessage( "Sélectionnez la sauvegarde", ), "Settings": MessageLookupByLibrary.simpleMessage("Paramètres"), "Share": MessageLookupByLibrary.simpleMessage("Partager"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("Sheikh Haziq"), "Show_Less": MessageLookupByLibrary.simpleMessage("Afficher moins"), "Show_More": MessageLookupByLibrary.simpleMessage("Afficher plus"), "Shuffle": MessageLookupByLibrary.simpleMessage("Aléatoire"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("Ignorer le Silence"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("Minuterie de Sommeil"), "Songs": MessageLookupByLibrary.simpleMessage("Titres"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "Les titres commenceront bientôt à être diffusés.", ), "Source_Code": MessageLookupByLibrary.simpleMessage("Code Source"), "Start_Radio": MessageLookupByLibrary.simpleMessage("Démarrer la radio"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage( "Qualité du Streaming", ), "Subscriptions": MessageLookupByLibrary.simpleMessage("Abonnements"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "Soutenez-moi sur Ko-fi", ), "Telegram": MessageLookupByLibrary.simpleMessage("Telegram"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("Thème"), "Translate_Lyrics": MessageLookupByLibrary.simpleMessage( "Traduire les paroles", ), "Version": MessageLookupByLibrary.simpleMessage("Version"), "Visitor_Id": MessageLookupByLibrary.simpleMessage( "Identifiant du Visiteur", ), "Window_Effect": MessageLookupByLibrary.simpleMessage("Effet fenêtre"), "YTMusic": MessageLookupByLibrary.simpleMessage("YouTube Music"), "Yes": MessageLookupByLibrary.simpleMessage("Oui"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_hi.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a hi locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'hi'; static String m1(count) => "${Intl.plural(count, zero: 'कोई गाने नहीं', one: '1 गाना', other: '${count} गाने')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("के बारे में"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "पसंदीदा में जोड़ें", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage( "लाइब्रेरी में जोड़ें", ), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "प्लेलिस्ट में जोड़ें", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("कतार में जोड़ें"), "Album": MessageLookupByLibrary.simpleMessage("एल्बम"), "Albums": MessageLookupByLibrary.simpleMessage("एल्बम"), "Appearence": MessageLookupByLibrary.simpleMessage("दिखावट"), "Artists": MessageLookupByLibrary.simpleMessage("कलाकार"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "ऑडियो और प्लेबैक", ), "Backup": MessageLookupByLibrary.simpleMessage("बैकअप"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "बैकअप और पुनर्स्थापना", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "ग्यावुन को ठीक से काम करने के लिए बैटरी ऑप्टिमाइजेशन को अक्षम करने के लिए यहां क्लिक करें", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "बैटरी ऑप्टिमाइजेशन पता चला", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("बग रिपोर्ट"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage( "मुझे एक कॉफी खरीदें", ), "Cancel": MessageLookupByLibrary.simpleMessage("रद्द करें"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "अपडेट के लिए जाँचें", ), "Confirm": MessageLookupByLibrary.simpleMessage("पुष्टि करें"), "Content": MessageLookupByLibrary.simpleMessage("सामग्री"), "Contributors": MessageLookupByLibrary.simpleMessage("योगदानकर्ता"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "क्लिपबोर्ड पर कॉपी किया गया", ), "Country": MessageLookupByLibrary.simpleMessage("देश"), "Create": MessageLookupByLibrary.simpleMessage("बनाएं"), "Create_Playlist": MessageLookupByLibrary.simpleMessage("प्लेलिस्ट बनाएं"), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "डाउनलोड गुणवत्ता", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई इस आइटम को हटाना चाहते हैं?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "प्लेबैक इतिहास हटाएं", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई प्लेबैक इतिहास हटाना चाहते हैं।", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "खोज इतिहास हटाएं", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई खोज इतिहास हटाना चाहते हैं।", ), "Developer": MessageLookupByLibrary.simpleMessage("डेवलपर"), "Donate": MessageLookupByLibrary.simpleMessage("दान करें"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "ग्यावुन के विकास का समर्थन करें", ), "Done": MessageLookupByLibrary.simpleMessage("हो गया"), "Download": MessageLookupByLibrary.simpleMessage("डाउनलोड"), "Downloads": MessageLookupByLibrary.simpleMessage("डाउनलोड"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("डायनामिक रंग"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "इक्वलाइज़र सक्षम करें", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "प्लेबैक इतिहास सक्षम करें", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "खोज इतिहास सक्षम करें", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "विज़िटर आईडी दर्ज करें", ), "Equalizer": MessageLookupByLibrary.simpleMessage("इक्वलाइज़र"), "Favourites": MessageLookupByLibrary.simpleMessage("पसंदीदा"), "Feature_Request": MessageLookupByLibrary.simpleMessage("फीचर अनुरोध"), "Google_Account": MessageLookupByLibrary.simpleMessage("गूगल अकाउंट"), "Gyawun": MessageLookupByLibrary.simpleMessage("ग्यावुन"), "High": MessageLookupByLibrary.simpleMessage("उच्च"), "History": MessageLookupByLibrary.simpleMessage("इतिहास"), "Home": MessageLookupByLibrary.simpleMessage("होम"), "Import": MessageLookupByLibrary.simpleMessage("आयात करें"), "Import_Playlist": MessageLookupByLibrary.simpleMessage( "प्लेलिस्ट आयात करें", ), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("झेलम कॉर्प"), "Language": MessageLookupByLibrary.simpleMessage("भाषा"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "लाउडनेस और इक्वलाइज़र", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage( "लाउडनेस एन्हांसर", ), "Low": MessageLookupByLibrary.simpleMessage("निम्न"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage("कश्मीर में बना"), "Name": MessageLookupByLibrary.simpleMessage("नाम"), "Next_Up": MessageLookupByLibrary.simpleMessage("अगला"), "No": MessageLookupByLibrary.simpleMessage("नहीं"), "Organisation": MessageLookupByLibrary.simpleMessage("संगठन"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage( "UPI के साथ भुगतान करें", ), "Payment_Methods": MessageLookupByLibrary.simpleMessage("भुगतान के तरीके"), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "व्यक्तिगत सामग्री", ), "Play_Next": MessageLookupByLibrary.simpleMessage("अगला चलाएं"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "प्लेबैक इतिहास हटाया गया", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("प्लेलिस्ट का नाम"), "Playlists": MessageLookupByLibrary.simpleMessage("प्लेलिस्ट"), "Progress": MessageLookupByLibrary.simpleMessage("प्रगति"), "Remove": MessageLookupByLibrary.simpleMessage("हटाएं"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई सभी इतिहास साफ़ करना चाहते हैं?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "पसंदीदा से हटाएं", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "लाइब्रेरी से हटाएं", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई इसे YT संगीत से हटाना चाहते हैं?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "क्या आप वाकई इसे हटाना चाहते हैं?", ), "Rename": MessageLookupByLibrary.simpleMessage("नाम बदलें"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "प्लेलिस्ट का नाम बदलें", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "विज़िटर आईडी रीसेट करें", ), "Restore": MessageLookupByLibrary.simpleMessage("पुनर्स्थापित"), "Saved": MessageLookupByLibrary.simpleMessage("सहेजे गए"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("ग्यावुन खोजें"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "खोज इतिहास हटाया गया", ), "Search_Settings": MessageLookupByLibrary.simpleMessage("खोज सेटिंग्स"), "Select_Backup": MessageLookupByLibrary.simpleMessage("बैकअप चुनें"), "Settings": MessageLookupByLibrary.simpleMessage("सेटिंग्स"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("शेख हाजिक"), "Show_Less": MessageLookupByLibrary.simpleMessage("कम दिखाएं"), "Show_More": MessageLookupByLibrary.simpleMessage("और दिखाएं"), "Shuffle": MessageLookupByLibrary.simpleMessage("शफल"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("चुप्पी छोड़ें"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("स्लीप टाइमर"), "Songs": MessageLookupByLibrary.simpleMessage("गाने"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "गाने जल्द ही बजना शुरू हो जाएंगे।", ), "Source_Code": MessageLookupByLibrary.simpleMessage("सोर्स कोड"), "Start_Radio": MessageLookupByLibrary.simpleMessage("रेडियो शुरू करें"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage( "स्ट्रीमिंग गुणवत्ता", ), "Subscriptions": MessageLookupByLibrary.simpleMessage("सब्सक्रिप्शन"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "को-फ़ी पर मुझे समर्थन दें", ), "Telegram": MessageLookupByLibrary.simpleMessage("टेलीग्राम"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("थीम मोड"), "Version": MessageLookupByLibrary.simpleMessage("संस्करण"), "Visitor_Id": MessageLookupByLibrary.simpleMessage("विज़िटर आईडी"), "Window_Effect": MessageLookupByLibrary.simpleMessage("विंडो प्रभाव"), "YTMusic": MessageLookupByLibrary.simpleMessage("YT संगीत"), "Yes": MessageLookupByLibrary.simpleMessage("हाँ"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_it.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a it locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'it'; static String m0(count) => "In coda (${count})"; static String m1(count) => "${Intl.plural(count, zero: 'Nessun brano', one: '1 brano', other: '${count} brani')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("Informazioni"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "Aggiungi ai preferiti", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage( "Aggiungi alla libreria", ), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "Aggiungi alla playlist", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("Aggiungi alla coda"), "Album": MessageLookupByLibrary.simpleMessage("Album"), "Albums": MessageLookupByLibrary.simpleMessage("Album"), "App_Folder": MessageLookupByLibrary.simpleMessage("Cartella App"), "Appearence": MessageLookupByLibrary.simpleMessage("Aspetto"), "Artists": MessageLookupByLibrary.simpleMessage("Artisti"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "Audio e riproduzione", ), "Autofetch_Songs": MessageLookupByLibrary.simpleMessage( "Riproduci automaticamente brani simili", ), "Backup": MessageLookupByLibrary.simpleMessage("Backup"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "Backup e ripristino", ), "Backup_Failed": MessageLookupByLibrary.simpleMessage( "Salvataggio dati non riuscito", ), "Backup_Success": MessageLookupByLibrary.simpleMessage( "Salvataggio dati riuscito al percorso", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "Clicca qui per disattivare l’ottimizzazione della batteria e permettere a Gyawun di funzionare correttamente", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "Ottimizzazione batteria rilevata", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage("Offrimi un caffè"), "Cancel": MessageLookupByLibrary.simpleMessage("Annulla"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "Verifica aggiornamenti", ), "Confirm": MessageLookupByLibrary.simpleMessage("Conferma"), "Confirm_Delete_All_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di volerli eliminare?", ), "Content": MessageLookupByLibrary.simpleMessage("Contenuti"), "Contributors": MessageLookupByLibrary.simpleMessage("Collaboratori"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "Copiato negli appunti", ), "Country": MessageLookupByLibrary.simpleMessage("Paese"), "Create": MessageLookupByLibrary.simpleMessage("Crea"), "Create_Playlist": MessageLookupByLibrary.simpleMessage("Crea playlist"), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "Qualità download", ), "Delete_All_Songs": MessageLookupByLibrary.simpleMessage( "Elimina tutti i brani", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di voler eliminare questo elemento?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "Elimina cronologia di riproduzione", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di voler eliminare la cronologia di riproduzione?", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "Elimina cronologia di ricerca", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di voler eliminare la cronologia di ricerca?", ), "Deleting_Songs": MessageLookupByLibrary.simpleMessage( "Eliminazione brani...", ), "Developer": MessageLookupByLibrary.simpleMessage("Sviluppatore"), "Donate": MessageLookupByLibrary.simpleMessage("Dona"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "Supporta lo sviluppo di Gyawun", ), "Done": MessageLookupByLibrary.simpleMessage("Fatto"), "Download": MessageLookupByLibrary.simpleMessage("Download"), "Download_Started": MessageLookupByLibrary.simpleMessage( "Download avviato...", ), "Downloading": MessageLookupByLibrary.simpleMessage("In download"), "Downloads": MessageLookupByLibrary.simpleMessage("Download"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("Colori dinamici"), "Edit": MessageLookupByLibrary.simpleMessage("Modifica"), "Edit_Playlist": MessageLookupByLibrary.simpleMessage("Modifica Playlist"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "Abilita equalizzatore", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "Abilita cronologia di riproduzione", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "Abilita cronologia di ricerca", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Inserisci Visitor ID", ), "Equalizer": MessageLookupByLibrary.simpleMessage("Equalizzatore"), "Favourites": MessageLookupByLibrary.simpleMessage("Preferiti"), "Feature_Request": MessageLookupByLibrary.simpleMessage( "Richiesta funzionalità", ), "File_Not_Found": MessageLookupByLibrary.simpleMessage("File non trovato"), "Go_To_Downloads": MessageLookupByLibrary.simpleMessage("Vai a Download"), "Google_Account": MessageLookupByLibrary.simpleMessage("Account Google"), "Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun"), "High": MessageLookupByLibrary.simpleMessage("Alta"), "History": MessageLookupByLibrary.simpleMessage("Cronologia"), "Home": MessageLookupByLibrary.simpleMessage("Home"), "Import": MessageLookupByLibrary.simpleMessage("Importa"), "Import_Playlist": MessageLookupByLibrary.simpleMessage("Importa playlist"), "In_Progress": MessageLookupByLibrary.simpleMessage("In corso"), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("Jhelum Corp"), "Language": MessageLookupByLibrary.simpleMessage("Lingua"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "Volume ed equalizzatore", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage( "Amplificatore volume", ), "Low": MessageLookupByLibrary.simpleMessage("Bassa"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage( "Realizzato in Kashmir", ), "Name": MessageLookupByLibrary.simpleMessage("Nome"), "Next_Up": MessageLookupByLibrary.simpleMessage("Prossimo"), "No": MessageLookupByLibrary.simpleMessage("No"), "No_Internet_Connection": MessageLookupByLibrary.simpleMessage( "Nessuna Connessione Internet", ), "No_Offline_Songs": MessageLookupByLibrary.simpleMessage( "Nessun brano scaricato", ), "Organisation": MessageLookupByLibrary.simpleMessage("Organizzazione"), "Other_Results": MessageLookupByLibrary.simpleMessage("Altri Risultati"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage("Paga con UPI"), "Payment_Methods": MessageLookupByLibrary.simpleMessage( "Metodi di pagamento", ), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "Contenuti personalizzati", ), "Play_All": MessageLookupByLibrary.simpleMessage("Riproduci"), "Play_Next": MessageLookupByLibrary.simpleMessage("Riproduci dopo"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "Cronologia di riproduzione eliminata", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("Nome playlist"), "Playlist_Not_Available": MessageLookupByLibrary.simpleMessage( "Playlist non disponibile", ), "Playlists": MessageLookupByLibrary.simpleMessage("Playlist"), "Progress": MessageLookupByLibrary.simpleMessage("Avanzamento"), "Queued": MessageLookupByLibrary.simpleMessage("In coda"), "Queued_Count": m0, "Remove": MessageLookupByLibrary.simpleMessage("Rimuovi"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di voler cancellare tutta la cronologia?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "Rimuovi dai preferiti", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "Rimuovi dalla libreria", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di volerlo rimuovere da YTMusic?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "Sei sicuro di volerlo rimuovere?", ), "Rename": MessageLookupByLibrary.simpleMessage("Rinomina"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "Rinomina playlist", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Reimposta Visitor ID", ), "Restore": MessageLookupByLibrary.simpleMessage("Ripristina"), "Restore_Failed": MessageLookupByLibrary.simpleMessage( "Ripristino dati non riuscito", ), "Restore_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Ripristina brani mancanti", ), "Restore_Success": MessageLookupByLibrary.simpleMessage( "Ripristino dati riuscito", ), "Restoring_Missing_Songs": MessageLookupByLibrary.simpleMessage( "Ripristino brani mancanti...", ), "Retry": MessageLookupByLibrary.simpleMessage("Riprova"), "Rotate_Device": MessageLookupByLibrary.simpleMessage( "Ruota il dispositivo per scrivere.", ), "Save": MessageLookupByLibrary.simpleMessage("Salva"), "Saved": MessageLookupByLibrary.simpleMessage("Salvati"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("Cerca in Gyawun"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "Cronologia di ricerca eliminata", ), "Search_Settings": MessageLookupByLibrary.simpleMessage( "Impostazioni di ricerca", ), "Select_Backup": MessageLookupByLibrary.simpleMessage("Seleziona backup"), "Select_Playlist_Icon": MessageLookupByLibrary.simpleMessage( "Seleziona Icona Playlist", ), "Settings": MessageLookupByLibrary.simpleMessage("Impostazioni"), "Share": MessageLookupByLibrary.simpleMessage("Condividi"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("Sheikh Haziq"), "Show_Less": MessageLookupByLibrary.simpleMessage("Mostra meno"), "Show_More": MessageLookupByLibrary.simpleMessage("Mostra di più"), "Shuffle": MessageLookupByLibrary.simpleMessage("Casuale"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("Salta silenzi"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("Timer di spegnimento"), "Songs": MessageLookupByLibrary.simpleMessage("Brani"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "La riproduzione dei brani inizierà a breve.", ), "Source_Code": MessageLookupByLibrary.simpleMessage("Codice sorgente"), "Start_Radio": MessageLookupByLibrary.simpleMessage("Avvia radio"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage( "Qualità streaming", ), "Subscriptions": MessageLookupByLibrary.simpleMessage("Iscrizioni"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "Supportami su Ko-fi", ), "Telegram": MessageLookupByLibrary.simpleMessage("Telegram"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("Tema"), "Top_Results": MessageLookupByLibrary.simpleMessage("Risultati Principali"), "Translate_Lyrics": MessageLookupByLibrary.simpleMessage("Traduci testi"), "Version": MessageLookupByLibrary.simpleMessage("Versione"), "View_Equalizer": MessageLookupByLibrary.simpleMessage( "Riproduci un brano per vedere l\'equalizzatore.", ), "Visitor_Id": MessageLookupByLibrary.simpleMessage("Visitor ID"), "Window_Effect": MessageLookupByLibrary.simpleMessage("Effetto finestra"), "YTMusic": MessageLookupByLibrary.simpleMessage("YTMusic"), "Yes": MessageLookupByLibrary.simpleMessage("Sì"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_tr.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a tr locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'tr'; static String m1(count) => "${Intl.plural(count, zero: 'Şarkı Yok', one: '1 Şarkı', other: '${count} Şarkı')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("Hakkında"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "Favorilere Ekle", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage("Kütüphaneye Ekle"), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "Çalma Listesine Ekle", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("Kuyruğa Ekle"), "Album": MessageLookupByLibrary.simpleMessage("Albüm"), "Albums": MessageLookupByLibrary.simpleMessage("Albümler"), "Appearence": MessageLookupByLibrary.simpleMessage("Görünüm"), "Artists": MessageLookupByLibrary.simpleMessage("Sanatçılar"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "Ses ve Yeniden Oynatma", ), "Backup": MessageLookupByLibrary.simpleMessage("Yedekle"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "Yedekle ve Geri Yükle", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "Gyawun un düzgün çalışması için pil optimizasyonunu devre dışı bırakmak için buraya tıklayın", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "Pil Optimizasyonu Tespit Edildi", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("Hata Raporu"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage( "Bana Bir Kahve Al", ), "Cancel": MessageLookupByLibrary.simpleMessage("İptal"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "Güncellemeleri Kontrol Et", ), "Confirm": MessageLookupByLibrary.simpleMessage("Onayla"), "Content": MessageLookupByLibrary.simpleMessage("İçerik"), "Contributors": MessageLookupByLibrary.simpleMessage("Katkıda Bulunanlar"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "Panoya Kopyalandı", ), "Country": MessageLookupByLibrary.simpleMessage("Ülke"), "Create": MessageLookupByLibrary.simpleMessage("Oluştur"), "Create_Playlist": MessageLookupByLibrary.simpleMessage( "Çalma Listesi Oluştur", ), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage( "İndirme Kalitesi", ), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "Bu öğeyi silmek istediğinizden emin misiniz?", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "Yeniden Oynatma Geçmişini Sil", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Yeniden Oynatma Geçmişini silmek istediğinizden emin misiniz.", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "Arama Geçmişini Sil", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "Arama Geçmişini silmek istediğinizden emin misiniz.", ), "Developer": MessageLookupByLibrary.simpleMessage("Geliştirici"), "Donate": MessageLookupByLibrary.simpleMessage("Bağış Yap"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "Gyawun un geliştirilmesini destekleyin", ), "Done": MessageLookupByLibrary.simpleMessage("Tamam"), "Download": MessageLookupByLibrary.simpleMessage("İndir"), "Downloads": MessageLookupByLibrary.simpleMessage("İndirilenler"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("Dinamik Renkler"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "Ekolayzeri Etkinleştir", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "Yeniden Oynatma Geçmişini Etkinleştir", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "Arama Geçmişini Etkinleştir", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Ziyaretçi Kimliği Girin", ), "Equalizer": MessageLookupByLibrary.simpleMessage("Ekolayzer"), "Favourites": MessageLookupByLibrary.simpleMessage("Favoriler"), "Feature_Request": MessageLookupByLibrary.simpleMessage("Özellik İsteği"), "Google_Account": MessageLookupByLibrary.simpleMessage("Google Hesabı"), "Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun"), "High": MessageLookupByLibrary.simpleMessage("Yüksek"), "History": MessageLookupByLibrary.simpleMessage("Geçmiş"), "Home": MessageLookupByLibrary.simpleMessage("Ana Sayfa"), "Import": MessageLookupByLibrary.simpleMessage("İçe Aktar"), "Import_Playlist": MessageLookupByLibrary.simpleMessage( "Çalma Listesi İçe Aktar", ), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("Jhelum Corp"), "Language": MessageLookupByLibrary.simpleMessage("Dil"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "Ses Yüksekliği ve Ekolayzer", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage("Ses Yükseltici"), "Low": MessageLookupByLibrary.simpleMessage("Düşük"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage( "Keşmir de Yapıldı", ), "Name": MessageLookupByLibrary.simpleMessage("İsim"), "Next_Up": MessageLookupByLibrary.simpleMessage("Sıradaki"), "No": MessageLookupByLibrary.simpleMessage("Hayır"), "Organisation": MessageLookupByLibrary.simpleMessage("Organizasyon"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage("UPI ile Öde"), "Payment_Methods": MessageLookupByLibrary.simpleMessage("Ödeme Yöntemleri"), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "Kişiselleştirilmiş İçerik", ), "Play_Next": MessageLookupByLibrary.simpleMessage("Sonraki Oynat"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "Yeniden Oynatma Geçmişi Silindi", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("Çalma Listesi Adı"), "Playlists": MessageLookupByLibrary.simpleMessage("Çalma Listeleri"), "Progress": MessageLookupByLibrary.simpleMessage("İlerleme"), "Remove": MessageLookupByLibrary.simpleMessage("Kaldır"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "Tüm geçmişi temizlemek istediğinizden emin misiniz?", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "Favorilerden Çıkar", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "Kütüphaneden Çıkar", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "YTMüzikten kaldırmak istediğinizden emin misiniz?", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "Kaldırmak istediğinizden emin misiniz?", ), "Rename": MessageLookupByLibrary.simpleMessage("Yeniden Adlandır"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "Çalma Listesini Yeniden Adlandır", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "Ziyaretçi Kimliğini Sıfırla", ), "Restore": MessageLookupByLibrary.simpleMessage("Geri Yükle"), "Saved": MessageLookupByLibrary.simpleMessage("Kaydedilenler"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("Gyawun Ara"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "Arama Geçmişi Silindi", ), "Search_Settings": MessageLookupByLibrary.simpleMessage("Arama Ayarları"), "Select_Backup": MessageLookupByLibrary.simpleMessage("Yedeği Seç"), "Settings": MessageLookupByLibrary.simpleMessage("Ayarlar"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("Sheikh Haziq"), "Show_Less": MessageLookupByLibrary.simpleMessage("Daha Az Göster"), "Show_More": MessageLookupByLibrary.simpleMessage("Daha Fazla Göster"), "Shuffle": MessageLookupByLibrary.simpleMessage("Karıştır"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("Sessizliği Atla"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("Uyku Zamanlayıcı"), "Songs": MessageLookupByLibrary.simpleMessage("Şarkılar"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "Şarkılar yakında çalmaya başlayacak.", ), "Source_Code": MessageLookupByLibrary.simpleMessage("Kaynak Kodu"), "Start_Radio": MessageLookupByLibrary.simpleMessage("Radyo Başlat"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage("Yayın Kalitesi"), "Subscriptions": MessageLookupByLibrary.simpleMessage("Abonelikler"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "Ko-fi üzerinden bana destek ol", ), "Telegram": MessageLookupByLibrary.simpleMessage("Telegram"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("Tema Modu"), "Version": MessageLookupByLibrary.simpleMessage("Versiyon"), "Visitor_Id": MessageLookupByLibrary.simpleMessage("Ziyaretçi Kimliği"), "Window_Effect": MessageLookupByLibrary.simpleMessage("Pencere Efekti"), "YTMusic": MessageLookupByLibrary.simpleMessage("YTMüzik"), "Yes": MessageLookupByLibrary.simpleMessage("Evet"), "nSongs": m1, }; } ================================================ FILE: lib/generated/intl/messages_ur.dart ================================================ // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart // This is a library that provides messages for a ur locale. All the // messages from the main program should be duplicated here with the same // function name. // Ignore issues from commonly used lints in this file. // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes import 'package:intl/intl.dart'; import 'package:intl/message_lookup_by_library.dart'; final messages = new MessageLookup(); typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ur'; static String m1(count) => "${Intl.plural(count, zero: 'کوئی گانے نہیں', one: '1 گانا', other: '${count} گانے')}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "About": MessageLookupByLibrary.simpleMessage("کے بارے میں"), "Add_To_Favourites": MessageLookupByLibrary.simpleMessage( "پسندیدہ میں شامل کریں", ), "Add_To_Library": MessageLookupByLibrary.simpleMessage( "لائبریری میں شامل کریں", ), "Add_To_Playlist": MessageLookupByLibrary.simpleMessage( "پلے لسٹ میں شامل کریں", ), "Add_To_Queue": MessageLookupByLibrary.simpleMessage("قطار میں شامل کریں"), "Album": MessageLookupByLibrary.simpleMessage("البم"), "Albums": MessageLookupByLibrary.simpleMessage("البمز"), "Appearence": MessageLookupByLibrary.simpleMessage("ظاہری شکل"), "Artists": MessageLookupByLibrary.simpleMessage("فنکار"), "Audio_And_Playback": MessageLookupByLibrary.simpleMessage( "آڈیو اور پلے بیک", ), "Backup": MessageLookupByLibrary.simpleMessage("بیک اپ"), "Backup_And_Restore": MessageLookupByLibrary.simpleMessage( "بیک اپ اور بحالی", ), "Battery_Optimisation_message": MessageLookupByLibrary.simpleMessage( "گیاون کو درست طریقے سے کام کرنے کے لئے بیٹری کی اصلاح کو غیر فعال کرنے کے لئے یہاں کلک کریں", ), "Battery_Optimisation_title": MessageLookupByLibrary.simpleMessage( "بیٹری کی اصلاح کا پتہ چلا", ), "Bug_Report": MessageLookupByLibrary.simpleMessage("بگ رپورٹ"), "Buy_Me_A_Coffee": MessageLookupByLibrary.simpleMessage( "مجھے ایک کافی خریدیں", ), "Cancel": MessageLookupByLibrary.simpleMessage("منسوخ کریں"), "Check_For_Update": MessageLookupByLibrary.simpleMessage( "اپ ڈیٹ کے لئے چیک کریں", ), "Confirm": MessageLookupByLibrary.simpleMessage("تصدیق کریں"), "Content": MessageLookupByLibrary.simpleMessage("مواد"), "Contributors": MessageLookupByLibrary.simpleMessage("شراکت دار"), "Copied_To_Clipboard": MessageLookupByLibrary.simpleMessage( "کلپ بورڈ پر کاپی کر دیا گیا", ), "Country": MessageLookupByLibrary.simpleMessage("ملک"), "Create": MessageLookupByLibrary.simpleMessage("بنائیں"), "Create_Playlist": MessageLookupByLibrary.simpleMessage("پلے لسٹ بنائیں"), "DOwnload_Quality": MessageLookupByLibrary.simpleMessage("ڈاؤن لوڈ کوالٹی"), "Delete_Item_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی اس آئٹم کو حذف کرنا چاہتے ہیں؟", ), "Delete_Playback_History": MessageLookupByLibrary.simpleMessage( "پلے بیک ہسٹری کو حذف کریں", ), "Delete_Playback_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی پلے بیک ہسٹری کو حذف کرنا چاہتے ہیں؟", ), "Delete_Search_History": MessageLookupByLibrary.simpleMessage( "تلاش کی ہسٹری کو حذف کریں", ), "Delete_Search_History_Confirm_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی تلاش کی ہسٹری کو حذف کرنا چاہتے ہیں؟", ), "Developer": MessageLookupByLibrary.simpleMessage("ڈویلپر"), "Donate": MessageLookupByLibrary.simpleMessage("عطیہ"), "Donate_Message": MessageLookupByLibrary.simpleMessage( "گیاون کی ترقی کی حمایت کریں", ), "Done": MessageLookupByLibrary.simpleMessage("ہو گیا"), "Download": MessageLookupByLibrary.simpleMessage("ڈاؤن لوڈ"), "Downloads": MessageLookupByLibrary.simpleMessage("ڈاؤن لوڈ"), "Dynamic_Colors": MessageLookupByLibrary.simpleMessage("ڈائنامک رنگ"), "Enable_Equalizer": MessageLookupByLibrary.simpleMessage( "ایکوالائزر کو فعال کریں", ), "Enable_Playback_History": MessageLookupByLibrary.simpleMessage( "پلے بیک ہسٹری کو فعال کریں", ), "Enable_Search_History": MessageLookupByLibrary.simpleMessage( "تلاش کی ہسٹری کو فعال کریں", ), "Enter_Visitor_Id": MessageLookupByLibrary.simpleMessage( "وزیٹر آئی ڈی درج کریں", ), "Equalizer": MessageLookupByLibrary.simpleMessage("ایکولائزر"), "Favourites": MessageLookupByLibrary.simpleMessage("پسندیدہ"), "Feature_Request": MessageLookupByLibrary.simpleMessage( "خصوصیت کی درخواست", ), "Google_Account": MessageLookupByLibrary.simpleMessage("گوگل اکاؤنٹ"), "Gyawun": MessageLookupByLibrary.simpleMessage("گیاون"), "High": MessageLookupByLibrary.simpleMessage("زیادہ"), "History": MessageLookupByLibrary.simpleMessage("تاریخ"), "Home": MessageLookupByLibrary.simpleMessage("ہوم"), "Import": MessageLookupByLibrary.simpleMessage("درآمد"), "Import_Playlist": MessageLookupByLibrary.simpleMessage( "پلے لسٹ درآمد کریں", ), "Jhelum_Corp": MessageLookupByLibrary.simpleMessage("جہلم کارپ"), "Language": MessageLookupByLibrary.simpleMessage("زبان"), "Loudness_And_Equalizer": MessageLookupByLibrary.simpleMessage( "لاؤڈنس اور ایکوالائزر", ), "Loudness_Enhancer": MessageLookupByLibrary.simpleMessage("لاؤڈنس انہینسر"), "Low": MessageLookupByLibrary.simpleMessage("کم"), "Made_In_Kashmir": MessageLookupByLibrary.simpleMessage( "کشمیر میں بنایا گیا", ), "Name": MessageLookupByLibrary.simpleMessage("نام"), "Next_Up": MessageLookupByLibrary.simpleMessage("اگلا"), "No": MessageLookupByLibrary.simpleMessage("نہیں"), "Organisation": MessageLookupByLibrary.simpleMessage("تنظیم"), "Pay_With_UPI": MessageLookupByLibrary.simpleMessage( "یو پی آئی کے ساتھ ادائیگی", ), "Payment_Methods": MessageLookupByLibrary.simpleMessage("ادائیگی کے طریقے"), "Personalised_Content": MessageLookupByLibrary.simpleMessage( "ذاتی بنایا گیا مواد", ), "Play_Next": MessageLookupByLibrary.simpleMessage("اگلا چلائیں"), "Playback_History_Deleted": MessageLookupByLibrary.simpleMessage( "پلے بیک ہسٹری حذف کر دی گئی", ), "Playlist_Name": MessageLookupByLibrary.simpleMessage("پلے لسٹ کا نام"), "Playlists": MessageLookupByLibrary.simpleMessage("پلے لسٹس"), "Progress": MessageLookupByLibrary.simpleMessage("پیش رفت"), "Remove": MessageLookupByLibrary.simpleMessage("ہٹائیں"), "Remove_All_History_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی تمام تاریخ کو صاف کرنا چاہتے ہیں؟", ), "Remove_From_Favourites": MessageLookupByLibrary.simpleMessage( "پسندیدہ سے ہٹائیں", ), "Remove_From_Library": MessageLookupByLibrary.simpleMessage( "لائبریری سے ہٹائیں", ), "Remove_From_YTMusic_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی اسے YTMusic سے ہٹانا چاہتے ہیں؟", ), "Remove_Message": MessageLookupByLibrary.simpleMessage( "کیا آپ واقعی اسے ہٹانا چاہتے ہیں؟", ), "Rename": MessageLookupByLibrary.simpleMessage("نام تبدیل کریں"), "Rename_Playlist": MessageLookupByLibrary.simpleMessage( "پلے لسٹ کا نام تبدیل کریں", ), "Reset_Visitor_Id": MessageLookupByLibrary.simpleMessage( "وزیٹر آئی ڈی دوبارہ ترتیب دیں", ), "Restore": MessageLookupByLibrary.simpleMessage("بحال کریں"), "Saved": MessageLookupByLibrary.simpleMessage("محفوظ کردہ"), "Search_Gyawun": MessageLookupByLibrary.simpleMessage("گیاون تلاش کریں"), "Search_History_Deleted": MessageLookupByLibrary.simpleMessage( "تلاش کی ہسٹری حذف کر دی گئی", ), "Search_Settings": MessageLookupByLibrary.simpleMessage("تلاش کی ترتیبات"), "Select_Backup": MessageLookupByLibrary.simpleMessage("بیک اپ منتخب کریں"), "Settings": MessageLookupByLibrary.simpleMessage("ترتیبات"), "Sheikh_Haziq": MessageLookupByLibrary.simpleMessage("شیخ ہازق"), "Show_Less": MessageLookupByLibrary.simpleMessage("کم دکھائیں"), "Show_More": MessageLookupByLibrary.simpleMessage("مزید دکھائیں"), "Shuffle": MessageLookupByLibrary.simpleMessage("بے ترتیب"), "Skip_Silence": MessageLookupByLibrary.simpleMessage("خاموشی کو چھوڑ دیں"), "Sleep_Timer": MessageLookupByLibrary.simpleMessage("سلیپ ٹائمر"), "Songs": MessageLookupByLibrary.simpleMessage("گانے"), "Songs_Will_Start_Playing_Soon": MessageLookupByLibrary.simpleMessage( "گانے جلد ہی چلنا شروع ہوجائیں گے۔", ), "Source_Code": MessageLookupByLibrary.simpleMessage("سورس کوڈ"), "Start_Radio": MessageLookupByLibrary.simpleMessage("ریڈیو شروع کریں"), "Streaming_Quality": MessageLookupByLibrary.simpleMessage("سٹریمنگ کوالٹی"), "Subscriptions": MessageLookupByLibrary.simpleMessage("سبسکرپشنز"), "Support_Me_On_Kofi": MessageLookupByLibrary.simpleMessage( "کوفی پر مجھے سپورٹ کریں", ), "Telegram": MessageLookupByLibrary.simpleMessage("ٹیلیگرام"), "Theme_Mode": MessageLookupByLibrary.simpleMessage("تھیم موڈ"), "Version": MessageLookupByLibrary.simpleMessage("ورژن"), "Visitor_Id": MessageLookupByLibrary.simpleMessage("وزیٹر آئی ڈی"), "Window_Effect": MessageLookupByLibrary.simpleMessage("ونڈو ایفیکٹ"), "YTMusic": MessageLookupByLibrary.simpleMessage("وائی ٹی میوزک"), "Yes": MessageLookupByLibrary.simpleMessage("ہاں"), "nSongs": m1, }; } ================================================ FILE: lib/generated/l10n.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'intl/messages_all.dart'; // ************************************************************************** // Generator: Flutter Intl IDE plugin // Made by Localizely // ************************************************************************** // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each // ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes class S { S(); static S? _current; static S get current { assert( _current != null, 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.', ); return _current!; } static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; final instance = S(); S._current = instance; return instance; }); } static S of(BuildContext context) { final instance = S.maybeOf(context); assert( instance != null, 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?', ); return instance!; } static S? maybeOf(BuildContext context) { return Localizations.of(context, S); } /// `Gyawun` String get Gyawun { return Intl.message('Gyawun', name: 'Gyawun', desc: '', args: []); } /// `Next Up` String get Next_Up { return Intl.message('Next Up', name: 'Next_Up', desc: '', args: []); } /// `Shuffle` String get Shuffle { return Intl.message('Shuffle', name: 'Shuffle', desc: '', args: []); } /// `Home` String get Home { return Intl.message('Home', name: 'Home', desc: '', args: []); } /// `Saved` String get Saved { return Intl.message('Saved', name: 'Saved', desc: '', args: []); } /// `YTMusic` String get YTMusic { return Intl.message('YTMusic', name: 'YTMusic', desc: '', args: []); } /// `Settings` String get Settings { return Intl.message('Settings', name: 'Settings', desc: '', args: []); } /// `Search Gyawun` String get Search_Gyawun { return Intl.message( 'Search Gyawun', name: 'Search_Gyawun', desc: '', args: [], ); } /// `Favourites` String get Favourites { return Intl.message('Favourites', name: 'Favourites', desc: '', args: []); } /// `Downloads` String get Downloads { return Intl.message('Downloads', name: 'Downloads', desc: '', args: []); } /// `History` String get History { return Intl.message('History', name: 'History', desc: '', args: []); } /// `{count, plural, =0{No Songs} =1{1 Song} other{{count} Songs}}` String nSongs(num count) { return Intl.plural( count, zero: 'No Songs', one: '1 Song', other: '$count Songs', name: 'nSongs', desc: 'Number of songs', args: [count], ); } /// `Songs` String get Songs { return Intl.message('Songs', name: 'Songs', desc: '', args: []); } /// `Albums` String get Albums { return Intl.message('Albums', name: 'Albums', desc: '', args: []); } /// `Playlists` String get Playlists { return Intl.message('Playlists', name: 'Playlists', desc: '', args: []); } /// `Artists` String get Artists { return Intl.message('Artists', name: 'Artists', desc: '', args: []); } /// `Subscriptions` String get Subscriptions { return Intl.message( 'Subscriptions', name: 'Subscriptions', desc: '', args: [], ); } /// `Search Settings` String get Search_Settings { return Intl.message( 'Search Settings', name: 'Search_Settings', desc: '', args: [], ); } /// `Battery Optimisation Detected` String get Battery_Optimisation_title { return Intl.message( 'Battery Optimisation Detected', name: 'Battery_Optimisation_title', desc: '', args: [], ); } /// `Click here disable battery optimisation for Gyawun to work properly` String get Battery_Optimisation_message { return Intl.message( 'Click here disable battery optimisation for Gyawun to work properly', name: 'Battery_Optimisation_message', desc: '', args: [], ); } /// `Donate` String get Donate { return Intl.message('Donate', name: 'Donate', desc: '', args: []); } /// `Support the development of Gyawun` String get Donate_Message { return Intl.message( 'Support the development of Gyawun', name: 'Donate_Message', desc: '', args: [], ); } /// `Payment Methods` String get Payment_Methods { return Intl.message( 'Payment Methods', name: 'Payment_Methods', desc: '', args: [], ); } /// `Pay with UPI` String get Pay_With_UPI { return Intl.message( 'Pay with UPI', name: 'Pay_With_UPI', desc: '', args: [], ); } /// `Support me on Ko-fi` String get Support_Me_On_Kofi { return Intl.message( 'Support me on Ko-fi', name: 'Support_Me_On_Kofi', desc: '', args: [], ); } /// `Buy me a Coffee` String get Buy_Me_A_Coffee { return Intl.message( 'Buy me a Coffee', name: 'Buy_Me_A_Coffee', desc: '', args: [], ); } /// `Google Account` String get Google_Account { return Intl.message( 'Google Account', name: 'Google_Account', desc: '', args: [], ); } /// `Appearence` String get Appearence { return Intl.message('Appearence', name: 'Appearence', desc: '', args: []); } /// `Theme Mode` String get Theme_Mode { return Intl.message('Theme Mode', name: 'Theme_Mode', desc: '', args: []); } /// `Window Effect` String get Window_Effect { return Intl.message( 'Window Effect', name: 'Window_Effect', desc: '', args: [], ); } /// `Dynamic Colors` String get Dynamic_Colors { return Intl.message( 'Dynamic Colors', name: 'Dynamic_Colors', desc: '', args: [], ); } /// `Content` String get Content { return Intl.message('Content', name: 'Content', desc: '', args: []); } /// `Country` String get Country { return Intl.message('Country', name: 'Country', desc: '', args: []); } /// `Language` String get Language { return Intl.message('Language', name: 'Language', desc: '', args: []); } /// `Translate Lyrics` String get Translate_Lyrics { return Intl.message( 'Translate Lyrics', name: 'Translate_Lyrics', desc: '', args: [], ); } /// `Autoplay Similar Songs` String get Autofetch_Songs { return Intl.message( 'Autoplay Similar Songs', name: 'Autofetch_Songs', desc: '', args: [], ); } /// `Personalised Content` String get Personalised_Content { return Intl.message( 'Personalised Content', name: 'Personalised_Content', desc: '', args: [], ); } /// `Enter Visitor Id` String get Enter_Visitor_Id { return Intl.message( 'Enter Visitor Id', name: 'Enter_Visitor_Id', desc: '', args: [], ); } /// `Visitor Id` String get Visitor_Id { return Intl.message('Visitor Id', name: 'Visitor_Id', desc: '', args: []); } /// `Reset Visitor Id` String get Reset_Visitor_Id { return Intl.message( 'Reset Visitor Id', name: 'Reset_Visitor_Id', desc: '', args: [], ); } /// `Audio and Playback` String get Audio_And_Playback { return Intl.message( 'Audio and Playback', name: 'Audio_And_Playback', desc: '', args: [], ); } /// `Loudness And Equalizer` String get Loudness_And_Equalizer { return Intl.message( 'Loudness And Equalizer', name: 'Loudness_And_Equalizer', desc: '', args: [], ); } /// `Loudness Enhancer` String get Loudness_Enhancer { return Intl.message( 'Loudness Enhancer', name: 'Loudness_Enhancer', desc: '', args: [], ); } /// `Enable Equalizer` String get Enable_Equalizer { return Intl.message( 'Enable Equalizer', name: 'Enable_Equalizer', desc: '', args: [], ); } /// `Streaming Quality` String get Streaming_Quality { return Intl.message( 'Streaming Quality', name: 'Streaming_Quality', desc: '', args: [], ); } /// `Download Quality` String get DOwnload_Quality { return Intl.message( 'Download Quality', name: 'DOwnload_Quality', desc: '', args: [], ); } /// `App Folder` String get App_Folder { return Intl.message('App Folder', name: 'App_Folder', desc: '', args: []); } /// `Skip Silence` String get Skip_Silence { return Intl.message( 'Skip Silence', name: 'Skip_Silence', desc: '', args: [], ); } /// `Enable Playback History` String get Enable_Playback_History { return Intl.message( 'Enable Playback History', name: 'Enable_Playback_History', desc: '', args: [], ); } /// `Delete Playback History` String get Delete_Playback_History { return Intl.message( 'Delete Playback History', name: 'Delete_Playback_History', desc: '', args: [], ); } /// `Are you sure you want to delete Playback History.` String get Delete_Playback_History_Confirm_Message { return Intl.message( 'Are you sure you want to delete Playback History.', name: 'Delete_Playback_History_Confirm_Message', desc: '', args: [], ); } /// `Playback History Deleted` String get Playback_History_Deleted { return Intl.message( 'Playback History Deleted', name: 'Playback_History_Deleted', desc: '', args: [], ); } /// `Enable Search History` String get Enable_Search_History { return Intl.message( 'Enable Search History', name: 'Enable_Search_History', desc: '', args: [], ); } /// `Delete Search History` String get Delete_Search_History { return Intl.message( 'Delete Search History', name: 'Delete_Search_History', desc: '', args: [], ); } /// `Are you sure you want to delete Search History.` String get Delete_Search_History_Confirm_Message { return Intl.message( 'Are you sure you want to delete Search History.', name: 'Delete_Search_History_Confirm_Message', desc: '', args: [], ); } /// `Search History Deleted` String get Search_History_Deleted { return Intl.message( 'Search History Deleted', name: 'Search_History_Deleted', desc: '', args: [], ); } /// `Backup and Restore` String get Backup_And_Restore { return Intl.message( 'Backup and Restore', name: 'Backup_And_Restore', desc: '', args: [], ); } /// `Backup` String get Backup { return Intl.message('Backup', name: 'Backup', desc: '', args: []); } /// `Restore` String get Restore { return Intl.message('Restore', name: 'Restore', desc: '', args: []); } /// `Share` String get Share { return Intl.message('Share', name: 'Share', desc: '', args: []); } /// `Save` String get Save { return Intl.message('Save', name: 'Save', desc: '', args: []); } /// `Backed up successfully at` String get Backup_Success { return Intl.message( 'Backed up successfully at', name: 'Backup_Success', desc: '', args: [], ); } /// `Failed to back up Data` String get Backup_Failed { return Intl.message( 'Failed to back up Data', name: 'Backup_Failed', desc: '', args: [], ); } /// `Data successfully restored` String get Restore_Success { return Intl.message( 'Data successfully restored', name: 'Restore_Success', desc: '', args: [], ); } /// `Failed to restore Data` String get Restore_Failed { return Intl.message( 'Failed to restore Data', name: 'Restore_Failed', desc: '', args: [], ); } /// `Select Backup` String get Select_Backup { return Intl.message( 'Select Backup', name: 'Select_Backup', desc: '', args: [], ); } /// `About` String get About { return Intl.message('About', name: 'About', desc: '', args: []); } /// `Name` String get Name { return Intl.message('Name', name: 'Name', desc: '', args: []); } /// `Version` String get Version { return Intl.message('Version', name: 'Version', desc: '', args: []); } /// `Developer` String get Developer { return Intl.message('Developer', name: 'Developer', desc: '', args: []); } /// `Sheikh Haziq` String get Sheikh_Haziq { return Intl.message( 'Sheikh Haziq', name: 'Sheikh_Haziq', desc: '', args: [], ); } /// `Organisation` String get Organisation { return Intl.message( 'Organisation', name: 'Organisation', desc: '', args: [], ); } /// `Jhelum Corp` String get Jhelum_Corp { return Intl.message('Jhelum Corp', name: 'Jhelum_Corp', desc: '', args: []); } /// `Telegram` String get Telegram { return Intl.message('Telegram', name: 'Telegram', desc: '', args: []); } /// `Contributors` String get Contributors { return Intl.message( 'Contributors', name: 'Contributors', desc: '', args: [], ); } /// `Source Code` String get Source_Code { return Intl.message('Source Code', name: 'Source_Code', desc: '', args: []); } /// `Bug Report` String get Bug_Report { return Intl.message('Bug Report', name: 'Bug_Report', desc: '', args: []); } /// `Feature Request` String get Feature_Request { return Intl.message( 'Feature Request', name: 'Feature_Request', desc: '', args: [], ); } /// `Made in Kashmir` String get Made_In_Kashmir { return Intl.message( 'Made in Kashmir', name: 'Made_In_Kashmir', desc: '', args: [], ); } /// `Check for Update` String get Check_For_Update { return Intl.message( 'Check for Update', name: 'Check_For_Update', desc: '', args: [], ); } /// `Progress` String get Progress { return Intl.message('Progress', name: 'Progress', desc: '', args: []); } /// `Play Next` String get Play_Next { return Intl.message('Play Next', name: 'Play_Next', desc: '', args: []); } /// `Add To Queue` String get Add_To_Queue { return Intl.message( 'Add To Queue', name: 'Add_To_Queue', desc: '', args: [], ); } /// `Add To Favourites` String get Add_To_Favourites { return Intl.message( 'Add To Favourites', name: 'Add_To_Favourites', desc: '', args: [], ); } /// `Remove From Favourites` String get Remove_From_Favourites { return Intl.message( 'Remove From Favourites', name: 'Remove_From_Favourites', desc: '', args: [], ); } /// `Download` String get Download { return Intl.message('Download', name: 'Download', desc: '', args: []); } /// `Add To Playlist` String get Add_To_Playlist { return Intl.message( 'Add To Playlist', name: 'Add_To_Playlist', desc: '', args: [], ); } /// `Start Radio` String get Start_Radio { return Intl.message('Start Radio', name: 'Start_Radio', desc: '', args: []); } /// `Album` String get Album { return Intl.message('Album', name: 'Album', desc: '', args: []); } /// `Rename` String get Rename { return Intl.message('Rename', name: 'Rename', desc: '', args: []); } /// `Add To Library` String get Add_To_Library { return Intl.message( 'Add To Library', name: 'Add_To_Library', desc: '', args: [], ); } /// `Remove From Library` String get Remove_From_Library { return Intl.message( 'Remove From Library', name: 'Remove_From_Library', desc: '', args: [], ); } /// `Are you sure you want to delete this item?` String get Delete_Item_Message { return Intl.message( 'Are you sure you want to delete this item?', name: 'Delete_Item_Message', desc: '', args: [], ); } /// `Equalizer` String get Equalizer { return Intl.message('Equalizer', name: 'Equalizer', desc: '', args: []); } /// `Sleep Timer` String get Sleep_Timer { return Intl.message('Sleep Timer', name: 'Sleep_Timer', desc: '', args: []); } /// `Create Playlist` String get Create_Playlist { return Intl.message( 'Create Playlist', name: 'Create_Playlist', desc: '', args: [], ); } /// `Playlist Name` String get Playlist_Name { return Intl.message( 'Playlist Name', name: 'Playlist_Name', desc: '', args: [], ); } /// `Create` String get Create { return Intl.message('Create', name: 'Create', desc: '', args: []); } /// `Import Playlist` String get Import_Playlist { return Intl.message( 'Import Playlist', name: 'Import_Playlist', desc: '', args: [], ); } /// `Import` String get Import { return Intl.message('Import', name: 'Import', desc: '', args: []); } /// `Rename Playlist` String get Rename_Playlist { return Intl.message( 'Rename Playlist', name: 'Rename_Playlist', desc: '', args: [], ); } /// `Done` String get Done { return Intl.message('Done', name: 'Done', desc: '', args: []); } /// `Cancel` String get Cancel { return Intl.message('Cancel', name: 'Cancel', desc: '', args: []); } /// `Confirm` String get Confirm { return Intl.message('Confirm', name: 'Confirm', desc: '', args: []); } /// `Yes` String get Yes { return Intl.message('Yes', name: 'Yes', desc: '', args: []); } /// `No` String get No { return Intl.message('No', name: 'No', desc: '', args: []); } /// `Show More` String get Show_More { return Intl.message('Show More', name: 'Show_More', desc: '', args: []); } /// `Show Less` String get Show_Less { return Intl.message('Show Less', name: 'Show_Less', desc: '', args: []); } /// `Remove` String get Remove { return Intl.message('Remove', name: 'Remove', desc: '', args: []); } /// `High` String get High { return Intl.message('High', name: 'High', desc: '', args: []); } /// `Low` String get Low { return Intl.message('Low', name: 'Low', desc: '', args: []); } /// `Songs will start playing soon.` String get Songs_Will_Start_Playing_Soon { return Intl.message( 'Songs will start playing soon.', name: 'Songs_Will_Start_Playing_Soon', desc: '', args: [], ); } /// `Are you sure you want to remove it?` String get Remove_Message { return Intl.message( 'Are you sure you want to remove it?', name: 'Remove_Message', desc: '', args: [], ); } /// `Are you sure you want to remove it from YTMusic?` String get Remove_From_YTMusic_Message { return Intl.message( 'Are you sure you want to remove it from YTMusic?', name: 'Remove_From_YTMusic_Message', desc: '', args: [], ); } /// `Are you sure you want to clear all history?` String get Remove_All_History_Message { return Intl.message( 'Are you sure you want to clear all history?', name: 'Remove_All_History_Message', desc: '', args: [], ); } /// `Copied to Clipboard` String get Copied_To_Clipboard { return Intl.message( 'Copied to Clipboard', name: 'Copied_To_Clipboard', desc: '', args: [], ); } /// `No Internet Connection` String get No_Internet_Connection { return Intl.message( 'No Internet Connection', name: 'No_Internet_Connection', desc: '', args: [], ); } /// `Go to Downloads` String get Go_To_Downloads { return Intl.message( 'Go to Downloads', name: 'Go_To_Downloads', desc: '', args: [], ); } /// `Retry` String get Retry { return Intl.message('Retry', name: 'Retry', desc: '', args: []); } /// `Playlist not available` String get Playlist_Not_Available { return Intl.message( 'Playlist not available', name: 'Playlist_Not_Available', desc: '', args: [], ); } /// `Are you sure you want to delete them?` String get Confirm_Delete_All_Message { return Intl.message( 'Are you sure you want to delete them?', name: 'Confirm_Delete_All_Message', desc: '', args: [], ); } /// `Downloading` String get Downloading { return Intl.message('Downloading', name: 'Downloading', desc: '', args: []); } /// `Restore Missing Songs` String get Restore_Missing_Songs { return Intl.message( 'Restore Missing Songs', name: 'Restore_Missing_Songs', desc: '', args: [], ); } /// `Delete All Songs` String get Delete_All_Songs { return Intl.message( 'Delete All Songs', name: 'Delete_All_Songs', desc: '', args: [], ); } /// `Download started...` String get Download_Started { return Intl.message( 'Download started...', name: 'Download_Started', desc: '', args: [], ); } /// `Restoring Missing Songs...` String get Restoring_Missing_Songs { return Intl.message( 'Restoring Missing Songs...', name: 'Restoring_Missing_Songs', desc: '', args: [], ); } /// `Deleting Songs...` String get Deleting_Songs { return Intl.message( 'Deleting Songs...', name: 'Deleting_Songs', desc: '', args: [], ); } /// `In Progress` String get In_Progress { return Intl.message('In Progress', name: 'In_Progress', desc: '', args: []); } /// `Queued` String get Queued { return Intl.message('Queued', name: 'Queued', desc: '', args: []); } /// `Queued ({count})` String Queued_Count(Object count) { return Intl.message( 'Queued ($count)', name: 'Queued_Count', desc: '', args: [count], ); } /// `File not found` String get File_Not_Found { return Intl.message( 'File not found', name: 'File_Not_Found', desc: '', args: [], ); } /// `Play All` String get Play_All { return Intl.message('Play All', name: 'Play_All', desc: '', args: []); } /// `Rotate your device to type.` String get Rotate_Device { return Intl.message( 'Rotate your device to type.', name: 'Rotate_Device', desc: '', args: [], ); } /// `Play a song to see the equalizer.` String get View_Equalizer { return Intl.message( 'Play a song to see the equalizer.', name: 'View_Equalizer', desc: '', args: [], ); } /// `No offline songs available` String get No_Offline_Songs { return Intl.message( 'No offline songs available', name: 'No_Offline_Songs', desc: '', args: [], ); } /// `Edit` String get Edit { return Intl.message('Edit', name: 'Edit', desc: '', args: []); } /// `Edit Playlist` String get Edit_Playlist { return Intl.message( 'Edit Playlist', name: 'Edit_Playlist', desc: '', args: [], ); } /// `Select Playlist Icon` String get Select_Playlist_Icon { return Intl.message( 'Select Playlist Icon', name: 'Select_Playlist_Icon', desc: '', args: [], ); } /// `Top Results` String get Top_Results { return Intl.message('Top Results', name: 'Top_Results', desc: '', args: []); } /// `Other Results` String get Other_Results { return Intl.message( 'Other Results', name: 'Other_Results', desc: '', args: [], ); } } class AppLocalizationDelegate extends LocalizationsDelegate { const AppLocalizationDelegate(); List get supportedLocales { return const [ Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'es'), Locale.fromSubtags(languageCode: 'fr'), Locale.fromSubtags(languageCode: 'hi'), Locale.fromSubtags(languageCode: 'it'), Locale.fromSubtags(languageCode: 'tr'), Locale.fromSubtags(languageCode: 'ur'), ]; } @override bool isSupported(Locale locale) => _isSupported(locale); @override Future load(Locale locale) => S.load(locale); @override bool shouldReload(AppLocalizationDelegate old) => false; bool _isSupported(Locale locale) { for (var supportedLocale in supportedLocales) { if (supportedLocale.languageCode == locale.languageCode) { return true; } } return false; } } ================================================ FILE: lib/l10n/intl_en.arb ================================================ { "@@locale": "en", "Gyawun": "Gyawun", "Next_Up": "Next Up", "@Next_Up": {}, "Shuffle": "Shuffle", "@Shuffle": {}, "@Gyawun": {}, "Home": "Home", "@Home": {}, "Saved": "Saved", "@Saved": {}, "YTMusic": "YTMusic", "@YTMusic": {}, "Settings": "Settings", "@Settings": {}, "Search_Gyawun": "Search Gyawun", "@Search_Gyawun": {}, "Favourites": "Favourites", "@Favourites": {}, "Downloads": "Downloads", "@Downloads": {}, "History": "History", "@History": {}, "nSongs": "{count, plural, =0{No Songs} =1{1 Song} other{{count} Songs}}", "@nSongs": { "description": "Number of songs", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "Songs", "@Songs": {}, "Albums": "Albums", "@Albums": {}, "Playlists": "Playlists", "@Playlists": {}, "Artists": "Artists", "@Artists": {}, "Subscriptions": "Subscriptions", "@Subscriptions": {}, "Search_Settings": "Search Settings", "@Search_Settings": {}, "Battery_Optimisation_title": "Battery Optimisation Detected", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "Click here disable battery optimisation for Gyawun to work properly", "@Battery_Optimisation_message": {}, "Donate": "Donate", "@Donate": {}, "Donate_Message": "Support the development of Gyawun", "@Donate_Message": {}, "Payment_Methods": "Payment Methods", "@Payment_Methods": {}, "Pay_With_UPI": "Pay with UPI", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "Support me on Ko-fi", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "Buy me a Coffee", "@Buy_Me_A_Coffee": {}, "Google_Account": "Google Account", "@Google_Account": {}, "Appearence": "Appearence", "Theme_Mode": "Theme Mode", "@Theme_Mode": {}, "Window_Effect": "Window Effect", "@Window_Effect": {}, "Dynamic_Colors": "Dynamic Colors", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "Content", "@Content": {}, "Country": "Country", "@Country": {}, "Language": "Language", "@Language": {}, "Translate_Lyrics": "Translate Lyrics", "@Translate_Lyrics": {}, "Autofetch_Songs": "Autoplay Similar Songs", "@Autofetch_Songs": {}, "Personalised_Content": "Personalised Content", "@Personalised_Content": {}, "Enter_Visitor_Id": "Enter Visitor Id", "@Enter_Visitor_Id": {}, "Visitor_Id": "Visitor Id", "@Visitor_Id": {}, "Reset_Visitor_Id": "Reset Visitor Id", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "Audio and Playback", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "Loudness And Equalizer", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "Loudness Enhancer", "@Loudness_Enhancer": {}, "Enable_Equalizer": "Enable Equalizer", "@Enable_Equalizer": {}, "Streaming_Quality": "Streaming Quality", "@Streaming_Quality": {}, "DOwnload_Quality": "Download Quality", "@DOwnload_Quality": {}, "App_Folder": "App Folder", "@App_Folder": {}, "Skip_Silence": "Skip Silence", "@Skip_Silence": {}, "Enable_Playback_History": "Enable Playback History", "@Enable_Playback_History": {}, "Delete_Playback_History": "Delete Playback History", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "Are you sure you want to delete Playback History.", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "Playback History Deleted", "@Playback_History_Deleted": {}, "Enable_Search_History": "Enable Search History", "@Enable_Search_History": {}, "Delete_Search_History": "Delete Search History", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "Are you sure you want to delete Search History.", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "Search History Deleted", "@Search_History_Deleted": {}, "Backup_And_Restore": "Backup and Restore", "@Backup_And_Restore": {}, "Backup": "Backup", "@Backup": {}, "Restore": "Restore", "@Restore": {}, "Share": "Share", "@Share": {}, "Save": "Save", "@Save": {}, "Backup_Success": "Backed up successfully at", "@Backup_Success": {}, "Backup_Failed": "Failed to back up Data", "@Backup_Failed": {}, "Restore_Success": "Data successfully restored", "@Restore_Success": {}, "Restore_Failed": "Failed to restore Data", "@Restore_Failed": {}, "Select_Backup": "Select Backup", "@Select_Backup": {}, "About": "About", "@About": {}, "Name": "Name", "@Name": {}, "Version": "Version", "@Version": {}, "Developer": "Developer", "@Developer": {}, "Sheikh_Haziq": "Sheikh Haziq", "@Sheikh_Haziq": {}, "Organisation": "Organisation", "@Organisation": {}, "Jhelum_Corp": "Jhelum Corp", "@Jhelum_Corp": {}, "Telegram": "Telegram", "@Telegram": {}, "Contributors": "Contributors", "@Contributors": {}, "Source_Code": "Source Code", "@Source_Code": {}, "Bug_Report": "Bug Report", "@Bug_Report": {}, "Feature_Request": "Feature Request", "@Feature_Request": {}, "Made_In_Kashmir": "Made in Kashmir", "@Made_In_Kashmir": {}, "Check_For_Update": "Check for Update", "@Check_For_Update": {}, "Progress": "Progress", "@Progress": {}, "Play_Next": "Play Next", "@Play_Next": {}, "Add_To_Queue": "Add To Queue", "@Add_To_Queue": {}, "Add_To_Favourites": "Add To Favourites", "@Add_To_Favourites": {}, "Remove_From_Favourites": "Remove From Favourites", "@Remove_From_Favourites": {}, "Download": "Download", "@Download": {}, "Add_To_Playlist": "Add To Playlist", "@Add_To_Playlist": {}, "Start_Radio": "Start Radio", "@Start_Radio": {}, "Album": "Album", "@Album": {}, "Rename": "Rename", "@Rename": {}, "Add_To_Library": "Add To Library", "@Add_To_Library": {}, "Remove_From_Library": "Remove From Library", "@Remove_From_Library": {}, "Delete_Item_Message": "Are you sure you want to delete this item?", "@Delete_Item_Message": {}, "Equalizer": "Equalizer", "@Equalizer": {}, "Sleep_Timer": "Sleep Timer", "@Sleep_Timer": {}, "Create_Playlist": "Create Playlist", "@Create_Playlist": {}, "Playlist_Name": "Playlist Name", "@Playlist_Name": {}, "Create": "Create", "@Create": {}, "Import_Playlist": "Import Playlist", "@Import_Playlist": {}, "Import": "Import", "@Import": {}, "Rename_Playlist": "Rename Playlist", "@Rename_Playlist": {}, "Done": "Done", "@Done": {}, "Cancel": "Cancel", "@Cancel": {}, "Confirm": "Confirm", "@Confirm": {}, "Yes": "Yes", "@Yes": {}, "No": "No", "@No": {}, "Show_More": "Show More", "@Show_More": {}, "Show_Less": "Show Less", "@Show_Less": {}, "Remove": "Remove", "@Remove": {}, "High": "High", "@High": {}, "Low": "Low", "@Low": {}, "Songs_Will_Start_Playing_Soon": "Songs will start playing soon.", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "Are you sure you want to remove it?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "Are you sure you want to remove it from YTMusic?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "Are you sure you want to clear all history?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "Copied to Clipboard", "@Copied_To_Clipboard": {}, "No_Internet_Connection": "No Internet Connection", "@No_Internet_Connection": {}, "Go_To_Downloads": "Go to Downloads", "@Go_To_Downloads": {}, "Retry": "Retry", "@Retry": {}, "Playlist_Not_Available": "Playlist not available", "@Playlist_Not_Available": {}, "Confirm_Delete_All_Message": "Are you sure you want to delete them?", "@Confirm_Delete_All_Message": {}, "Downloading": "Downloading", "@Downloading": {}, "Restore_Missing_Songs": "Restore Missing Songs", "@Restore_Missing_Songs": {}, "Delete_All_Songs": "Delete All Songs", "@Delete_All_Songs": {}, "Download_Started": "Download started...", "@Download_Started": {}, "Restoring_Missing_Songs": "Restoring Missing Songs...", "@Restoring_Missing_Songs": {}, "Deleting_Songs": "Deleting Songs...", "@Deleting_Songs": {}, "In_Progress": "In Progress", "@In_Progress": {}, "Queued": "Queued", "@Queued": {}, "Queued_Count": "Queued ({count})", "@Queued_Count": {}, "File_Not_Found": "File not found", "@File_Not_Found": {}, "Play_All": "Play All", "@Play_All": {}, "Rotate_Device": "Rotate your device to type.", "@Rotate_Device": {}, "View_Equalizer": "Play a song to see the equalizer.", "@View_Equalizer": {}, "No_Offline_Songs": "No offline songs available", "@No_Offline_Songs": {}, "Edit": "Edit", "@Edit": {}, "Edit_Playlist": "Edit Playlist", "@Edit_Playlist": {}, "Select_Playlist_Icon": "Select Playlist Icon", "@Select_Playlist_Icon": {}, "Top_Results": "Top Results", "@Top_Results": {}, "Other_Results": "Other Results", "@Other_Results": {} } ================================================ FILE: lib/l10n/intl_es.arb ================================================ { "@@locale": "es", "Gyawun": "Gyawun", "Next_Up": "Siguiente", "@Next_Up": {}, "Shuffle": "Aleatorio", "@Shuffle": {}, "@Gyawun": {}, "Home": "Inicio", "@Home": {}, "Saved": "Guardado", "@Saved": {}, "YTMusic": "YTMusic", "@YTMusic": {}, "Settings": "Configuraciones", "@Settings": {}, "Search_Gyawun": "Buscar Gyawun", "@Search_Gyawun": {}, "Favourites": "Favoritos", "@Favourites": {}, "Downloads": "Descargas", "@Downloads": {}, "History": "Historial", "@History": {}, "nSongs": "{count, plural, =0{No hay canciones} =1{1 canción} other{{count} canciones}}", "@nSongs": { "description": "Número de canciones", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "Canciones", "@Songs": {}, "Albums": "Álbumes", "@Albums": {}, "Playlists": "Listas de reproducción", "@Playlists": {}, "Artists": "Artistas", "@Artists": {}, "Subscriptions": "Suscripciones", "@Subscriptions": {}, "Search_Settings": "Configuración de búsqueda", "@Search_Settings": {}, "Battery_Optimisation_title": "Optimización de batería detectada", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "Haz clic aquí para desactivar la optimización de batería para que Gyawun funcione correctamente", "@Battery_Optimisation_message": {}, "Donate": "Donar", "@Donate": {}, "Donate_Message": "Apoya el desarrollo de Gyawun", "@Donate_Message": {}, "Payment_Methods": "Métodos de pago", "@Payment_Methods": {}, "Pay_With_UPI": "Pagar con UPI", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "Apóyame en Ko-fi", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "Cómprame un café", "@Buy_Me_A_Coffee": {}, "Google_Account": "Cuenta de Google", "@Google_Account": {}, "Appearence": "Apariencia", "Theme_Mode": "Modo de tema", "@Theme_Mode": {}, "Window_Effect": "Efecto de ventana", "@Window_Effect": {}, "Dynamic_Colors": "Colores dinámicos", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "Contenido", "@Content": {}, "Country": "País", "@Country": {}, "Language": "Idioma", "@Language": {}, "Personalised_Content": "Contenido personalizado", "@Personalised_Content": {}, "Enter_Visitor_Id": "Introducir ID de visitante", "@Enter_Visitor_Id": {}, "Visitor_Id": "ID de visitante", "@Visitor_Id": {}, "Reset_Visitor_Id": "Restablecer ID de visitante", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "Audio y reproducción", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "Volumen y ecualizador", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "Mejorador de volumen", "@Loudness_Enhancer": {}, "Enable_Equalizer": "Activar ecualizador", "@Enable_Equalizer": {}, "Streaming_Quality": "Calidad de transmisión", "@Streaming_Quality": {}, "DOwnload_Quality": "Calidad de descarga", "@DOwnload_Quality": {}, "Skip_Silence": "Saltar silencio", "@Skip_Silence": {}, "Enable_Playback_History": "Activar historial de reproducción", "@Enable_Playback_History": {}, "Delete_Playback_History": "Eliminar historial de reproducción", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "¿Estás seguro de que quieres eliminar el historial de reproducción?", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "Historial de reproducción eliminado", "@Playback_History_Deleted": {}, "Enable_Search_History": "Activar historial de búsqueda", "@Enable_Search_History": {}, "Delete_Search_History": "Eliminar historial de búsqueda", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "¿Estás seguro de que quieres eliminar el historial de búsqueda?", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "Historial de búsqueda eliminado", "@Search_History_Deleted": {}, "Backup_And_Restore": "Copia de seguridad y restauración", "@Backup_And_Restore": {}, "Backup": "Copia de seguridad", "@Backup": {}, "Restore": "Restaurar", "@Restore": {}, "Select_Backup": "Seleccionar copia de seguridad", "@Select_Backup": {}, "About": "Acerca de", "@About": {}, "Name": "Nombre", "@Name": {}, "Version": "Versión", "@Version": {}, "Developer": "Desarrollador", "@Developer": {}, "Sheikh_Haziq": "Sheikh Haziq", "@Sheikh_Haziq": {}, "Organisation": "Organización", "@Organisation": {}, "Jhelum_Corp": "Jhelum Corp", "@Jhelum_Corp": {}, "Telegram": "Telegram", "@Telegram": {}, "Contributors": "Colaboradores", "@Contributors": {}, "Source_Code": "Código fuente", "@Source_Code": {}, "Bug_Report": "Reporte de errores", "@Bug_Report": {}, "Feature_Request": "Solicitud de características", "@Feature_Request": {}, "Made_In_Kashmir": "Hecho en Cachemira", "@Made_In_Kashmir": {}, "Check_For_Update": "Buscar actualizaciones", "@Check_For_Update": {}, "Progress": "Progreso", "@Progress": {}, "Play_Next": "Reproducir siguiente", "@Play_Next": {}, "Add_To_Queue": "Añadir a la cola", "@Add_To_Queue": {}, "Add_To_Favourites": "Añadir a favoritos", "@Add_To_Favourites": {}, "Remove_From_Favourites": "Eliminar de favoritos", "@Remove_From_Favourites": {}, "Download": "Descargar", "@Download": {}, "Add_To_Playlist": "Añadir a la lista de reproducción", "@Add_To_Playlist": {}, "Start_Radio": "Iniciar radio", "@Start_Radio": {}, "Album": "Álbum", "@Album": {}, "Rename": "Renombrar", "@Rename": {}, "Add_To_Library": "Añadir a la biblioteca", "@Add_To_Library": {}, "Remove_From_Library": "Eliminar de la biblioteca", "@Remove_From_Library": {}, "Delete_Item_Message": "¿Estás seguro de que quieres eliminar este elemento?", "@Delete_Item_Message": {}, "Equalizer": "Ecualizador", "@Equalizer": {}, "Sleep_Timer": "Temporizador de apagado", "@Sleep_Timer": {}, "Create_Playlist": "Crear lista de reproducción", "@Create_Playlist": {}, "Playlist_Name": "Nombre de la lista", "@Playlist_Name": {}, "Create": "Crear", "@Create": {}, "Import_Playlist": "Importar lista de reproducción", "@Import_Playlist": {}, "Import": "Importar", "@Import": {}, "Rename_Playlist": "Renombrar lista de reproducción", "@Rename_Playlist": {}, "Done": "Hecho", "@Done": {}, "Cancel": "Cancelar", "@Cancel": {}, "Confirm": "Confirmar", "@Confirm": {}, "Yes": "Sí", "@Yes": {}, "No": "No", "@No": {}, "Show_More": "Mostrar más", "@Show_More": {}, "Show_Less": "Mostrar menos", "@Show_Less": {}, "Remove": "Eliminar", "@Remove": {}, "High": "Alto", "@High": {}, "Low": "Bajo", "@Low": {}, "Songs_Will_Start_Playing_Soon": "Las canciones comenzarán a reproducirse pronto.", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "¿Estás seguro de que quieres eliminarlo?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "¿Estás seguro de que quieres eliminarlo de YTMusic?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "¿Estás seguro de que quieres borrar todo el historial?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "Copiado al portapapeles", "@Copied_To_Clipboard": {} } ================================================ FILE: lib/l10n/intl_fr.arb ================================================ { "@@locale": "fr", "Gyawun": "Gyawun", "Next_Up": "Suivant", "@Next_Up": {}, "Shuffle": "Aléatoire", "@Shuffle": {}, "@Gyawun": {}, "Home": "Accueil", "@Home": {}, "Saved": "Enregistré", "@Saved": {}, "YTMusic": "YouTube Music", "@YTMusic": {}, "Settings": "Paramètres", "@Settings": {}, "Search_Gyawun": "Rechercher sur Gyawun", "@Search_Gyawun": {}, "Favourites": "Favoris", "@Favourites": {}, "Downloads": "Téléchargements", "@Downloads": {}, "History": "Historique", "@History": {}, "nSongs": "{count, plural, =0{Pas de Titres} =1{1 Titre} other{{count} Titres}}", "@nSongs": { "description": "Nombre de titres", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "Titres", "@Songs": {}, "Albums": "Albums", "@Albums": {}, "Playlists": "Playlists", "@Playlists": {}, "Artists": "Artistes", "@Artists": {}, "Subscriptions": "Abonnements", "@Subscriptions": {}, "Search_Settings": "Paramètres de recherche", "@Search_Settings": {}, "Battery_Optimisation_title": "Optimisation de batterie détectée", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "Cliquez ici pour désactiver l'optimisation de la batterie afin que Gyawun fonctionne correctement.", "@Battery_Optimisation_message": {}, "Donate": "Faire un don", "@Donate": {}, "Donate_Message": "Soutenez le développement de Gyawun", "@Donate_Message": {}, "Payment_Methods": "Modes de Paiement", "@Payment_Methods": {}, "Pay_With_UPI": "Payer avec UPI", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "Soutenez-moi sur Ko-fi", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "Offrez-moi un café", "@Buy_Me_A_Coffee": {}, "Google_Account": "Compte Google", "@Google_Account": {}, "Appearence": "Apparence", "Theme_Mode": "Thème", "@Theme_Mode": {}, "Window_Effect": "Effet fenêtre", "@Window_Effect": {}, "Dynamic_Colors": "Couleurs Dynamiques", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "Contenu", "@Content": {}, "Country": "Pays", "@Country": {}, "Language": "Langue", "@Language": {}, "Translate_Lyrics": "Traduire les paroles", "@Translate_Lyrics": {}, "Autofetch_Songs": "Lecture automatique de titres similaires", "@Autofetch_Songs": {}, "Personalised_Content": "Contenu Personnalisé", "@Personalised_Content": {}, "Enter_Visitor_Id": "Saisir l'identifiant du visiteur", "@Enter_Visitor_Id": {}, "Visitor_Id": "Identifiant du Visiteur", "@Visitor_Id": {}, "Reset_Visitor_Id": "Réinitialiser l'identifiant du visiteur", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "Audio et Lecture", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "Volume et Égaliseur", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "Amplificateur de Volume", "@Loudness_Enhancer": {}, "Enable_Equalizer": "Activer l'égaliseur", "@Enable_Equalizer": {}, "Streaming_Quality": "Qualité du Streaming", "@Streaming_Quality": {}, "DOwnload_Quality": "Qualité du Téléchargement", "@DOwnload_Quality": {}, "App_Folder": "Dossier Application", "@App_Folder": {}, "Skip_Silence": "Ignorer le Silence", "@Skip_Silence": {}, "Enable_Playback_History": "Activer l'historique de lecture", "@Enable_Playback_History": {}, "Delete_Playback_History": "Supprimer l'historique de lecture", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "Êtes-vous sûr de vouloir supprimer l'historique de lecture ?", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "Historique de lecture supprimé", "@Playback_History_Deleted": {}, "Enable_Search_History": "Activer l'historique de recherche", "@Enable_Search_History": {}, "Delete_Search_History": "Supprimer l'historique de recherche", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "Êtes-vous sûr de vouloir supprimer l'historique de recherche ?", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "Historique de recherche supprimé", "@Search_History_Deleted": {}, "Backup_And_Restore": "Sauvegarde et Restauration", "@Backup_And_Restore": {}, "Backup": "Sauvegarde", "@Backup": {}, "Restore": "Restauration", "@Restore": {}, "Share": "Partager", "@Share": {}, "Save": "Enregistrer", "@Save": {}, "Backup_Success": "Sauvegarde effectuée avec succès à", "@Backup_Success": {}, "Backup_Failed": "Échec de la sauvegarde des données", "@Backup_Failed": {}, "Restore_Success": "Données restaurées avec succès", "@Restore_Success": {}, "Restore_Failed": "Échec de la restauration des données", "@Restore_Failed": {}, "Select_Backup": "Sélectionnez la sauvegarde", "@Select_Backup": {}, "About": "À propos", "@About": {}, "Name": "Nom", "@Name": {}, "Version": "Version", "@Version": {}, "Developer": "Développeur", "@Developer": {}, "Sheikh_Haziq": "Sheikh Haziq", "@Sheikh_Haziq": {}, "Organisation": "Organisation", "@Organisation": {}, "Jhelum_Corp": "Jhelum Corp", "@Jhelum_Corp": {}, "Telegram": "Telegram", "@Telegram": {}, "Contributors": "Contributeurs", "@Contributors": {}, "Source_Code": "Code Source", "@Source_Code": {}, "Bug_Report": "Rapport de bug", "@Bug_Report": {}, "Feature_Request": "Demande de fonctionnalité", "@Feature_Request": {}, "Made_In_Kashmir": "Fabriqué au Cachemire", "@Made_In_Kashmir": {}, "Check_For_Update": "Vérifier les mises à jour", "@Check_For_Update": {}, "Progress": "Progrès", "@Progress": {}, "Play_Next": "Lire ensuite", "@Play_Next": {}, "Add_To_Queue": "Ajouter à la file d'attente", "@Add_To_Queue": {}, "Add_To_Favourites": "Ajouter aux favoris", "@Add_To_Favourites": {}, "Remove_From_Favourites": "Supprimer des favoris", "@Remove_From_Favourites": {}, "Download": "Télécharger", "@Download": {}, "Add_To_Playlist": "Ajouter à une playlist", "@Add_To_Playlist": {}, "Start_Radio": "Démarrer la radio", "@Start_Radio": {}, "Album": "Album", "@Album": {}, "Rename": "Renommer", "@Rename": {}, "Add_To_Library": "Ajouter à la bibliothèque", "@Add_To_Library": {}, "Remove_From_Library": "Supprimer de la bibliothèque", "@Remove_From_Library": {}, "Delete_Item_Message": "Êtes-vous sûr de vouloir supprimer cet élément ?", "@Delete_Item_Message": {}, "Equalizer": "Égaliseur", "@Equalizer": {}, "Sleep_Timer": "Minuterie de Sommeil", "@Sleep_Timer": {}, "Create_Playlist": "Créer une playlist", "@Create_Playlist": {}, "Playlist_Name": "Nom de la playlist", "@Playlist_Name": {}, "Create": "Créer", "@Create": {}, "Import_Playlist": "Importer une playlist", "@Import_Playlist": {}, "Import": "Importer", "@Import": {}, "Rename_Playlist": "Renommer la playlist", "@Rename_Playlist": {}, "Done": "Terminé", "@Done": {}, "Cancel": "Annuler", "@Cancel": {}, "Confirm": "Confirmer", "@Confirm": {}, "Yes": "Oui", "@Yes": {}, "No": "Non", "@No": {}, "Show_More": "Afficher plus", "@Show_More": {}, "Show_Less": "Afficher moins", "@Show_Less": {}, "Remove": "Supprimer", "@Remove": {}, "High": "Haute", "@High": {}, "Low": "Basse", "@Low": {}, "Songs_Will_Start_Playing_Soon": "Les titres commenceront bientôt à être diffusés.", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "Êtes-vous sûr de vouloir le supprimer ?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "Êtes-vous sûr de vouloir le supprimer de YouTube Music ?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "Êtes-vous sûr de vouloir effacer tout l'historique ?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "Copié dans le presse-papiers", "@Copied_To_Clipboard": {}, "No_Internet_Connection": "Aucune connexion Internet", "@No_Internet_Connection": {}, "Go_To_Downloads": "Accéder aux téléchargements", "@Go_To_Downloads": {}, "Retry": "Réessayer", "@Retry": {}, "Playlist_Not_Available": "Playlist indisponible", "@Playlist_Not_Available": {}, "Confirm_Delete_All_Message": "Êtes-vous sûr de vouloir les supprimer ?", "@Confirm_Delete_All_Message": {}, "Downloading": "Téléchargement", "@Downloading": {}, "Restore_Missing_Songs": "Restaurer les titres manquants", "@Restore_Missing_Songs": {}, "Delete_All_Songs": "Supprimer tous les titres", "@Delete_All_Songs": {}, "Download_Started": "Téléchargement en cours...", "@Download_Started": {}, "Restoring_Missing_Songs": "Restauration des titres manquants...", "@Restoring_Missing_Songs": {}, "Deleting_Songs": "Suppression de titres...", "@Deleting_Songs": {}, "In_Progress": "En cours", "@In_Progress": {}, "Queued": "En attente", "@Queued": {}, "QueuedCount": "En attente ({count})", "@QueuedCount": {}, "FileNotFound": "Fichier introuvable", "@FileNotFound": {} } ================================================ FILE: lib/l10n/intl_hi.arb ================================================ { "@@locale": "hi", "Gyawun": "ग्यावुन", "@Gyawun": {}, "Next_Up": "अगला", "@Next_Up": {}, "Shuffle": "शफल", "@Shuffle": {}, "Home": "होम", "@Home": {}, "Saved": "सहेजे गए", "@Saved": {}, "YTMusic": "YT संगीत", "@YTMusic": {}, "Settings": "सेटिंग्स", "@Settings": {}, "Search_Gyawun": "ग्यावुन खोजें", "@Search_Gyawun": {}, "Favourites": "पसंदीदा", "@Favourites": {}, "Downloads": "डाउनलोड", "@Downloads": {}, "History": "इतिहास", "@History": {}, "nSongs": "{count, plural, =0{कोई गाने नहीं} =1{1 गाना} other{{count} गाने}}", "@nSongs": { "description": "गानों की संख्या", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "गाने", "@Songs": {}, "Albums": "एल्बम", "@Albums": {}, "Playlists": "प्लेलिस्ट", "@Playlists": {}, "Artists": "कलाकार", "@Artists": {}, "Subscriptions": "सब्सक्रिप्शन", "@Subscriptions": {}, "Search_Settings": "खोज सेटिंग्स", "@Search_Settings": {}, "Battery_Optimisation_title": "बैटरी ऑप्टिमाइजेशन पता चला", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "ग्यावुन को ठीक से काम करने के लिए बैटरी ऑप्टिमाइजेशन को अक्षम करने के लिए यहां क्लिक करें", "@Battery_Optimisation_message": {}, "Donate": "दान करें", "@Donate": {}, "Donate_Message": "ग्यावुन के विकास का समर्थन करें", "@Donate_Message": {}, "Payment_Methods": "भुगतान के तरीके", "@Payment_Methods": {}, "Pay_With_UPI": "UPI के साथ भुगतान करें", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "को-फ़ी पर मुझे समर्थन दें", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "मुझे एक कॉफी खरीदें", "@Buy_Me_A_Coffee": {}, "Google_Account": "गूगल अकाउंट", "@Google_Account": {}, "Appearence": "दिखावट", "Theme_Mode": "थीम मोड", "@Theme_Mode": {}, "Window_Effect": "विंडो प्रभाव", "@Window_Effect": {}, "Dynamic_Colors": "डायनामिक रंग", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "सामग्री", "@Content": {}, "Country": "देश", "@Country": {}, "Language": "भाषा", "@Language": {}, "Personalised_Content": "व्यक्तिगत सामग्री", "@Personalised_Content": {}, "Enter_Visitor_Id": "विज़िटर आईडी दर्ज करें", "@Enter_Visitor_Id": {}, "Visitor_Id": "विज़िटर आईडी", "@Visitor_Id": {}, "Reset_Visitor_Id": "विज़िटर आईडी रीसेट करें", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "ऑडियो और प्लेबैक", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "लाउडनेस और इक्वलाइज़र", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "लाउडनेस एन्हांसर", "@Loudness_Enhancer": {}, "Enable_Equalizer": "इक्वलाइज़र सक्षम करें", "@Enable_Equalizer": {}, "Streaming_Quality": "स्ट्रीमिंग गुणवत्ता", "@Streaming_Quality": {}, "DOwnload_Quality": "डाउनलोड गुणवत्ता", "@DOwnload_Quality": {}, "Skip_Silence": "चुप्पी छोड़ें", "@Skip_Silence": {}, "Enable_Playback_History": "प्लेबैक इतिहास सक्षम करें", "@Enable_Playback_History": {}, "Delete_Playback_History": "प्लेबैक इतिहास हटाएं", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "क्या आप वाकई प्लेबैक इतिहास हटाना चाहते हैं।", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "प्लेबैक इतिहास हटाया गया", "@Playback_History_Deleted": {}, "Enable_Search_History": "खोज इतिहास सक्षम करें", "@Enable_Search_History": {}, "Delete_Search_History": "खोज इतिहास हटाएं", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "क्या आप वाकई खोज इतिहास हटाना चाहते हैं।", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "खोज इतिहास हटाया गया", "@Search_History_Deleted": {}, "Backup_And_Restore": "बैकअप और पुनर्स्थापना", "@Backup_And_Restore": {}, "Backup": "बैकअप", "@Backup": {}, "Restore": "पुनर्स्थापित", "@Restore": {}, "Select_Backup": "बैकअप चुनें", "@Select_Backup": {}, "About": "के बारे में", "@About": {}, "Name": "नाम", "@Name": {}, "Version": "संस्करण", "@Version": {}, "Developer": "डेवलपर", "@Developer": {}, "Sheikh_Haziq": "शेख हाजिक", "@Sheikh_Haziq": {}, "Organisation": "संगठन", "@Organisation": {}, "Jhelum_Corp": "झेलम कॉर्प", "@Jhelum_Corp": {}, "Telegram": "टेलीग्राम", "@Telegram": {}, "Contributors": "योगदानकर्ता", "@Contributors": {}, "Source_Code": "सोर्स कोड", "@Source_Code": {}, "Bug_Report": "बग रिपोर्ट", "@Bug_Report": {}, "Feature_Request": "फीचर अनुरोध", "@Feature_Request": {}, "Made_In_Kashmir": "कश्मीर में बना", "@Made_In_Kashmir": {}, "Check_For_Update": "अपडेट के लिए जाँचें", "@Check_For_Update": {}, "Progress": "प्रगति", "@Progress": {}, "Play_Next": "अगला चलाएं", "@Play_Next": {}, "Add_To_Queue": "कतार में जोड़ें", "@Add_To_Queue": {}, "Add_To_Favourites": "पसंदीदा में जोड़ें", "@Add_To_Favourites": {}, "Remove_From_Favourites": "पसंदीदा से हटाएं", "@Remove_From_Favourites": {}, "Download": "डाउनलोड", "@Download": {}, "Add_To_Playlist": "प्लेलिस्ट में जोड़ें", "@Add_To_Playlist": {}, "Start_Radio": "रेडियो शुरू करें", "@Start_Radio": {}, "Album": "एल्बम", "@Album": {}, "Rename": "नाम बदलें", "@Rename": {}, "Add_To_Library": "लाइब्रेरी में जोड़ें", "@Add_To_Library": {}, "Remove_From_Library": "लाइब्रेरी से हटाएं", "@Remove_From_Library": {}, "Delete_Item_Message": "क्या आप वाकई इस आइटम को हटाना चाहते हैं?", "@Delete_Item_Message": {}, "Equalizer": "इक्वलाइज़र", "@Equalizer": {}, "Sleep_Timer": "स्लीप टाइमर", "@Sleep_Timer": {}, "Create_Playlist": "प्लेलिस्ट बनाएं", "@Create_Playlist": {}, "Playlist_Name": "प्लेलिस्ट का नाम", "@Playlist_Name": {}, "Create": "बनाएं", "@Create": {}, "Import_Playlist": "प्लेलिस्ट आयात करें", "@Import_Playlist": {}, "Import": "आयात करें", "@Import": {}, "Rename_Playlist": "प्लेलिस्ट का नाम बदलें", "@Rename_Playlist": {}, "Done": "हो गया", "@Done": {}, "Cancel": "रद्द करें", "@Cancel": {}, "Confirm": "पुष्टि करें", "@Confirm": {}, "Yes": "हाँ", "@Yes": {}, "No": "नहीं", "@No": {}, "Show_More": "और दिखाएं", "@Show_More": {}, "Show_Less": "कम दिखाएं", "@Show_Less": {}, "Remove": "हटाएं", "@Remove": {}, "High": "उच्च", "@High": {}, "Low": "निम्न", "@Low": {}, "Songs_Will_Start_Playing_Soon": "गाने जल्द ही बजना शुरू हो जाएंगे।", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "क्या आप वाकई इसे हटाना चाहते हैं?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "क्या आप वाकई इसे YT संगीत से हटाना चाहते हैं?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "क्या आप वाकई सभी इतिहास साफ़ करना चाहते हैं?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "क्लिपबोर्ड पर कॉपी किया गया", "@Copied_To_Clipboard": {} } ================================================ FILE: lib/l10n/intl_it.arb ================================================ { "@@locale": "it", "Gyawun": "Gyawun", "@Gyawun": {}, "Next_Up": "Prossimo", "@Next_Up": {}, "Shuffle": "Casuale", "@Shuffle": {}, "Home": "Home", "@Home": {}, "Saved": "Salvati", "@Saved": {}, "YTMusic": "YTMusic", "@YTMusic": {}, "Settings": "Impostazioni", "@Settings": {}, "Search_Gyawun": "Cerca in Gyawun", "@Search_Gyawun": {}, "Favourites": "Preferiti", "@Favourites": {}, "Downloads": "Download", "@Downloads": {}, "History": "Cronologia", "@History": {}, "nSongs": "{count, plural, =0{Nessun brano} =1{1 brano} other{{count} brani}}", "@nSongs": { "description": "Numero di brani", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "Brani", "@Songs": {}, "Albums": "Album", "@Albums": {}, "Playlists": "Playlist", "@Playlists": {}, "Artists": "Artisti", "@Artists": {}, "Subscriptions": "Iscrizioni", "@Subscriptions": {}, "Search_Settings": "Impostazioni di ricerca", "@Search_Settings": {}, "Battery_Optimisation_title": "Ottimizzazione batteria rilevata", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "Clicca qui per disattivare l’ottimizzazione della batteria e permettere a Gyawun di funzionare correttamente", "@Battery_Optimisation_message": {}, "Donate": "Dona", "@Donate": {}, "Donate_Message": "Supporta lo sviluppo di Gyawun", "@Donate_Message": {}, "Payment_Methods": "Metodi di pagamento", "@Payment_Methods": {}, "Pay_With_UPI": "Paga con UPI", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "Supportami su Ko-fi", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "Offrimi un caffè", "@Buy_Me_A_Coffee": {}, "Google_Account": "Account Google", "@Google_Account": {}, "Appearence": "Aspetto", "@Appearence": {}, "Theme_Mode": "Tema", "@Theme_Mode": {}, "Window_Effect": "Effetto finestra", "@Window_Effect": {}, "Dynamic_Colors": "Colori dinamici", "@Dynamic_Colors": {}, "Content": "Contenuti", "@Content": {}, "Country": "Paese", "@Country": {}, "Language": "Lingua", "@Language": {}, "Translate_Lyrics": "Traduci testi", "@Translate_Lyrics": {}, "Autofetch_Songs": "Riproduci automaticamente brani simili", "@Autofetch_Songs": {}, "Personalised_Content": "Contenuti personalizzati", "@Personalised_Content": {}, "Enter_Visitor_Id": "Inserisci Visitor ID", "@Enter_Visitor_Id": {}, "Visitor_Id": "Visitor ID", "@Visitor_Id": {}, "Reset_Visitor_Id": "Reimposta Visitor ID", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "Audio e riproduzione", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "Volume ed equalizzatore", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "Amplificatore volume", "@Loudness_Enhancer": {}, "Enable_Equalizer": "Abilita equalizzatore", "@Enable_Equalizer": {}, "Streaming_Quality": "Qualità streaming", "@Streaming_Quality": {}, "DOwnload_Quality": "Qualità download", "@DOwnload_Quality": {}, "App_Folder": "Cartella App", "@App_Folder": {}, "Skip_Silence": "Salta silenzi", "@Skip_Silence": {}, "Enable_Playback_History": "Abilita cronologia di riproduzione", "@Enable_Playback_History": {}, "Delete_Playback_History": "Elimina cronologia di riproduzione", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "Sei sicuro di voler eliminare la cronologia di riproduzione?", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "Cronologia di riproduzione eliminata", "@Playback_History_Deleted": {}, "Enable_Search_History": "Abilita cronologia di ricerca", "@Enable_Search_History": {}, "Delete_Search_History": "Elimina cronologia di ricerca", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "Sei sicuro di voler eliminare la cronologia di ricerca?", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "Cronologia di ricerca eliminata", "@Search_History_Deleted": {}, "Backup_And_Restore": "Backup e ripristino", "@Backup_And_Restore": {}, "Backup": "Backup", "@Backup": {}, "Restore": "Ripristina", "@Restore": {}, "Share": "Condividi", "@Share": {}, "Save": "Salva", "@Save": {}, "Backup_Success": "Salvataggio dati riuscito al percorso", "@Backup_Success": {}, "Backup_Failed": "Salvataggio dati non riuscito", "@Backup_Failed": {}, "Restore_Success": "Ripristino dati riuscito", "@Restore_Success": {}, "Restore_Failed": "Ripristino dati non riuscito", "@Restore_Failed": {}, "Select_Backup": "Seleziona backup", "@Select_Backup": {}, "About": "Informazioni", "@About": {}, "Name": "Nome", "@Name": {}, "Version": "Versione", "@Version": {}, "Developer": "Sviluppatore", "@Developer": {}, "Sheikh_Haziq": "Sheikh Haziq", "@Sheikh_Haziq": {}, "Organisation": "Organizzazione", "@Organisation": {}, "Jhelum_Corp": "Jhelum Corp", "@Jhelum_Corp": {}, "Telegram": "Telegram", "@Telegram": {}, "Contributors": "Collaboratori", "@Contributors": {}, "Source_Code": "Codice sorgente", "@Source_Code": {}, "Bug_Report": "Segnala un bug", "@Bug_Report": {}, "Feature_Request": "Richiesta funzionalità", "@Feature_Request": {}, "Made_In_Kashmir": "Realizzato in Kashmir", "@Made_In_Kashmir": {}, "Check_For_Update": "Verifica aggiornamenti", "@Check_For_Update": {}, "Progress": "Avanzamento", "@Progress": {}, "Play_Next": "Riproduci dopo", "@Play_Next": {}, "Add_To_Queue": "Aggiungi alla coda", "@Add_To_Queue": {}, "Add_To_Favourites": "Aggiungi ai preferiti", "@Add_To_Favourites": {}, "Remove_From_Favourites": "Rimuovi dai preferiti", "@Remove_From_Favourites": {}, "Download": "Download", "@Download": {}, "Add_To_Playlist": "Aggiungi alla playlist", "@Add_To_Playlist": {}, "Start_Radio": "Avvia radio", "@Start_Radio": {}, "Album": "Album", "@Album": {}, "Rename": "Rinomina", "@Rename": {}, "Add_To_Library": "Aggiungi alla libreria", "@Add_To_Library": {}, "Remove_From_Library": "Rimuovi dalla libreria", "@Remove_From_Library": {}, "Delete_Item_Message": "Sei sicuro di voler eliminare questo elemento?", "@Delete_Item_Message": {}, "Equalizer": "Equalizzatore", "@Equalizer": {}, "Sleep_Timer": "Timer di spegnimento", "@Sleep_Timer": {}, "Create_Playlist": "Crea playlist", "@Create_Playlist": {}, "Playlist_Name": "Nome playlist", "@Playlist_Name": {}, "Create": "Crea", "@Create": {}, "Import_Playlist": "Importa playlist", "@Import_Playlist": {}, "Import": "Importa", "@Import": {}, "Rename_Playlist": "Rinomina playlist", "@Rename_Playlist": {}, "Done": "Fatto", "@Done": {}, "Cancel": "Annulla", "@Cancel": {}, "Confirm": "Conferma", "@Confirm": {}, "Yes": "Sì", "@Yes": {}, "No": "No", "@No": {}, "Show_More": "Mostra di più", "@Show_More": {}, "Show_Less": "Mostra meno", "@Show_Less": {}, "Remove": "Rimuovi", "@Remove": {}, "High": "Alta", "@High": {}, "Low": "Bassa", "@Low": {}, "Songs_Will_Start_Playing_Soon": "La riproduzione dei brani inizierà a breve.", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "Sei sicuro di volerlo rimuovere?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "Sei sicuro di volerlo rimuovere da YTMusic?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "Sei sicuro di voler cancellare tutta la cronologia?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "Copiato negli appunti", "@Copied_To_Clipboard": {}, "No_Internet_Connection": "Nessuna Connessione Internet", "@No_Internet_Connection": {}, "Go_To_Downloads": "Vai a Download", "@Go_To_Downloads": {}, "Retry": "Riprova", "@Retry": {}, "Playlist_Not_Available": "Playlist non disponibile", "@Playlist_Not_Available": {}, "Confirm_Delete_All_Message": "Sei sicuro di volerli eliminare?", "@Confirm_Delete_All_Message": {}, "Downloading": "In download", "@Downloading": {}, "Restore_Missing_Songs": "Ripristina brani mancanti", "@Restore_Missing_Songs": {}, "Delete_All_Songs": "Elimina tutti i brani", "@Delete_All_Songs": {}, "Download_Started": "Download avviato...", "@Download_Started": {}, "Restoring_Missing_Songs": "Ripristino brani mancanti...", "@Restoring_Missing_Songs": {}, "Deleting_Songs": "Eliminazione brani...", "@Deleting_Songs": {}, "In_Progress": "In corso", "@In_Progress": {}, "Queued": "In coda", "@Queued": {}, "Queued_Count": "In coda ({count})", "@Queued_Count": {}, "File_Not_Found": "File non trovato", "@File_Not_Found": {}, "Play_All": "Riproduci", "@Play_All": {}, "Rotate_Device": "Ruota il dispositivo per scrivere.", "@Rotate_Device": {}, "View_Equalizer": "Riproduci un brano per vedere l'equalizzatore.", "@View_Equalizer": {}, "No_Offline_Songs": "Nessun brano scaricato", "@No_Offline_Songs": {}, "Edit": "Modifica", "@Edit": {}, "Edit_Playlist": "Modifica Playlist", "@Edit_Playlist": {}, "Select_Playlist_Icon": "Seleziona Icona Playlist", "@Select_Playlist_Icon": {}, "Top_Results": "Risultati Principali", "@Top_Results": {}, "Other_Results": "Altri Risultati", "@Other_Results": {} } ================================================ FILE: lib/l10n/intl_tr.arb ================================================ { "@@locale": "tr", "Gyawun": "Gyawun", "Next_Up": "Sıradaki", "@Next_Up": {}, "Shuffle": "Karıştır", "@Shuffle": {}, "@Gyawun": {}, "Home": "Ana Sayfa", "@Home": {}, "Saved": "Kaydedilenler", "@Saved": {}, "YTMusic": "YTMüzik", "@YTMusic": {}, "Settings": "Ayarlar", "@Settings": {}, "Search_Gyawun": "Gyawun Ara", "@Search_Gyawun": {}, "Favourites": "Favoriler", "@Favourites": {}, "Downloads": "İndirilenler", "@Downloads": {}, "History": "Geçmiş", "@History": {}, "nSongs": "{count, plural, =0{Şarkı Yok} =1{1 Şarkı} other{{count} Şarkı}}", "@nSongs": { "description": "Şarkı sayısı", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "Şarkılar", "@Songs": {}, "Albums": "Albümler", "@Albums": {}, "Playlists": "Çalma Listeleri", "@Playlists": {}, "Artists": "Sanatçılar", "@Artists": {}, "Subscriptions": "Abonelikler", "@Subscriptions": {}, "Search_Settings": "Arama Ayarları", "@Search_Settings": {}, "Battery_Optimisation_title": "Pil Optimizasyonu Tespit Edildi", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "Gyawun un düzgün çalışması için pil optimizasyonunu devre dışı bırakmak için buraya tıklayın", "@Battery_Optimisation_message": {}, "Donate": "Bağış Yap", "@Donate": {}, "Donate_Message": "Gyawun un geliştirilmesini destekleyin", "@Donate_Message": {}, "Payment_Methods": "Ödeme Yöntemleri", "@Payment_Methods": {}, "Pay_With_UPI": "UPI ile Öde", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "Ko-fi üzerinden bana destek ol", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "Bana Bir Kahve Al", "@Buy_Me_A_Coffee": {}, "Google_Account": "Google Hesabı", "@Google_Account": {}, "Appearence": "Görünüm", "Theme_Mode": "Tema Modu", "@Theme_Mode": {}, "Window_Effect": "Pencere Efekti", "@Window_Effect": {}, "Dynamic_Colors": "Dinamik Renkler", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "İçerik", "@Content": {}, "Country": "Ülke", "@Country": {}, "Language": "Dil", "@Language": {}, "Personalised_Content": "Kişiselleştirilmiş İçerik", "@Personalised_Content": {}, "Enter_Visitor_Id": "Ziyaretçi Kimliği Girin", "@Enter_Visitor_Id": {}, "Visitor_Id": "Ziyaretçi Kimliği", "@Visitor_Id": {}, "Reset_Visitor_Id": "Ziyaretçi Kimliğini Sıfırla", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "Ses ve Yeniden Oynatma", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "Ses Yüksekliği ve Ekolayzer", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "Ses Yükseltici", "@Loudness_Enhancer": {}, "Enable_Equalizer": "Ekolayzeri Etkinleştir", "@Enable_Equalizer": {}, "Streaming_Quality": "Yayın Kalitesi", "@Streaming_Quality": {}, "DOwnload_Quality": "İndirme Kalitesi", "@DOwnload_Quality": {}, "Skip_Silence": "Sessizliği Atla", "@Skip_Silence": {}, "Enable_Playback_History": "Yeniden Oynatma Geçmişini Etkinleştir", "@Enable_Playback_History": {}, "Delete_Playback_History": "Yeniden Oynatma Geçmişini Sil", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "Yeniden Oynatma Geçmişini silmek istediğinizden emin misiniz.", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "Yeniden Oynatma Geçmişi Silindi", "@Playback_History_Deleted": {}, "Enable_Search_History": "Arama Geçmişini Etkinleştir", "@Enable_Search_History": {}, "Delete_Search_History": "Arama Geçmişini Sil", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "Arama Geçmişini silmek istediğinizden emin misiniz.", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "Arama Geçmişi Silindi", "@Search_History_Deleted": {}, "Backup_And_Restore": "Yedekle ve Geri Yükle", "@Backup_And_Restore": {}, "Backup": "Yedekle", "@Backup": {}, "Restore": "Geri Yükle", "@Restore": {}, "Select_Backup": "Yedeği Seç", "@Select_Backup": {}, "About": "Hakkında", "@About": {}, "Name": "İsim", "@Name": {}, "Version": "Versiyon", "@Version": {}, "Developer": "Geliştirici", "@Developer": {}, "Sheikh_Haziq": "Sheikh Haziq", "@Sheikh_Haziq": {}, "Organisation": "Organizasyon", "@Organisation": {}, "Jhelum_Corp": "Jhelum Corp", "@Jhelum_Corp": {}, "Telegram": "Telegram", "@Telegram": {}, "Contributors": "Katkıda Bulunanlar", "@Contributors": {}, "Source_Code": "Kaynak Kodu", "@Source_Code": {}, "Bug_Report": "Hata Raporu", "@Bug_Report": {}, "Feature_Request": "Özellik İsteği", "@Feature_Request": {}, "Made_In_Kashmir": "Keşmir de Yapıldı", "@Made_In_Kashmir": {}, "Check_For_Update": "Güncellemeleri Kontrol Et", "@Check_For_Update": {}, "Progress": "İlerleme", "@Progress": {}, "Play_Next": "Sonraki Oynat", "@Play_Next": {}, "Add_To_Queue": "Kuyruğa Ekle", "@Add_To_Queue": {}, "Add_To_Favourites": "Favorilere Ekle", "@Add_To_Favourites": {}, "Remove_From_Favourites": "Favorilerden Çıkar", "@Remove_From_Favourites": {}, "Download": "İndir", "@Download": {}, "Add_To_Playlist": "Çalma Listesine Ekle", "@Add_To_Playlist": {}, "Start_Radio": "Radyo Başlat", "@Start_Radio": {}, "Album": "Albüm", "@Album": {}, "Rename": "Yeniden Adlandır", "@Rename": {}, "Add_To_Library": "Kütüphaneye Ekle", "@Add_To_Library": {}, "Remove_From_Library": "Kütüphaneden Çıkar", "@Remove_From_Library": {}, "Delete_Item_Message": "Bu öğeyi silmek istediğinizden emin misiniz?", "@Delete_Item_Message": {}, "Equalizer": "Ekolayzer", "@Equalizer": {}, "Sleep_Timer": "Uyku Zamanlayıcı", "@Sleep_Timer": {}, "Create_Playlist": "Çalma Listesi Oluştur", "@Create_Playlist": {}, "Playlist_Name": "Çalma Listesi Adı", "@Playlist_Name": {}, "Create": "Oluştur", "@Create": {}, "Import_Playlist": "Çalma Listesi İçe Aktar", "@Import_Playlist": {}, "Import": "İçe Aktar", "@Import": {}, "Rename_Playlist": "Çalma Listesini Yeniden Adlandır", "@Rename_Playlist": {}, "Done": "Tamam", "@Done": {}, "Cancel": "İptal", "@Cancel": {}, "Confirm": "Onayla", "@Confirm": {}, "Yes": "Evet", "@Yes": {}, "No": "Hayır", "@No": {}, "Show_More": "Daha Fazla Göster", "@Show_More": {}, "Show_Less": "Daha Az Göster", "@Show_Less": {}, "Remove": "Kaldır", "@Remove": {}, "High": "Yüksek", "@High": {}, "Low": "Düşük", "@Low": {}, "Songs_Will_Start_Playing_Soon": "Şarkılar yakında çalmaya başlayacak.", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "Kaldırmak istediğinizden emin misiniz?", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "YTMüzikten kaldırmak istediğinizden emin misiniz?", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "Tüm geçmişi temizlemek istediğinizden emin misiniz?", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "Panoya Kopyalandı", "@Copied_To_Clipboard": {} } ================================================ FILE: lib/l10n/intl_ur.arb ================================================ { "@@locale": "ur", "Gyawun": "گیاون", "Next_Up": "اگلا", "@Next_Up": {}, "Shuffle": "بے ترتیب", "@Shuffle": {}, "@Gyawun": {}, "Home": "ہوم", "@Home": {}, "Saved": "محفوظ کردہ", "@Saved": {}, "YTMusic": "وائی ٹی میوزک", "@YTMusic": {}, "Settings": "ترتیبات", "@Settings": {}, "Search_Gyawun": "گیاون تلاش کریں", "@Search_Gyawun": {}, "Favourites": "پسندیدہ", "@Favourites": {}, "Downloads": "ڈاؤن لوڈ", "@Downloads": {}, "History": "تاریخ", "@History": {}, "nSongs": "{count, plural, =0{کوئی گانے نہیں} =1{1 گانا} other{{count} گانے}}", "@nSongs": { "description": "گانوں کی تعداد", "placeholders": { "count": { "type": "num", "format": "compact" } } }, "Songs": "گانے", "@Songs": {}, "Albums": "البمز", "@Albums": {}, "Playlists": "پلے لسٹس", "@Playlists": {}, "Artists": "فنکار", "@Artists": {}, "Subscriptions": "سبسکرپشنز", "@Subscriptions": {}, "Search_Settings": "تلاش کی ترتیبات", "@Search_Settings": {}, "Battery_Optimisation_title": "بیٹری کی اصلاح کا پتہ چلا", "@Battery_Optimisation_title": {}, "Battery_Optimisation_message": "گیاون کو درست طریقے سے کام کرنے کے لئے بیٹری کی اصلاح کو غیر فعال کرنے کے لئے یہاں کلک کریں", "@Battery_Optimisation_message": {}, "Donate": "عطیہ", "@Donate": {}, "Donate_Message": "گیاون کی ترقی کی حمایت کریں", "@Donate_Message": {}, "Payment_Methods": "ادائیگی کے طریقے", "@Payment_Methods": {}, "Pay_With_UPI": "یو پی آئی کے ساتھ ادائیگی", "@Pay_With_UPI": {}, "Support_Me_On_Kofi": "کوفی پر مجھے سپورٹ کریں", "@Support_Me_On_Kofi": {}, "Buy_Me_A_Coffee": "مجھے ایک کافی خریدیں", "@Buy_Me_A_Coffee": {}, "Google_Account": "گوگل اکاؤنٹ", "@Google_Account": {}, "Appearence": "ظاہری شکل", "Theme_Mode": "تھیم موڈ", "@Theme_Mode": {}, "Window_Effect": "ونڈو ایفیکٹ", "@Window_Effect": {}, "Dynamic_Colors": "ڈائنامک رنگ", "@Dynamic_Colors": {}, "@Appearence": {}, "Content": "مواد", "@Content": {}, "Country": "ملک", "@Country": {}, "Language": "زبان", "@Language": {}, "Personalised_Content": "ذاتی بنایا گیا مواد", "@Personalised_Content": {}, "Enter_Visitor_Id": "وزیٹر آئی ڈی درج کریں", "@Enter_Visitor_Id": {}, "Visitor_Id": "وزیٹر آئی ڈی", "@Visitor_Id": {}, "Reset_Visitor_Id": "وزیٹر آئی ڈی دوبارہ ترتیب دیں", "@Reset_Visitor_Id": {}, "Audio_And_Playback": "آڈیو اور پلے بیک", "@Audio_And_Playback": {}, "Loudness_And_Equalizer": "لاؤڈنس اور ایکوالائزر", "@Loudness_And_Equalizer": {}, "Loudness_Enhancer": "لاؤڈنس انہینسر", "@Loudness_Enhancer": {}, "Enable_Equalizer": "ایکوالائزر کو فعال کریں", "@Enable_Equalizer": {}, "Streaming_Quality": "سٹریمنگ کوالٹی", "@Streaming_Quality": {}, "DOwnload_Quality": "ڈاؤن لوڈ کوالٹی", "@DOwnload_Quality": {}, "Skip_Silence": "خاموشی کو چھوڑ دیں", "@Skip_Silence": {}, "Enable_Playback_History": "پلے بیک ہسٹری کو فعال کریں", "@Enable_Playback_History": {}, "Delete_Playback_History": "پلے بیک ہسٹری کو حذف کریں", "@Delete_Playback_History": {}, "Delete_Playback_History_Confirm_Message": "کیا آپ واقعی پلے بیک ہسٹری کو حذف کرنا چاہتے ہیں؟", "@Delete_Playback_History_Confirm_Message": {}, "Playback_History_Deleted": "پلے بیک ہسٹری حذف کر دی گئی", "@Playback_History_Deleted": {}, "Enable_Search_History": "تلاش کی ہسٹری کو فعال کریں", "@Enable_Search_History": {}, "Delete_Search_History": "تلاش کی ہسٹری کو حذف کریں", "@Delete_Search_History": {}, "Delete_Search_History_Confirm_Message": "کیا آپ واقعی تلاش کی ہسٹری کو حذف کرنا چاہتے ہیں؟", "@Delete_Search_History_Confirm_Message": {}, "Search_History_Deleted": "تلاش کی ہسٹری حذف کر دی گئی", "@Search_History_Deleted": {}, "Backup_And_Restore": "بیک اپ اور بحالی", "@Backup_And_Restore": {}, "Backup": "بیک اپ", "@Backup": {}, "Restore": "بحال کریں", "@Restore": {}, "Select_Backup": "بیک اپ منتخب کریں", "@Select_Backup": {}, "About": "کے بارے میں", "@About": {}, "Name": "نام", "@Name": {}, "Version": "ورژن", "@Version": {}, "Developer": "ڈویلپر", "@Developer": {}, "Sheikh_Haziq": "شیخ ہازق", "@Sheikh_Haziq": {}, "Organisation": "تنظیم", "@Organisation": {}, "Jhelum_Corp": "جہلم کارپ", "@Jhelum_Corp": {}, "Telegram": "ٹیلیگرام", "@Telegram": {}, "Contributors": "شراکت دار", "@Contributors": {}, "Source_Code": "سورس کوڈ", "@Source_Code": {}, "Bug_Report": "بگ رپورٹ", "@Bug_Report": {}, "Feature_Request": "خصوصیت کی درخواست", "@Feature_Request": {}, "Made_In_Kashmir": "کشمیر میں بنایا گیا", "@Made_In_Kashmir": {}, "Check_For_Update": "اپ ڈیٹ کے لئے چیک کریں", "@Check_For_Update": {}, "Progress": "پیش رفت", "@Progress": {}, "Play_Next": "اگلا چلائیں", "@Play_Next": {}, "Add_To_Queue": "قطار میں شامل کریں", "@Add_To_Queue": {}, "Add_To_Favourites": "پسندیدہ میں شامل کریں", "@Add_To_Favourites": {}, "Remove_From_Favourites": "پسندیدہ سے ہٹائیں", "@Remove_From_Favourites": {}, "Download": "ڈاؤن لوڈ", "@Download": {}, "Add_To_Playlist": "پلے لسٹ میں شامل کریں", "@Add_To_Playlist": {}, "Start_Radio": "ریڈیو شروع کریں", "@Start_Radio": {}, "Album": "البم", "@Album": {}, "Rename": "نام تبدیل کریں", "@Rename": {}, "Add_To_Library": "لائبریری میں شامل کریں", "@Add_To_Library": {}, "Remove_From_Library": "لائبریری سے ہٹائیں", "@Remove_From_Library": {}, "Delete_Item_Message": "کیا آپ واقعی اس آئٹم کو حذف کرنا چاہتے ہیں؟", "@Delete_Item_Message": {}, "Equalizer": "ایکولائزر", "@Equalizer": {}, "Sleep_Timer": "سلیپ ٹائمر", "@Sleep_Timer": {}, "Create_Playlist": "پلے لسٹ بنائیں", "@Create_Playlist": {}, "Playlist_Name": "پلے لسٹ کا نام", "@Playlist_Name": {}, "Create": "بنائیں", "@Create": {}, "Import_Playlist": "پلے لسٹ درآمد کریں", "@Import_Playlist": {}, "Import": "درآمد", "@Import": {}, "Rename_Playlist": "پلے لسٹ کا نام تبدیل کریں", "@Rename_Playlist": {}, "Done": "ہو گیا", "@Done": {}, "Cancel": "منسوخ کریں", "@Cancel": {}, "Confirm": "تصدیق کریں", "@Confirm": {}, "Yes": "ہاں", "@Yes": {}, "No": "نہیں", "@No": {}, "Show_More": "مزید دکھائیں", "@Show_More": {}, "Show_Less": "کم دکھائیں", "@Show_Less": {}, "Remove": "ہٹائیں", "@Remove": {}, "High": "زیادہ", "@High": {}, "Low": "کم", "@Low": {}, "Songs_Will_Start_Playing_Soon": "گانے جلد ہی چلنا شروع ہوجائیں گے۔", "@Songs_Will_Start_Playing_Soon": {}, "Remove_Message": "کیا آپ واقعی اسے ہٹانا چاہتے ہیں؟", "@Remove_Message": {}, "Remove_From_YTMusic_Message": "کیا آپ واقعی اسے YTMusic سے ہٹانا چاہتے ہیں؟", "@Remove_From_YTMusic_Message": {}, "Remove_All_History_Message": "کیا آپ واقعی تمام تاریخ کو صاف کرنا چاہتے ہیں؟", "@Remove_All_History_Message": {}, "Copied_To_Clipboard": "کلپ بورڈ پر کاپی کر دیا گیا", "@Copied_To_Clipboard": {} } ================================================ FILE: lib/main.dart ================================================ import 'dart:io'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/themes/theme.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:yt_music/client.dart'; import 'package:yt_music/modals/yt_config.dart'; import 'package:yt_music/ytmusic.dart'; import 'generated/l10n.dart'; import 'services/download_manager.dart'; import 'services/favourites_manager.dart'; import 'services/file_storage.dart'; import 'services/history_manager.dart'; import 'services/library.dart'; import 'services/lyrics.dart'; import 'services/media_player.dart'; import 'services/settings_manager.dart'; import 'utils/router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); if (Platform.isAndroid) { await JustAudioBackground.init( androidNotificationChannelId: 'com.jhelum.gyawun.audio', androidNotificationChannelName: 'Audio playback', androidNotificationOngoing: true, // androidStopForegroundOnPause: false, ); } if (Platform.isWindows || Platform.isLinux) { JustAudioMediaKit.ensureInitialized(); JustAudioMediaKit.bufferSize = 8 * 1024 * 1024; JustAudioMediaKit.title = 'Gyawun Music'; JustAudioMediaKit.prefetchPlaylist = true; JustAudioMediaKit.pitch = true; } await initialiseHive(); await SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top], ); await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); SettingsManager settingsManager = await SettingsManager.create(); GetIt.I.registerSingleton(settingsManager); final ytConfig = await getYtConfig(settingsManager); YTMusic ytMusic = YTMusic(config: ytConfig!); GetIt.I.registerSingleton(ytMusic); final GlobalKey panelKey = GlobalKey(); GetIt.I.registerSingleton(panelKey); FileStorage fileStorage = await FileStorage.create(); GetIt.I.registerSingleton(fileStorage); MediaPlayer mediaPlayer = MediaPlayer(); GetIt.I.registerSingleton(mediaPlayer); LibraryService libraryService = await LibraryService.create(); GetIt.I.registerSingleton(libraryService); DownloadManager downloadManager = await DownloadManager.create(); GetIt.I.registerSingleton(downloadManager); HistoryManager historyManager = await HistoryManager.create(); GetIt.I.registerSingleton(historyManager); GetIt.I.registerSingleton(Lyrics()); FavouritesManager favouritesManager = await FavouritesManager.create(); GetIt.I.registerSingleton(favouritesManager); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => settingsManager), ChangeNotifierProvider(create: (_) => mediaPlayer), ChangeNotifierProvider(create: (_) => libraryService), ], child: const Gyawun(), ), ); } class Gyawun extends StatelessWidget { const Gyawun({super.key}); @override Widget build(BuildContext context) { final settings = context.select( (SettingsManager s) => ( language: s.language['value']!, themeMode: s.themeMode, dynamicColors: s.dynamicColors, accentColor: s.accentColor, amoledBlack: s.amoledBlack, ), ); return DynamicColorBuilder( builder: (lightScheme, darkScheme) { final primaryColor = (settings.dynamicColors && darkScheme != null ? darkScheme.primary : settings.accentColor) ?? Colors.red; final isPureBlack = settings.amoledBlack; return Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), }, child: MaterialApp.router( title: 'Gyawun Music', routerConfig: router, locale: Locale(settings.language), localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: S.delegate.supportedLocales, debugShowCheckedModeBanner: false, themeMode: settings.themeMode, theme: ColorScheme.fromSeed( seedColor: primaryColor, ).toM3EThemeData(base: AppTheme.light(primary: primaryColor)), darkTheme: ColorScheme.fromSeed( brightness: Brightness.dark, surface: isPureBlack ? Colors.black : null, seedColor: primaryColor, ).toM3EThemeData( base: AppTheme.dark( primary: primaryColor, isPureBlack: isPureBlack, ), ), ), ); }, ); } } Future initialiseHive() async { String? applicationDataDirectoryPath; if (Platform.isWindows || Platform.isLinux) { applicationDataDirectoryPath = "${(await getApplicationSupportDirectory()).path}/database"; } await Hive.initFlutter(applicationDataDirectoryPath); } Future? getYtConfig(SettingsManager settingsManager) async { final visitorId = settingsManager.visitorId; final apiKey = settingsManager.apiKey; final clientName = settingsManager.clientName; final clientVersion = settingsManager.clientVersion; if (visitorId == null || apiKey == null || clientName == null || clientVersion == null) { final config = await YTClient.getConfig(); settingsManager.visitorId = visitorId ?? config?.visitorData; settingsManager.apiKey = apiKey ?? config?.apiKey; settingsManager.clientName = clientName ?? config?.clientName; settingsManager.clientVersion = clientVersion ?? config?.clientVersion; return config; } else { return YTConfig( visitorData: visitorId, language: settingsManager.language['value']!, location: settingsManager.location['value']!, apiKey: apiKey, clientName: clientName, clientVersion: clientVersion, ); } } ================================================ FILE: lib/screens/browse/browse_page.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:expandable_text/expandable_text.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/core/widgets/internet_guard.dart'; import 'package:gyawun/core/utils/service_locator.dart'; import 'package:gyawun/screens/browse/cubit/browse_cubit.dart'; import 'package:gyawun/core/widgets/section_item.dart'; import 'package:loading_indicator_m3e/loading_indicator_m3e.dart'; import '../../generated/l10n.dart'; import '../../services/bottom_message.dart'; import '../../services/library.dart'; import '../../services/media_player.dart'; import '../../utils/bottom_modals.dart'; import '../../utils/enhanced_image.dart'; import '../../utils/extensions.dart'; class BrowsePage extends StatelessWidget { final Map endpoint; final bool isMore; const BrowsePage({super.key, required this.endpoint, this.isMore = false}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => BrowseCubit(sl(), endpoint: endpoint)..fetch(), child: _BrowsePage(endpoint: endpoint, isMore: isMore), ); } } class _BrowsePage extends StatefulWidget { const _BrowsePage({required this.endpoint, this.isMore = false}); final Map endpoint; final bool isMore; @override State<_BrowsePage> createState() => _BrowsePageState(); } class _BrowsePageState extends State<_BrowsePage> { late ScrollController _scrollController; String? continuation; @override void initState() { super.initState(); _scrollController = ScrollController(); _scrollController.addListener(_scrollListener); } // @override // void didUpdateWidget(covariant _BrowsePage oldWidget) { // super.didUpdateWidget(oldWidget); // if (oldWidget.endpoint['browseId'] != widget.endpoint['browseId']) { // fetchData(); // } // } Future _scrollListener() async { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { await context.read().fetchNext(); } } @override Widget build(BuildContext context) { return InternetGuard( onConnectivityRestored: context.read().fetch, child: BlocBuilder( builder: (context, state) { switch (state) { case BrowseLoading(): return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: const Center(child: LoadingIndicatorM3E()), ); case BrowseError(): return Center(child: Text(state.message ?? '')); case BrowseSuccess(): final isAddedToLibrary = (state.header.containsKey('playlistId') && context.watch().getPlaylist( state.header['playlistId'], ) != null); return Scaffold( appBar: AppBar( title: state.header['title'] != null ? Text(state.header['title']) : null, actionsPadding: .only(right: 8), actions: [ if (state.header['privacy'] != 'PRIVATE' && state.header.containsKey('playlistId') && state.header['playlistId'] != 'LM') IconButton( icon: Icon( isAddedToLibrary ? Icons.bookmark_added : Icons.bookmark_add_outlined, ), onPressed: () { context .read() .addToOrRemoveFromLibrary({ 'endpoint': widget.endpoint, ...state.header, }) .then((String message) { if (context.mounted) { BottomMessage.showText(context, message); } }); }, ), ], ), body: SingleChildScrollView( controller: _scrollController, child: Center( child: Container( padding: const EdgeInsets.only( left: 8, right: 8, bottom: 8, ), constraints: const BoxConstraints(maxWidth: 1000), child: Column( children: [ if (state.header['thumbnails'] != null) HeaderWidget( header: { 'endpoint': widget.endpoint, ...state.header, }, ), const SizedBox(height: 8), ...state.sections.indexed.map((sec) { return SectionItem( section: sec.$2, isMore: widget.isMore || state.sections.length == 1 || sec.$1 == 0, ); }), if (!state.loadingMore && state.continuation != null) const SizedBox(height: 64), if (state.loadingMore) const Center( child: Padding( padding: EdgeInsets.all(8.0), child: ExpressiveLoadingIndicator(), ), ), ], ), ), ), ), ); } }, ), ); } } class HeaderWidget extends StatefulWidget { const HeaderWidget({super.key, required this.header}); final Map header; @override State createState() => _HeaderWidgetState(); } class _HeaderWidgetState extends State { // late bool isAddedToLibrary; @override initState() { super.initState(); } Widget _buildImage( BuildContext context, List thumbnails, double maxWidth, { bool isRound = false, }) { return isRound ? CircleAvatar( backgroundImage: CachedNetworkImageProvider( getEnhancedImage( thumbnails.first['url'], dp: MediaQuery.of(context).devicePixelRatio, width: 250, ), ), radius: 125, backgroundColor: Theme.of(context).scaffoldBackgroundColor, ) : ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( imageUrl: getEnhancedImage( thumbnails.last['url'], dp: MediaQuery.of(context).devicePixelRatio, width: 250, ), filterQuality: FilterQuality.high, width: 250, height: 250, ), ); } Padding _buildContent( Map header, BuildContext context, { bool isRow = false, }) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column( crossAxisAlignment: isRow ? CrossAxisAlignment.start : CrossAxisAlignment.center, mainAxisAlignment: isRow ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ if (header['subtitle'] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text(header['subtitle'] ?? '', maxLines: 2), ), if (header['secondSubtitle'] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text(header['secondSubtitle']), ), if (header['description'] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: ExpandableText( header['description'].split('\n')[0], expandText: S.of(context).Show_More, collapseText: S.of(context).Show_Less, maxLines: isRow ? 3 : 2, style: TextStyle(color: context.subtitleColor), textAlign: TextAlign.center, ), ), if (header['playlistId'] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Wrap( spacing: 4, runSpacing: 8, alignment: WrapAlignment.center, runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (header['videoId'] != null || header['playlistId'] != null) FilledButton.icon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topLeft: .circular(24), bottomLeft: .circular(24), topRight: .circular(8), bottomRight: .circular(8), ), ), ), ), onPressed: () async { BottomMessage.showText( context, S.of(context).Songs_Will_Start_Playing_Soon, ); await GetIt.I().startPlaylistSongs( Map.from(header), ); }, label: const Text('Play All'), icon: const Icon(FluentIcons.play_24_filled), ), FilledButton( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 8, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topLeft: .circular(8), bottomLeft: .circular(8), topRight: .circular(24), bottomRight: .circular(24), ), ), ), ), child: const Icon(Icons.more_vert, size: 20), onPressed: () { Modals.showPlaylistBottomModal(context, header); }, ), ], ), ), ], ), ); } @override Widget build(BuildContext context) { return SizedBox( width: double.maxFinite, child: LayoutBuilder( builder: (context, constraints) { return constraints.maxWidth > 600 ? Row( children: [ if (widget.header['thumbnails'] != null) _buildImage( context, widget.header['thumbnails'], constraints.maxWidth, isRound: widget.header['type'] == 'ARTIST', ), const SizedBox(width: 4), Expanded( child: _buildContent(widget.header, context, isRow: true), ), ], ) : Column( children: [ if (widget.header['thumbnails'] != null) _buildImage( context, widget.header['thumbnails'], constraints.maxWidth, isRound: widget.header['type'] == 'ARTIST', ), SizedBox( height: widget.header['thumbnails'] != null ? 4 : 0, ), _buildContent(widget.header, context), ], ); }, ), ); } } ================================================ FILE: lib/screens/browse/cubit/browse_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:yt_music/ytmusic.dart'; part 'browse_state.dart'; class BrowseCubit extends Cubit { final YTMusic _ytMusic; final Map endpoint; BrowseCubit(this._ytMusic, {required this.endpoint}) : super(BrowseLoading()); Future fetch() async { emit(const BrowseLoading()); try { final feed = await _ytMusic.browse(body: endpoint, limit: 2); emit(BrowseSuccess( header: feed['header'] ?? {}, sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, )); } catch (e, st) { emit(BrowseError(e.toString(), st.toString())); } } Future fetchNext() async { final current = state; if (current is! BrowseSuccess) return; if (current.loadingMore || current.continuation == null) return; emit(current.copyWith(loadingMore: true)); try { final feed = await _ytMusic.browseContinuation( additionalParams: current.continuation!); emit( BrowseSuccess( header: current.header, sections: [...current.sections, ...feed['sections']], continuation: feed['continuation'], loadingMore: false, ), ); } catch (e, st) { emit(BrowseError(e.toString(), st.toString())); } } } ================================================ FILE: lib/screens/browse/cubit/browse_state.dart ================================================ part of 'browse_cubit.dart'; @immutable sealed class BrowseState { const BrowseState(); } final class BrowseLoading extends BrowseState { const BrowseLoading(); } final class BrowseError extends BrowseState { final String? message; final String? stackTrace; const BrowseError([this.message, this.stackTrace]); } final class BrowseSuccess extends BrowseState { final Map header; final List sections; final bool loadingMore; final String? continuation; const BrowseSuccess({ required this.header, required this.sections, required this.continuation, required this.loadingMore, }); BrowseSuccess copyWith({ Map? header, List? sections, String? continuation, bool? loadingMore, }) { return BrowseSuccess( header: header ?? this.header, sections: sections ?? this.sections, continuation: continuation ?? this.continuation, loadingMore: loadingMore ?? this.loadingMore, ); } } ================================================ FILE: lib/screens/chip/chip_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/internet_guard.dart'; import 'package:gyawun/core/utils/service_locator.dart'; import 'package:gyawun/screens/chip/cubit/chip_cubit.dart'; import 'package:gyawun/core/widgets/section_item.dart'; import 'package:loading_indicator_m3e/loading_indicator_m3e.dart'; class ChipPage extends StatelessWidget { const ChipPage({super.key, required this.title, required this.endpoint}); final String title; final Map endpoint; @override Widget build(BuildContext context) { return BlocProvider( create: (context) => ChipCubit(sl(), endpoint: endpoint)..fetch(), child: _ChipPage(title: title), ); } } class _ChipPage extends StatelessWidget { const _ChipPage({required this.title}); final String title; @override Widget build(BuildContext context) { return InternetGuard( onConnectivityRestored: context.read().fetch, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ExpressiveAppBar(title: title, hasLeading: true)]; }, body: BlocBuilder( builder: (context, state) { switch (state) { case ChipLoading(): return Center(child: LoadingIndicatorM3E()); case ChipError(): return Center(child: Text(state.message ?? '')); case ChipSuccess(): return NotificationListener( onNotification: (scrollInfo) { if (!state.loadingMore && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { context.read().fetchNext(); } return false; }, child: SafeArea( child: SingleChildScrollView( child: Column( children: [ ...state.sections.map((section) { return SectionItem(section: section); }), if (state.loadingMore) const Padding( padding: EdgeInsets.all(8.0), child: ExpressiveLoadingIndicator(), ), ], ), ), ), ); } }, ), ), ), ); } } ================================================ FILE: lib/screens/chip/cubit/chip_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:yt_music/ytmusic.dart'; part 'chip_state.dart'; class ChipCubit extends Cubit { final YTMusic _ytMusic; final Map endpoint; ChipCubit(this._ytMusic, {required this.endpoint}) : super(ChipLoading()); Future fetch() async { emit(const ChipLoading()); try { final feed = await _ytMusic.browse(body: endpoint); emit(ChipSuccess( sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, )); } catch (e, st) { emit(ChipError(e.toString(), st.toString())); } } Future refresh() async { try { final feed = await _ytMusic.browse(); emit(ChipSuccess( sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, )); } catch (e, st) { emit(ChipError(e.toString(), st.toString())); } } Future fetchNext() async { final current = state; if (current is! ChipSuccess) return; if (current.loadingMore || current.continuation == null) return; emit(current.copyWith(loadingMore: true)); try { final feed = await _ytMusic.browseContinuation( additionalParams: current.continuation!); emit( ChipSuccess( sections: [...current.sections, ...feed['sections'] ?? []], continuation: feed['continuation'], loadingMore: false, ), ); } catch (e, st) { emit(ChipError(e.toString(), st.toString())); } } } ================================================ FILE: lib/screens/chip/cubit/chip_state.dart ================================================ part of 'chip_cubit.dart'; @immutable sealed class ChipState { const ChipState(); } final class ChipLoading extends ChipState { const ChipLoading(); } final class ChipError extends ChipState { final String? message; final String? stackTrace; const ChipError([this.message, this.stackTrace]); } final class ChipSuccess extends ChipState { final List sections; final bool loadingMore; final String? continuation; const ChipSuccess({ required this.sections, required this.continuation, required this.loadingMore, }); ChipSuccess copyWith({ List? sections, String? continuation, bool? loadingMore, }) { return ChipSuccess( sections: sections ?? this.sections, continuation: continuation ?? this.continuation, loadingMore: loadingMore ?? this.loadingMore, ); } } ================================================ FILE: lib/screens/home/cubit/home_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:yt_music/ytmusic.dart'; part 'home_state.dart'; class HomeCubit extends Cubit { final YTMusic _ytMusic; HomeCubit(this._ytMusic) : super(HomeLoading()); Future fetch() async { emit(const HomeLoading()); try { final feed = await _ytMusic.browse(); emit(HomeSuccess( chips: feed['chips'] ?? [], sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, )); } catch (e, st) { emit(HomeError(e.toString(), st.toString())); } } Future refresh() async { try { final feed = await _ytMusic.browse(); emit(HomeSuccess( chips: feed['chips'] ?? [], sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, )); } catch (e, st) { emit(HomeError(e.toString(), st.toString())); } } Future fetchNext() async { final current = state; if (current is! HomeSuccess) return; if (current.loadingMore || current.continuation == null) return; emit(current.copyWith(loadingMore: true)); try { final feed = await _ytMusic.browseContinuation( additionalParams: current.continuation!); emit( HomeSuccess( chips: current.chips, sections: [...current.sections, ...feed['sections']], continuation: feed['continuation'], loadingMore: false, ), ); } catch (e, st) { emit(HomeError(e.toString(), st.toString())); } } } ================================================ FILE: lib/screens/home/cubit/home_state.dart ================================================ part of 'home_cubit.dart'; @immutable sealed class HomeState { const HomeState(); } final class HomeLoading extends HomeState { const HomeLoading(); } final class HomeError extends HomeState { final String? message; final String? stackTrace; const HomeError([this.message, this.stackTrace]); } final class HomeSuccess extends HomeState { final List chips; final List sections; final bool loadingMore; final String? continuation; const HomeSuccess({ required this.chips, required this.sections, required this.continuation, required this.loadingMore, }); HomeSuccess copyWith({ List? chips, List? sections, String? continuation, bool? loadingMore, }) { return HomeSuccess( chips: chips ?? this.chips, sections: sections ?? this.sections, continuation: continuation ?? this.continuation, loadingMore: loadingMore ?? this.loadingMore, ); } } ================================================ FILE: lib/screens/home/home_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/internet_guard.dart'; import 'package:gyawun/core/utils/service_locator.dart'; import 'package:gyawun/screens/home/cubit/home_cubit.dart'; import 'package:gyawun/core/widgets/section_item.dart'; import 'package:gyawun/screens/home/widgets/chips_row.dart'; import 'package:m3e_collection/m3e_collection.dart'; import '../../generated/l10n.dart'; import '../../utils/adaptive_widgets/adaptive_widgets.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => HomeCubit(sl())..fetch(), child: _HomePage(), ); } } class _HomePage extends StatefulWidget { const _HomePage(); @override State<_HomePage> createState() => _HomePageState(); } class _HomePageState extends State<_HomePage> { late ScrollController _scrollController; @override void initState() { super.initState(); _scrollController = ScrollController(); _scrollController.addListener(_scrollListener); } Future _scrollListener() async { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { await context.read().fetchNext(); } } @override Widget build(BuildContext context) { return InternetGuard( onConnectivityRestored: context.read().fetch, child: Scaffold( appBar: PreferredSize( preferredSize: AppBar().preferredSize, child: AppBar( automaticallyImplyLeading: false, title: Material( color: Colors.transparent, child: LayoutBuilder( builder: (context, constraints) { return Row( children: [ ConstrainedBox( constraints: BoxConstraints( maxWidth: constraints.maxWidth > 400 ? (400) : constraints.maxWidth, ), child: AdaptiveTextField( onTap: () => context.go('/search'), readOnly: true, keyboardType: TextInputType.text, maxLines: 1, autofocus: false, textInputAction: TextInputAction.search, fillColor: Theme.of(context).colorScheme.surfaceContainer, contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 8, ), borderRadius: BorderRadius.circular( Platform.isWindows ? 4.0 : 35, ), hintText: S.of(context).Search_Gyawun, prefix: Icon(AdaptiveIcons.search), ), ), ], ); }, ), ), centerTitle: false, ), ), body: ExpressiveRefreshIndicator( onRefresh: context.read().refresh, child: BlocBuilder( builder: (context, state) { switch (state) { case HomeLoading(): return Center(child: LoadingIndicatorM3E()); case HomeError(): return Center(child: Text(state.message ?? '')); case HomeSuccess(): return SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), controller: _scrollController, child: SafeArea( child: Column( children: [ ChipsRow(chips: state.chips), Column( children: [ ...state.sections.map((section) { return SectionItem(section: section); }), if (!state.loadingMore && state.continuation != null) const SizedBox(height: 50), if (state.loadingMore) const Padding( padding: EdgeInsets.all(8.0), child: ExpressiveLoadingIndicator(), ), ], ), ], ), ), ); } }, ), ), ), ); } } ================================================ FILE: lib/screens/home/widgets/chips_row.dart ================================================ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class ChipsRow extends StatelessWidget { const ChipsRow({super.key, required this.chips}); final List chips; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 12), scrollDirection: Axis.horizontal, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: chips.map((chip) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: FilledButton.tonal( onPressed: () => context.go('/chip', extra: chip), child: Text(chip['title']), ), ); }).toList(), ), ); } } ================================================ FILE: lib/screens/library/cubit/library_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; import 'package:gyawun/services/favourites_manager.dart'; import '../../../../services/library.dart'; import '../../../services/history_manager.dart'; part 'library_state.dart'; class LibraryCubit extends Cubit { final LibraryService libraryService; late final FavouritesManager _favouritesManager; late final DownloadManager _downloadsManager; late final SongHistory _songHistory; late final VoidCallback _listener; LibraryCubit(this.libraryService) : super(const LibraryLoading()) { _favouritesManager = GetIt.I(); _downloadsManager = GetIt.I(); _songHistory = GetIt.I().songs; _listener = _emitCurrentState; libraryService.addListener(_listener); _favouritesManager.listenable.addListener(_listener); _downloadsManager.downloadsNotifier.addListener(_listener); _songHistory.listenable.addListener(_listener); } void loadLibrary() { _emitCurrentState(); } void _emitCurrentState() { try { final downloadedCount = _downloadsManager.downloads.length; emit( LibraryLoaded( playlists: libraryService.playlists, favourites: _favouritesManager.playlist, downloadsCount: downloadedCount, historyCount: _songHistory.count, ), ); } catch (e) { emit(LibraryError(e.toString())); } } @override Future close() { libraryService.removeListener(_listener); _favouritesManager.listenable.removeListener(_listener); _downloadsManager.downloadsNotifier.removeListener(_listener); _songHistory.listenable.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/cubit/library_state.dart ================================================ part of 'library_cubit.dart'; @immutable sealed class LibraryState { const LibraryState(); } class LibraryLoading extends LibraryState { const LibraryLoading(); } class LibraryLoaded extends LibraryState { final Map playlists; final Map favourites; final int downloadsCount; final int historyCount; const LibraryLoaded({ required this.playlists, required this.favourites, required this.downloadsCount, required this.historyCount, }); } class LibraryError extends LibraryState { final String message; const LibraryError(this.message); } ================================================ FILE: lib/screens/library/downloads/cubit/downloads_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; part 'downloads_state.dart'; class DownloadsCubit extends Cubit { final DownloadManager _manager = GetIt.I(); late final VoidCallback _listener; DownloadsCubit() : super(const DownloadsLoading()) { _listener = () { if (!isClosed) { _emitState(); } }; _manager.playlistsNotifier.addListener(_listener); } void load() { _emitState(); } void _emitState() { if (isClosed) return; try { emit(DownloadsLoaded(_manager.playlistsNotifier.value)); } catch (e) { if (!isClosed) { emit(DownloadsError(e.toString())); } } } @override Future close() { _manager.playlistsNotifier.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/downloads/cubit/downloads_state.dart ================================================ part of 'downloads_cubit.dart'; @immutable sealed class DownloadsState { const DownloadsState(); } class DownloadsLoading extends DownloadsState { const DownloadsLoading(); } class DownloadsLoaded extends DownloadsState { final Map playlists; const DownloadsLoaded(this.playlists); } class DownloadsError extends DownloadsState { final String message; const DownloadsError(this.message); } ================================================ FILE: lib/screens/library/downloads/downloading/cubit/downloading_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; part 'downloading_state.dart'; class DownloadingCubit extends Cubit { final DownloadManager _manager = GetIt.I(); late final VoidCallback _listener; DownloadingCubit() : super(const DownloadingLoading()) { _listener = () { if (!isClosed) { _emitState(); } }; _manager.downloadsNotifier.addListener(_listener); } void load() { _emitState(); } void _emitState() { if (isClosed) return; try { final allSongs = _manager.downloadsNotifier.value; final downloading = allSongs .where((s) => s['status'] == 'DOWNLOADING') .toList(); final queued = _manager.getDownloadQueue(); emit(DownloadingLoaded(downloading: downloading, queued: queued)); } catch (e) { if (!isClosed) { emit(DownloadingError(e.toString())); } } } @override Future close() { _manager.downloadsNotifier.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/downloads/downloading/cubit/downloading_state.dart ================================================ part of 'downloading_cubit.dart'; @immutable sealed class DownloadingState { const DownloadingState(); } class DownloadingLoading extends DownloadingState { const DownloadingLoading(); } class DownloadingLoaded extends DownloadingState { final List downloading; final List queued; const DownloadingLoaded({ required this.downloading, required this.queued, }); } class DownloadingError extends DownloadingState { final String message; const DownloadingError(this.message); } ================================================ FILE: lib/screens/library/downloads/downloading/downloading_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gyawun/screens/library/downloads/downloading/widgets/downloading_section_tile.dart'; import '../../../../../generated/l10n.dart'; import 'cubit/downloading_cubit.dart'; import 'widgets/downloading_song_tile.dart'; class DownloadingPage extends StatelessWidget { const DownloadingPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => DownloadingCubit()..load(), child: Scaffold( appBar: AppBar( title: Text(S.of(context).Downloading), centerTitle: true, ), body: BlocBuilder( builder: (context, state) { return switch (state) { DownloadingLoading() => const Center( child: CircularProgressIndicator(), ), DownloadingError(:final message) => Center(child: Text(message)), DownloadingLoaded(:final downloading, :final queued) => CustomScrollView( slivers: [ if (downloading.isNotEmpty) ...[ SliverToBoxAdapter( child: DownloadingSectionTile( title: S.of(context).In_Progress, ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => DownloadingSongTile(song: downloading[index]), childCount: downloading.length, ), ), ], if (queued.isNotEmpty) ...[ SliverToBoxAdapter( child: DownloadingSectionTile( title: S.of(context).Queued_Count(queued.length), ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => DownloadingSongTile(song: queued[index]), childCount: queued.length, ), ), ], ], ), }; }, ), ), ); } } ================================================ FILE: lib/screens/library/downloads/downloading/widgets/downloading_section_tile.dart ================================================ import 'package:flutter/material.dart'; class DownloadingSectionTile extends StatelessWidget { const DownloadingSectionTile({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 8), child: Text( title, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), ); } } ================================================ FILE: lib/screens/library/downloads/downloading/widgets/downloading_song_tile.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:expandable_text/expandable_text.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/services/download_manager.dart'; import 'package:gyawun/utils/adaptive_widgets/listtile.dart'; import 'package:gyawun/utils/extensions.dart'; class DownloadingSongTile extends StatelessWidget { const DownloadingSongTile({required this.song, super.key}); final Map song; @override Widget build(BuildContext context) { List thumbnails = song['thumbnails']; double height = (song['aspectRatio'] != null ? 50 / song['aspectRatio'] : 50) .toDouble(); final notifier = GetIt.I().getProgressNotifier(song['videoId']); return AdaptiveListTile( title: Text(song['title'] ?? "", maxLines: 1), leading: ClipRRect( borderRadius: BorderRadius.circular(3), child: CachedNetworkImage( imageUrl: thumbnails.where((el) => el['width'] >= 50).toList().first['url'], height: height, width: 50, fit: BoxFit.cover, ), ), subtitle: (notifier != null) ? ValueListenableBuilder( valueListenable: notifier, builder: (context, progress, child) => LinearProgressIndicator( value: progress, color: Theme.of(context).primaryColor, backgroundColor: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.3), ), ) : LinearProgressIndicator( value: 0.0, color: Theme.of(context).primaryColor, backgroundColor: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.3), ), description: song['type'] == 'EPISODE' && song['description'] != null ? ExpandableText( song['description'].split('\n')?[0] ?? '', expandText: S.of(context).Show_More, collapseText: S.of(context).Show_Less, maxLines: 3, style: TextStyle(color: context.subtitleColor), ) : null, ); } } ================================================ FILE: lib/screens/library/downloads/downloads_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/services/download_manager.dart'; import 'package:gyawun/utils/adaptive_widgets/icons.dart'; import '../../../../generated/l10n.dart'; import '../../../../utils/bottom_modals.dart'; import '../../../../utils/playlist_thumbnail.dart'; import '../../../core/widgets/expressive_list_group.dart'; import '../../../services/favourites_manager.dart'; import 'cubit/downloads_cubit.dart'; class DownloadsPage extends StatelessWidget { const DownloadsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => DownloadsCubit()..load(), child: Scaffold( body: BlocBuilder( builder: (context, state) { return switch (state) { DownloadsLoading() => const Center( child: CircularProgressIndicator(), ), DownloadsError(:final message) => Center(child: Text(message)), DownloadsLoaded(:final playlists) => _DownloadsBody( playlists: playlists, ), }; }, ), ), ); } } class _DownloadsBody extends StatelessWidget { const _DownloadsBody({required this.playlists}); final Map playlists; @override Widget build(BuildContext context) { List sortedEntries = playlists.entries.toList(); sortedEntries.sort((a, b) { if (a.key == DownloadManager.songsPlaylistId) return -1; if (b.key == DownloadManager.songsPlaylistId) return 1; if (a.key == FavouritesManager.playlistId) return -1; if (b.key == FavouritesManager.playlistId) return 1; return a.value['title'].compareTo(b.value['title']); }); return NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar( title: S.of(context).Downloads, hasLeading: true, actions: [ IconButton( onPressed: () { Modals.showDownloadBottomModal(context); }, icon: const Icon(Icons.more_vert, size: 25), ), ], ), ]; }, body: Padding( padding: const .symmetric(vertical: 4, horizontal: 16), child: ExpressiveListGroup( children: [ ...sortedEntries.map((entry) { final playlist = entry.value; return ExpressiveListTile( title: playlist['id'] == DownloadManager.songsPlaylistId ? Text(S.of(context).Songs) : playlist['id'] == FavouritesManager.playlistId ? Text(S.of(context).Favourites) : Text(playlist['title']), leading: _leading(context, playlist), subtitle: Text(S.of(context).nSongs(playlist['songs'].length)), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () { context.push( '/library/downloads/download_playlist', extra: {'playlistId': playlist['id']}, ); }, onLongPress: () { Modals.showDownloadDetailsBottomModal(context, playlist); }, ); }), ], ), ), ); } Widget _leading(BuildContext context, Map playlist) { if (playlist['id'] == DownloadManager.songsPlaylistId || playlist['id'] == FavouritesManager.playlistId) { return Container( height: 40, width: 40, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Icon( playlist['id'] == DownloadManager.songsPlaylistId ? CupertinoIcons.music_note_list : AdaptiveIcons.heart_fill, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ); } if (playlist['type'] == 'ALBUM') { return PlaylistThumbnail(playlist: [playlist['songs'][0]], size: 40); } return PlaylistThumbnail(playlist: playlist['songs'], size: 40); } } ================================================ FILE: lib/screens/library/downloads/playlist/cubit/download_playlist_cubit.dart ================================================ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; part 'download_playlist_state.dart'; class DownloadPlaylistCubit extends Cubit { final String playlistId; final DownloadManager _manager = GetIt.I(); late final VoidCallback _listener; DownloadPlaylistCubit(this.playlistId) : super(const DownloadPlaylistLoading()) { _listener = () { if (!isClosed) { _emitState(); } }; _manager.playlistsNotifier.addListener(_listener); } void load() { _verifyPlaylistIntegrity(); _emitState(); } void _emitState() { if (isClosed) return; final allPlaylists = _manager.playlistsNotifier.value; final playlist = allPlaylists[playlistId]; if (playlist == null || playlist['songs'] == null) { emit(const DownloadPlaylistError('Playlist not available')); return; } emit( DownloadPlaylistLoaded( playlist: playlist, songs: List.from(playlist['songs']), ), ); } Future _verifyPlaylistIntegrity() async { final allPlaylists = _manager.playlistsNotifier.value; final playlist = allPlaylists[playlistId]; if (playlist == null) return; final List songs = playlist['songs'] ?? []; for (final song in songs) { final path = song['path']; if (path == null) continue; final exists = await File(path).exists(); final status = song['status']; if (!exists && status == 'DOWNLOADED') { await _manager.updateStatus(song['videoId'], 'DELETED'); } } } Future removeSong(Map song) async { await _manager.deleteSong(key: song['videoId'], playlistId: playlistId); } Map getCleanSong(Map song) { return _manager.getCleanSong(song); } List? getDownloadedSongs(String? playlistId) { return _manager.getDownloadedSongs(playlistId); } Future restoreDownloads(List songs) async { await _manager.restoreDownloads(songs: songs); } @override Future close() { _manager.playlistsNotifier.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/downloads/playlist/cubit/download_playlist_state.dart ================================================ part of 'download_playlist_cubit.dart'; @immutable sealed class DownloadPlaylistState { const DownloadPlaylistState(); } class DownloadPlaylistLoading extends DownloadPlaylistState { const DownloadPlaylistLoading(); } class DownloadPlaylistLoaded extends DownloadPlaylistState { final Map playlist; final List songs; const DownloadPlaylistLoaded({ required this.playlist, required this.songs, }); } class DownloadPlaylistError extends DownloadPlaylistState { final String message; const DownloadPlaylistError(this.message); } ================================================ FILE: lib/screens/library/downloads/playlist/download_playlist_page.dart ================================================ import 'dart:ui'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/core/widgets/song_tile.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/themes/text_styles.dart'; import '../../../../../generated/l10n.dart'; import '../../../../../utils/bottom_modals.dart'; import '../../../../services/bottom_message.dart'; import '../../../../services/download_manager.dart'; import '../../../../services/favourites_manager.dart'; import '../../../../utils/adaptive_widgets/appbar.dart'; import '../../../../utils/adaptive_widgets/scaffold.dart'; import 'cubit/download_playlist_cubit.dart'; class DownloadPlaylistPage extends StatelessWidget { const DownloadPlaylistPage({super.key, required this.playlistId}); final String playlistId; @override Widget build(BuildContext context) { return BlocProvider( create: (_) => DownloadPlaylistCubit(playlistId)..load(), child: BlocBuilder( builder: (context, state) { return switch (state) { DownloadPlaylistLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), DownloadPlaylistError() => AdaptiveScaffold( appBar: AdaptiveAppBar(), body: Center(child: Text(S.of(context).Playlist_Not_Available)), ), DownloadPlaylistLoaded(:final playlist, :final songs) => _PlaylistView( playlist: playlist, songs: songs, playlistId: playlistId, ), }; }, ), ); } } class _PlaylistView extends StatelessWidget { _PlaylistView({ required this.playlist, required this.songs, required this.playlistId, }); final Map playlist; final List songs; final String playlistId; final Map statusMap = { "DELETED": _SongStatusConfig( onTap: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).File_Not_Found); }, onLongPress: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).File_Not_Found); }, icon: FluentIcons.arrow_circle_down_24_regular, onIconPress: (ctx, song) { ctx.read().restoreDownloads([song]); }, ), "QUEUED": _SongStatusConfig( onTap: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Queued); }, onLongPress: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Queued); }, icon: FluentIcons.clock_24_regular, onIconPress: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Queued); }, ), "DOWNLOADING": _SongStatusConfig( onTap: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Downloading); }, onLongPress: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Downloading); }, icon: FluentIcons.arrow_sync_circle_24_regular, onIconPress: (ctx, _) { BottomMessage.showText(ctx, S.of(ctx).Downloading); }, ), }; @override Widget build(BuildContext context) { final downloadedSongs = context .read() .getDownloadedSongs(playlistId); return Scaffold( body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( pinned: true, expandedHeight: 120, flexibleSpace: LayoutBuilder( builder: (context, constraints) { final maxHeight = 120.0; final t = (constraints.maxHeight / (maxHeight + 30)) .clamp(0.0, 1.0); final paddingLeft = lerpDouble(100, 16, t)!; return FlexibleSpaceBar( titlePadding: EdgeInsets.only( left: paddingLeft, bottom: 8, ), title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( playlist.isNotEmpty && playlist['id'] == DownloadManager.songsPlaylistId ? S.of(context).Songs : playlist.isNotEmpty && playlist['id'] == FavouritesManager.playlistId ? S.of(context).Favourites : playlist.isNotEmpty ? playlist['title'] : null, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith(fontSize: 16), ), SizedBox(height: 2), Text( S.of(context).nSongs(songs.length), maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith( fontSize: 11, fontWeight: FontWeight.w600, ), ), ], ), ); }, ), ), ]; }, body: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ FilledButton.icon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topRight: .circular(8), bottomRight: .circular(8), topLeft: .circular(24), bottomLeft: .circular(24), ), ), ), ), onPressed: () { if (downloadedSongs == null || downloadedSongs.isEmpty) { BottomMessage.showText( context, S.of(context).No_Offline_Songs, ); } else { GetIt.I().playAll(downloadedSongs); } }, icon: const Icon(FluentIcons.play_24_filled), label: Text(S.of(context).Play_All), ), SizedBox(width: 4), FilledButton.tonalIcon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topLeft: .circular(8), bottomLeft: .circular(8), topRight: .circular(24), bottomRight: .circular(24), ), ), ), ), onPressed: () { if (downloadedSongs == null || downloadedSongs.isEmpty) { BottomMessage.showText( context, S.of(context).No_Offline_Songs, ); } else { final shuffled = List.from(downloadedSongs); shuffled.shuffle(); GetIt.I().playAll(shuffled); } }, icon: const Icon(FluentIcons.arrow_shuffle_24_filled), label: Text(S.of(context).Shuffle), ), SizedBox(width: 8), IconButton.filled( enableFeedback: true, onPressed: () { Modals.showDownloadDetailsBottomModal( context, playlist, ); }, icon: Icon(Icons.more_vert), ), ], ), ), ), SliverList( delegate: SliverChildBuilderDelegate((context, index) { final song = songs[index]; final config = statusMap[song['status']]; return Padding( padding: const .symmetric(horizontal: 8, vertical: 4), child: SwipeActionCell( key: ObjectKey(song['videoId']), backgroundColor: Colors.transparent, trailingActions: [ SwipeAction( title: S.of(context).Remove, color: Colors.red, onTap: (handler) async { final confirm = await Modals.showConfirmBottomModal( context, message: S.of(context).Remove_Message, isDanger: true, ); if (confirm && context.mounted) { await context .read() .removeSong(song); } else { handler(false); } }, ), ], child: SongTile( song: context .read() .getCleanSong(song), onTap: config?.onTap, onLongPress: config?.onLongPress, icon: config?.icon, onIconPress: config?.onIconPress, isFirst: index == 0, isLast: index == songs.length - 1, ), ), ); }, childCount: songs.length), ), const SliverToBoxAdapter(child: SizedBox(height: 20)), ], ), ), ), ), ); } } class _SongStatusConfig { final void Function(BuildContext, Map)? onTap; final void Function(BuildContext, Map)? onLongPress; final IconData icon; final void Function(BuildContext, Map)? onIconPress; const _SongStatusConfig({ required this.onTap, required this.onLongPress, required this.icon, this.onIconPress, }); } ================================================ FILE: lib/screens/library/downloads/playlist/widgets/download_playlist_header.dart ================================================ // import 'dart:io'; // import 'package:flutter/cupertino.dart'; // import 'package:flutter/material.dart'; // import 'package:get_it/get_it.dart'; // import 'package:gyawun/generated/l10n.dart'; // import 'package:gyawun/services/media_player.dart'; // import 'package:gyawun/themes/colors.dart'; // import 'package:gyawun/utils/adaptive_widgets/buttons.dart'; // import 'package:gyawun/utils/bottom_modals.dart'; // import 'package:gyawun/utils/extensions.dart'; // import 'package:gyawun/utils/playlist_thumbnail.dart'; // class DownloadPlaylistHeader extends StatelessWidget { // const DownloadPlaylistHeader({ // super.key, // required this.playlist, // required this.imageType, // }); // final Map playlist; // final String imageType; // Widget _buildImage(List songs, double maxWidth, // {bool isRound = false, bool isDark = false}) { // return (songs.isNotEmpty && imageType == "SONGS") // ? Container( // height: 200, // width: 200, // decoration: BoxDecoration( // color: greyColor, // borderRadius: BorderRadius.circular(3), // ), // child: Icon( // CupertinoIcons.music_note_list, // color: isDark ? Colors.white : Colors.black, // ), // ) // : (songs.isNotEmpty && imageType == "ALBUM") // ? PlaylistThumbnail(playslist: [songs[0]], size: 225, radius: 8) // : PlaylistThumbnail(playslist: songs, size: 225, radius: 8); // } // Padding _buildContent(Map playlist, BuildContext context, // {bool isRow = false}) { // return Padding( // padding: const EdgeInsets.only(left: 8, top: 4), // child: Column( // crossAxisAlignment: // isRow ? CrossAxisAlignment.start : CrossAxisAlignment.center, // mainAxisAlignment: // isRow ? MainAxisAlignment.start : MainAxisAlignment.center, // children: [ // if (playlist['songs'] != null) // Padding( // padding: const EdgeInsets.symmetric(vertical: 4), // child: Text(S.of(context).nSongs(playlist['songs'].length), // maxLines: 2), // ), // Wrap( // spacing: 8, // runSpacing: 8, // alignment: WrapAlignment.center, // runAlignment: WrapAlignment.center, // crossAxisAlignment: WrapCrossAlignment.center, // children: [ // if (playlist['songs'].isNotEmpty) // AdaptiveFilledButton( // onPressed: () { // GetIt.I().playAll(playlist['songs']); // }, // padding: // const EdgeInsets.symmetric(horizontal: 16, vertical: 10), // shape: RoundedRectangleBorder( // borderRadius: // BorderRadius.circular(Platform.isWindows ? 8 : 35), // ), // color: context.isDarkMode ? Colors.white : Colors.black, // child: Row( // mainAxisSize: MainAxisSize.min, // crossAxisAlignment: CrossAxisAlignment.center, // children: [ // Icon( // Icons.play_arrow, // color: context.isDarkMode ? Colors.black : Colors.white, // size: 24, // ), // const SizedBox(width: 8), // const Text("Play All", style: TextStyle(fontSize: 18)) // ], // ), // ), // AdaptiveFilledButton( // shape: const CircleBorder(), // color: greyColor, // padding: const EdgeInsets.all(14), // onPressed: () { // Modals.showDownloadDetailsBottomModal(context, playlist); // }, // child: Icon( // Icons.more_vert, // size: 20, // color: context.isDarkMode ? Colors.white : Colors.black, // ), // ) // ], // ) // ], // ), // ); // } // @override // Widget build(BuildContext context) { // return SizedBox( // width: double.maxFinite, // child: Card( // child: LayoutBuilder(builder: (context, constraints) { // return constraints.maxWidth > 600 // ? Row( // children: [ // if (playlist['songs'] != null) // _buildImage(playlist['songs'], constraints.maxWidth, // isRound: playlist['type'] == 'ARTIST', // isDark: context.isDarkMode), // const SizedBox(width: 4), // Expanded( // child: _buildContent(playlist, context, isRow: true)), // ], // ) // : Column( // children: [ // if (playlist['songs'] != null) // _buildImage(playlist['songs'], constraints.maxWidth, // isRound: playlist['type'] == 'ARTIST', // isDark: context.isDarkMode), // SizedBox(height: playlist['thumbnails'] != null ? 4 : 0), // _buildContent(playlist, context), // ], // ); // }), // ), // ); // } // } ================================================ FILE: lib/screens/library/downloads/playlist/widgets/download_song_tile.dart ================================================ // import 'package:expandable_text/expandable_text.dart'; // import 'package:flutter/material.dart'; // import 'package:get_it/get_it.dart'; // import 'package:gyawun/generated/l10n.dart'; // import 'package:gyawun/services/download_manager.dart'; // import 'package:gyawun/services/media_player.dart'; // import 'package:gyawun/utils/adaptive_widgets/listtile.dart'; // import 'package:gyawun/utils/bottom_modals.dart'; // import 'package:gyawun/utils/extensions.dart'; // import 'package:gyawun/utils/song_thumbnail.dart'; // class DownloadedSongTile extends StatelessWidget { // const DownloadedSongTile({required this.song, super.key}); // final Map song; // @override // Widget build(BuildContext context) { // double height = // (song['aspectRatio'] != null ? 50 / song['aspectRatio'] : 50) // .toDouble(); // return AdaptiveListTile( // onTap: () async { // if (song['videoId'] != null && song['status'] == 'DOWNLOADED') { // await GetIt.I().playSong(Map.from(song)); // } // }, // onSecondaryTap: () { // if (song['videoId'] != null && song['status'] == 'DOWNLOADED') { // Modals.showSongBottomModal(context, song); // } // }, // onLongPress: () { // if (song['videoId'] != null && song['status'] == 'DOWNLOADED') { // Modals.showSongBottomModal(context, song); // } // }, // title: Text(song['title'] ?? "", maxLines: 1), // leading: ClipRRect( // borderRadius: BorderRadius.circular(3), // child: SongThumbnail( // song: song, // height: height, // width: 50, // fit: BoxFit.cover, // ), // ), // subtitle: Text( // song['status'] == 'DELETED' // ? S.of(context).FileNotFound // : song['status'] == 'DOWNLOADING' // ? S.of(context).Downloading // : song['status'] == 'QUEUED' // ? S.of(context).Queued // : _buildSubtitle(song), // maxLines: 1, // style: TextStyle( // color: song['status'] == 'DELETED' // ? Colors.red // : song['status'] == 'DOWNLOADING' // ? Theme.of(context).colorScheme.primary // : song['status'] == 'QUEUED' // ? Theme.of(context) // .colorScheme // .primary // .withValues(alpha: 0.5) // : Colors.grey.withAlpha(250), // ), // overflow: TextOverflow.ellipsis, // ), // trailing: song['status'] == 'DELETED' // ? IconButton( // onPressed: () { // GetIt.I().downloadSong(song); // }, // icon: const Icon(Icons.refresh)) // : null, // description: song['type'] == 'EPISODE' && song['description'] != null // ? ExpandableText( // song['description'].split('\n')?[0] ?? '', // expandText: S.of(context).Show_More, // collapseText: S.of(context).Show_Less, // maxLines: 3, // style: TextStyle(color: context.subtitleColor), // ) // : null, // ); // } // String _buildSubtitle(Map item) { // List sub = []; // if (sub.isEmpty && item['artists'] != null) { // for (Map artist in item['artists']) { // sub.add(artist['name']); // } // } // if (sub.isEmpty && item['album'] != null) { // sub.add(item['album']['name']); // } // String s = sub.join(' · '); // return item['subtitle'] ?? s; // } // } ================================================ FILE: lib/screens/library/favourites/cubit/favourites_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/favourites_manager.dart'; part 'favourites_state.dart'; class FavouritesCubit extends Cubit { late final FavouritesManager _manager; late final VoidCallback _listener; FavouritesCubit() : super(const FavouritesLoading()) { _manager = GetIt.I(); _listener = () { if (!isClosed) { _emitState(); } }; _manager.listenable.addListener(_listener); } void load() { _emitState(); } void _emitState() { if (isClosed) return; try { emit(FavouritesLoaded(_manager.playlist)); } catch (e) { if (!isClosed) { emit(FavouritesError(e.toString())); } } } Future remove(dynamic key) async { await _manager.remove(key); } @override Future close() { _manager.listenable.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/favourites/cubit/favourites_state.dart ================================================ part of 'favourites_cubit.dart'; @immutable sealed class FavouritesState { const FavouritesState(); } class FavouritesLoading extends FavouritesState { const FavouritesLoading(); } class FavouritesLoaded extends FavouritesState { final Map favourites; const FavouritesLoaded(this.favourites); } class FavouritesError extends FavouritesState { final String message; const FavouritesError(this.message); } ================================================ FILE: lib/screens/library/favourites/favourites_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/song_tile.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/themes/text_styles.dart'; import '../../../../generated/l10n.dart'; import '../../../../utils/bottom_modals.dart'; import '../../../../utils/adaptive_widgets/adaptive_widgets.dart'; import 'cubit/favourites_cubit.dart'; class FavouritesPage extends StatelessWidget { const FavouritesPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => FavouritesCubit()..load(), child: Scaffold( body: BlocBuilder( builder: (context, state) { return switch (state) { FavouritesLoading() => const Center( child: AdaptiveProgressRing(), ), FavouritesError(:final message) => Center(child: Text(message)), FavouritesLoaded(favourites: final playlist) => _FavouritesBody( playlist: playlist, ), }; }, ), ), ); } } class _FavouritesBody extends StatelessWidget { const _FavouritesBody({required this.playlist}); final Map playlist; @override Widget build(BuildContext context) { final songs = playlist['songs']; return NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar( hasLeading: true, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Favourites', maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith(fontSize: 16), ), SizedBox(height: 2), Text( S.of(context).nSongs(songs.length), maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle( context, ).copyWith(fontSize: 11, fontWeight: FontWeight.w600), ), ], ), ), ]; }, body: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ FilledButton.icon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topRight: .circular(8), bottomRight: .circular(8), topLeft: .circular(24), bottomLeft: .circular(24), ), ), ), ), onPressed: () { if (songs.isEmpty) return; GetIt.I().playAll(songs); }, icon: const Icon(FluentIcons.play_24_filled), label: const Text('Play it'), ), SizedBox(width: 4), FilledButton.tonalIcon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topLeft: .circular(8), bottomLeft: .circular(8), topRight: .circular(24), bottomRight: .circular(24), ), ), ), ), onPressed: () { if (songs.isEmpty) return; final shuffled = List.from(songs); shuffled.shuffle(); GetIt.I().playAll(shuffled); }, icon: const Icon(FluentIcons.arrow_shuffle_24_filled), label: const Text('Shuffle'), ), SizedBox(width: 8), IconButton.filled( enableFeedback: true, onPressed: () { Modals.showFavouritesBottomModal(context, playlist); }, icon: Icon(Icons.more_vert), ), ], ), ), ), if (songs.isNotEmpty) SliverList( delegate: SliverChildBuilderDelegate((context, index) { final song = songs[index]; return Padding( padding: const .symmetric(horizontal: 8, vertical: 4), child: SwipeActionCell( backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerLowest, key: ObjectKey(song['videoId']), trailingActions: [ SwipeAction( title: S.of(context).Remove, color: Colors.red, onTap: (handler) async { final confirm = await Modals.showConfirmBottomModal( context, message: S.of(context).Remove_Message, isDanger: true, ); if (confirm && context.mounted) { await context.read().remove(song); } else { handler(false); } }, ), ], child: SongTile( song: song, isFirst: index == 0, isLast: index == songs.length - 1, ), ), ); }, childCount: songs.length), ), const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ), ); } } ================================================ FILE: lib/screens/library/history/cubit/history_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import '../../../../services/history_manager.dart'; part 'history_state.dart'; class HistoryCubit extends Cubit { late final SongHistory _songHistory; late final VoidCallback _listener; HistoryCubit() : super(const HistoryLoading()) { _songHistory = GetIt.I().songs; _listener = () { if (!isClosed) { _emitState(); } }; _songHistory.listenable.addListener(_listener); } void load() { _emitState(); } void _emitState() { if (isClosed) return; try { final songs = _songHistory.getList(); emit(HistoryLoaded(songs)); } catch (e) { if (!isClosed) { emit(HistoryError(e.toString())); } } } Future remove(Map song) async { await _songHistory.remove(song); } @override Future close() { _songHistory.listenable.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/history/cubit/history_state.dart ================================================ part of 'history_cubit.dart'; @immutable sealed class HistoryState { const HistoryState(); } class HistoryLoading extends HistoryState { const HistoryLoading(); } class HistoryLoaded extends HistoryState { final List songs; const HistoryLoaded(this.songs); } class HistoryError extends HistoryState { final String message; const HistoryError(this.message); } ================================================ FILE: lib/screens/library/history/history_page.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; import 'package:gyawun/core/widgets/song_tile.dart'; import 'package:gyawun/themes/text_styles.dart'; import '../../../../generated/l10n.dart'; import '../../../../utils/bottom_modals.dart'; import '../../../../utils/adaptive_widgets/adaptive_widgets.dart'; import 'cubit/history_cubit.dart'; class HistoryPage extends StatelessWidget { const HistoryPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => HistoryCubit()..load(), child: Scaffold( body: BlocBuilder( builder: (context, state) { return switch (state) { HistoryLoading() => const Center(child: AdaptiveProgressRing()), HistoryError(:final message) => Center(child: Text(message)), HistoryLoaded(:final songs) => _HistoryBody(songs: songs), }; }, ), ), ); } } class _HistoryBody extends StatelessWidget { const _HistoryBody({required this.songs}); final List songs; @override Widget build(BuildContext context) { if (songs.isEmpty) { return Center(child: Text("No History Found")); } return Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), padding: const EdgeInsets.symmetric(horizontal: 8), child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( pinned: true, expandedHeight: 120, flexibleSpace: LayoutBuilder( builder: (context, constraints) { final maxHeight = 120.0; final t = (constraints.maxHeight / (maxHeight + 30)).clamp( 0.0, 1.0, ); final paddingLeft = lerpDouble(100, 16, t)!; return FlexibleSpaceBar( titlePadding: EdgeInsets.only( left: paddingLeft, bottom: 8, ), title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( S.of(context).History, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith(fontSize: 16), ), SizedBox(height: 2), Text( S.of(context).nSongs(songs.length), maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith( fontSize: 11, fontWeight: FontWeight.w600, ), ), ], ), ); }, ), ), ]; }, body: CustomScrollView( slivers: [ SliverList( delegate: SliverChildBuilderDelegate((context, index) { final song = songs[index]; return Padding( padding: const .symmetric(horizontal: 8, vertical: 4), child: SwipeActionCell( backgroundColor: Colors.transparent, key: ObjectKey(song['videoId']), trailingActions: [ SwipeAction( title: S.of(context).Remove, color: Colors.red, onTap: (handler) async { final confirm = await Modals.showConfirmBottomModal( context, message: S.of(context).Remove_Message, isDanger: true, ); if (confirm && context.mounted) { context.read().remove(song); } else { handler(false); } }, ), ], child: SongTile( song: song, isFirst: index == 0, isLast: index == songs.length - 1, ), ), ); }, childCount: songs.length), ), ], ), ), ), ); } } ================================================ FILE: lib/screens/library/library_page.dart ================================================ import 'dart:collection'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/core/widgets/internet_guard.dart'; import 'package:gyawun/core/utils/service_locator.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import '../../../../generated/l10n.dart'; import '../../../../services/library.dart'; import '../../../../utils/adaptive_widgets/adaptive_widgets.dart'; import '../../../../utils/bottom_modals.dart'; import '../../utils/playlist_icon_widget.dart'; import '../../utils/playlist_icons.dart'; import 'cubit/library_cubit.dart'; class LibraryPage extends StatelessWidget { const LibraryPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => LibraryCubit(sl())..loadLibrary(), child: BlocBuilder( builder: (context, state) { return InternetGuard( child: Scaffold( floatingActionButton: Column( mainAxisSize: .min, children: [ FloatingActionButton.small( heroTag: 'import_playlist', onPressed: () { Modals.showImportplaylistModal(context); }, child: const Icon(Icons.import_export_rounded), ), SizedBox(height: 8), FloatingActionButton( heroTag: 'create_playlist', onPressed: () { Modals.showCreateplaylistModal(context); }, child: const Icon(FluentIcons.add_24_filled), ), ], ), body: switch (state) { LibraryLoading() => const Center(child: AdaptiveProgressRing()), LibraryError(:final message) => Center(child: Text(message)), LibraryLoaded( :final playlists, favourites: final favourites, :final downloadsCount, :final historyCount, ) => _LibraryBody( playlists: playlists, favourites: favourites, downloadsCount: downloadsCount, historyCount: historyCount, ), }, ), ); }, ), ); } } class _LibraryBody extends StatelessWidget { const _LibraryBody({ required this.playlists, required this.favourites, this.downloadsCount = 0, this.historyCount = 0, }); final Map playlists; final Map favourites; final int downloadsCount; final int historyCount; @override Widget build(BuildContext context) { final favSongs = favourites['songs']; return NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ExpressiveAppBar(title: "Library")]; }, body: CustomScrollView( slivers: [ SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8.0), sliver: SliverToBoxAdapter( child: Padding( padding: const .symmetric(vertical: 4, horizontal: 8), child: Column( children: [ ExpressiveListGroup( title: "Default", children: [ ExpressiveListTile( title: Text(S.of(context).Favourites), leading: ColorIcon( icon: FluentIcons.heart_24_filled, boxColor: Theme.of( context, ).colorScheme.primaryContainer, iconColor: Theme.of( context, ).colorScheme.onPrimaryContainer, size: 30, ), subtitle: Text(S.of(context).nSongs(favSongs.length)), trailing: Icon(FluentIcons.chevron_right_24_filled), onTap: () => context.push('/library/favourites'), onLongPress: () { Modals.showFavouritesBottomModal( context, favourites, ); }, ), ExpressiveListTile( title: Text(S.of(context).Downloads), leading: ColorIcon( icon: FluentIcons.cloud_arrow_down_24_filled, boxColor: Theme.of( context, ).colorScheme.primaryContainer, iconColor: Theme.of( context, ).colorScheme.onPrimaryContainer, size: 30, ), subtitle: Text(S.of(context).nSongs(downloadsCount)), trailing: Icon(FluentIcons.chevron_right_24_filled), onTap: () => context.push('/library/downloads'), onLongPress: () { Modals.showDownloadBottomModal(context); }, ), ExpressiveListTile( title: Text(S.of(context).History), leading: ColorIcon( icon: FluentIcons.history_24_filled, boxColor: Theme.of( context, ).colorScheme.primaryContainer, iconColor: Theme.of( context, ).colorScheme.onPrimaryContainer, size: 30, ), subtitle: Text(S.of(context).nSongs(historyCount)), trailing: Icon(FluentIcons.chevron_right_24_filled), onTap: () => context.push('/library/history'), ), ], ), SizedBox(height: 17), if (playlists.isNotEmpty) ExpressiveListGroup( title: "Custom", children: SplayTreeMap.from(playlists) .map((key, item) { if (item == null) { return MapEntry(key, const SizedBox()); } return MapEntry( key, Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), child: ExpressiveListTile( title: Text(item['title']), leading: _playlistLeading( context, key, item, ), subtitle: (item['songs'] != null || item['isPredefined']) ? Text( item['isPredefined'] == true ? item['subtitle'] : S .of(context) .nSongs( item['songs'].length, ), ) : null, trailing: Icon( FluentIcons.chevron_right_24_filled, ), onTap: () { if (item['isPredefined'] == true) { context.push( '/browse', extra: { 'endpoint': item['endpoint'] .cast(), }, ); } else { context.push( '/library/playlist_details', extra: {'playlistkey': key}, ); } }, onLongPress: () => _showPlaylistMenu(context, key, item), ), ), ); }) .values .toList(), ), ], ), ), ), ), ], ), ); } Widget _playlistLeading(BuildContext context, String key, Map item) { if (item['isPredefined'] == true) { return ClipRRect( borderRadius: BorderRadius.circular(item['type'] == 'ARTIST' ? 30 : 8), child: CachedNetworkImage( imageUrl: item['thumbnails'].first['url'].replaceAll( 'w540-h225', 'w60-h60', ), height: 40, width: 40, ), ); } return Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: PlaylistIconWidget( data: PlaylistIcons.byId(item['iconId']), size: 30, ), ); } void _showPlaylistMenu(BuildContext context, String key, Map item) { if (item['videoId'] == null && item['playlistId'] != null) { Modals.showPlaylistBottomModal(context, item); } else if (item['isPredefined'] == false) { Modals.showPlaylistBottomModal(context, {...item, 'playlistId': key}); } } } ================================================ FILE: lib/screens/library/playlist/cubit/playlist_details_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/library.dart'; part 'playlist_details_state.dart'; class PlaylistDetailsCubit extends Cubit { final String playlistKey; final LibraryService _library = GetIt.I(); late final VoidCallback _listener; PlaylistDetailsCubit(this.playlistKey) : super(const PlaylistDetailsLoading()) { _listener = () { if (!isClosed) { _emitState(); } }; _library.addListener(_listener); } void load() { _emitState(); } void _emitState() { if (isClosed) return; try { final playlist = _library.getPlaylist(playlistKey); if (playlist == null) { emit(const PlaylistDetailsError('Playlist not available')); return; } emit(PlaylistDetailsLoaded(playlist)); } catch (e) { if (!isClosed) { emit(PlaylistDetailsError(e.toString())); } } } Future removeSong(Map song) { return _library.removeFromPlaylist( item: song, playlistId: playlistKey, ); } @override Future close() { _library.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/library/playlist/cubit/playlist_details_state.dart ================================================ part of 'playlist_details_cubit.dart'; @immutable sealed class PlaylistDetailsState { const PlaylistDetailsState(); } class PlaylistDetailsLoading extends PlaylistDetailsState { const PlaylistDetailsLoading(); } class PlaylistDetailsLoaded extends PlaylistDetailsState { final Map playlist; const PlaylistDetailsLoaded(this.playlist); } class PlaylistDetailsError extends PlaylistDetailsState { final String message; const PlaylistDetailsError(this.message); } ================================================ FILE: lib/screens/library/playlist/playlist_details_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/song_tile.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/themes/text_styles.dart'; import '../../../../../generated/l10n.dart'; import '../../../../../utils/bottom_modals.dart'; import '../../../utils/adaptive_widgets/appbar.dart'; import '../../../utils/adaptive_widgets/scaffold.dart'; import 'cubit/playlist_details_cubit.dart'; class PlaylistDetailsPage extends StatelessWidget { const PlaylistDetailsPage({super.key, required this.playlistkey}); final String playlistkey; @override Widget build(BuildContext context) { return BlocProvider( create: (_) => PlaylistDetailsCubit(playlistkey)..load(), child: BlocBuilder( builder: (context, state) { return switch (state) { PlaylistDetailsLoading() => const Scaffold( body: Center(child: CircularProgressIndicator()), ), PlaylistDetailsError() => AdaptiveScaffold( appBar: AdaptiveAppBar(), body: Center(child: Text(S.of(context).Playlist_Not_Available)), ), PlaylistDetailsLoaded(:final playlist) => _PlaylistView( playlist: playlist, playlistKey: playlistkey, ), }; }, ), ); } } class _PlaylistView extends StatelessWidget { const _PlaylistView({required this.playlist, required this.playlistKey}); final Map playlist; final String playlistKey; @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar( hasLeading: true, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( playlist['title'], maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle(context).copyWith(fontSize: 16), ), SizedBox(height: 2), Text( S.of(context).nSongs(playlist['songs'].length), maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle( context, ).copyWith(fontSize: 11, fontWeight: FontWeight.w600), ), ], ), ), ]; }, body: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ FilledButton.icon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topRight: .circular(8), bottomRight: .circular(8), topLeft: .circular(24), bottomLeft: .circular(24), ), ), ), ), onPressed: () { GetIt.I().playAll(playlist['songs']); }, icon: const Icon(FluentIcons.play_24_filled), label: const Text('Play it'), ), SizedBox(width: 4), FilledButton.tonalIcon( style: const ButtonStyle( padding: WidgetStatePropertyAll( .symmetric(horizontal: 24, vertical: 16), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: .only( topLeft: .circular(8), bottomLeft: .circular(8), topRight: .circular(24), bottomRight: .circular(24), ), ), ), ), onPressed: () { final shuffled = List.from(playlist['songs']); shuffled.shuffle(); GetIt.I().playAll(shuffled); }, icon: const Icon(FluentIcons.arrow_shuffle_24_filled), label: const Text('Shuffle'), ), SizedBox(width: 8), IconButton.filled( enableFeedback: true, onPressed: () { Modals.showPlaylistBottomModal(context, { ...playlist, 'playlistId': playlistKey, }); }, icon: Icon(Icons.more_vert), ), ], ), ), ), if (playlist['songs'].isNotEmpty) SliverList( delegate: SliverChildBuilderDelegate((context, index) { final song = playlist['songs'][index]; return Padding( padding: const .symmetric(horizontal: 8, vertical: 4), child: SwipeActionCell( key: ObjectKey(song['videoId']), backgroundColor: Colors.transparent, trailingActions: [ SwipeAction( title: S.of(context).Remove, color: Colors.red, onTap: (handler) async { final confirm = await Modals.showConfirmBottomModal( context, message: S.of(context).Remove_Message, isDanger: true, ); if (confirm && context.mounted) { await context .read() .removeSong(song); } else { handler(false); } }, ), ], child: SongTile( song: song, isFirst: index == 0, isLast: index == playlist['songs'].length - 1, ), ), ); }, childCount: playlist['songs'].length), ), const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ), ), ); } } ================================================ FILE: lib/screens/library/widgets/my_playlist_header.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/themes/colors.dart'; import 'package:gyawun/utils/adaptive_widgets/buttons.dart'; import 'package:gyawun/utils/extensions.dart'; class MyPlayistHeader extends StatelessWidget { const MyPlayistHeader({super.key, required this.playlist}); final Map playlist; Widget _buildImage( List songs, double maxWidth, { bool isRound = false, bool isDark = false, }) { return (songs.isNotEmpty) ? ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( height: 225, width: 225, child: StaggeredGrid.count( crossAxisCount: songs.length > 1 ? 2 : 1, axisDirection: AxisDirection.down, children: songs.sublist(0, min(songs.length, 4)).indexed.map(( ind, ) { int index = ind.$1; Map song = ind.$2; return CachedNetworkImage( imageUrl: song['thumbnails'].first['url'] .replaceAll('w540-h225', 'w225-h225') .replaceAll('w60-h60', 'w225-h225'), height: (songs.length <= 2 || (songs.length == 3 && index == 0)) ? 225 : 225 / 2, width: 255 / 2, fit: BoxFit.cover, ); }).toList(), ), ), ) : Container( height: 200, width: 200, decoration: BoxDecoration( color: greyColor, borderRadius: BorderRadius.circular(3), ), child: Icon( CupertinoIcons.music_note_list, color: isDark ? Colors.white : Colors.black, ), ); } Padding _buildContent( Map playlist, BuildContext context, { bool isRow = false, }) { return Padding( padding: const EdgeInsets.only(left: 8, top: 4), child: Column( crossAxisAlignment: isRow ? CrossAxisAlignment.start : CrossAxisAlignment.center, mainAxisAlignment: isRow ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ if (playlist['songs'] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text( S.of(context).nSongs(playlist['songs'].length), maxLines: 2, ), ), Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (playlist['songs'].isNotEmpty) AdaptiveFilledButton( onPressed: () { GetIt.I().playAll(playlist['songs']); }, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Platform.isWindows ? 8 : 35, ), ), color: context.isDarkMode ? Colors.white : Colors.black, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.play_arrow, color: context.isDarkMode ? Colors.black : Colors.white, size: 24, ), const SizedBox(width: 8), Text( S.of(context).Play_All, style: TextStyle(fontSize: 18), ), ], ), ), ], ), ], ), ); } @override Widget build(BuildContext context) { return SizedBox( width: double.maxFinite, child: Card( child: LayoutBuilder( builder: (context, constraints) { return constraints.maxWidth > 600 ? Row( children: [ if (playlist['songs'] != null) _buildImage( playlist['songs'], constraints.maxWidth, isRound: playlist['type'] == 'ARTIST', isDark: context.isDarkMode, ), const SizedBox(width: 4), Expanded( child: _buildContent(playlist, context, isRow: true), ), ], ) : Column( children: [ if (playlist['songs'] != null) _buildImage( playlist['songs'], constraints.maxWidth, isRound: playlist['type'] == 'ARTIST', isDark: context.isDarkMode, ), SizedBox(height: playlist['thumbnails'] != null ? 4 : 0), _buildContent(playlist, context), ], ); }, ), ), ); } } ================================================ FILE: lib/screens/player/player_page.dart ================================================ import 'dart:io'; import 'dart:math'; import 'dart:ui'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/screens/player/widgets/play_pause_button.dart'; import 'package:gyawun/utils/song_thumbnail.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:provider/provider.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:text_scroll/text_scroll.dart'; import 'package:yt_music/ytmusic.dart'; import '../../generated/l10n.dart'; import '../../services/download_manager.dart'; import '../../services/favourites_manager.dart'; import '../../services/media_player.dart'; import '../../themes/colors.dart'; import '../../themes/dark.dart'; import '../../themes/text_styles.dart'; import '../../utils/adaptive_widgets/adaptive_widgets.dart'; import '../../utils/bottom_modals.dart'; import 'widgets/lyrics_box.dart'; import 'widgets/queue_list.dart'; class PlayerPage extends StatefulWidget { const PlayerPage({super.key, this.videoId}); final String? videoId; @override State createState() => _PlayerPageState(); } class _PlayerPageState extends State { late PanelController panelController; final GlobalKey _key = GlobalKey(); Color? color; ImageProvider? image; bool canPop = false; bool showLyrics = false; bool fetchedSong = false; late MediaItem? currentSong; @override void initState() { super.initState(); panelController = PanelController(); if (widget.videoId != null) { GetIt.I().getSongDetails(widget.videoId!).then((song) { if (song != null) { GetIt.I().playSong(song); setState(() { fetchedSong = true; }); } }); } currentSong = GetIt.I().currentSongNotifier.value; GetIt.I().currentSongNotifier.addListener(songListener); } @override dispose() { GetIt.I().currentSongNotifier.removeListener(songListener); super.dispose(); } void songListener() { if (currentSong != GetIt.I().currentSongNotifier.value) { if (mounted) { setState(() { currentSong = GetIt.I().currentSongNotifier.value; }); } } } void setShowLyrics() { if (mounted) { setState(() { showLyrics = !showLyrics; }); } } Future updateBackgroundColor(ImageProvider image) async { final c = await ColorScheme.fromImageProvider(provider: image); if (mounted) { setState(() { color = c.primary; }); } } MaterialColor primaryWhite = const MaterialColor(0xFFFFFFFF, { 50: Color(0xFFFFFFFF), 100: Color(0xFFFFFFFF), 200: Color(0xFFFFFFFF), 300: Color(0xFFFFFFFF), 400: Color(0xFFFFFFFF), 500: Color(0xFFFFFFFF), 600: Color(0xFFFFFFFF), 700: Color(0xFFFFFFFF), 800: Color(0xFFFFFFFF), 900: Color(0xFFFFFFFF), }); @override Widget build(BuildContext context) { return Theme( data: darkTheme( colorScheme: ColorScheme.fromSeed( seedColor: primaryWhite, primary: primaryWhite, brightness: Brightness.dark, ), ), child: (widget.videoId != null && fetchedSong == false) ? const Center(child: AdaptiveProgressRing()) // ignore: deprecated_member_use : WillPopScope( onWillPop: () async { if (panelController.isAttached && panelController.isPanelOpen) { await panelController.close(); return false; } return true; }, child: AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarBrightness: Brightness.dark, statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, ), child: Container( color: Colors.black, child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeIn, decoration: BoxDecoration( gradient: LinearGradient( colors: [ (color ?? Theme.of( context, ).colorScheme.surfaceContainerLow) .withAlpha(200), (color ?? Theme.of( context, ).colorScheme.surfaceContainerLow) .withAlpha(80), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Scaffold( resizeToAvoidBottomInset: false, appBar: PreferredSize( preferredSize: AppBar().preferredSize, child: AppBar( backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, elevation: 0, iconTheme: const IconThemeData(color: Colors.white), leading: AdaptiveIconButton( onPressed: () { context.pop(); }, icon: Icon(AdaptiveIcons.chevron_down), ), actions: [ AdaptiveIconButton( onPressed: () { setState(() { showLyrics = !showLyrics; }); }, icon: Icon(AdaptiveIcons.lyrics), ), if (MediaQuery.of(context).size.width > MediaQuery.of(context).size.height || Platform.isWindows) AdaptiveIconButton( onPressed: () { _key.currentState?.openEndDrawer(); }, icon: Icon(AdaptiveIcons.queue), ), ], ), ), key: _key, backgroundColor: Colors.transparent, endDrawer: MediaQuery.of(context).size.width > MediaQuery.of(context).size.height || Platform.isWindows ? SizedBox( width: min(400, MediaQuery.of(context).size.width) - 50, child: const QueueList(), ) : null, body: SizedBox( width: double.maxFinite, child: LayoutBuilder( builder: (context, constraints) { EdgeInsets padding = MediaQuery.of( context, ).viewPadding; double maxWidth = constraints.maxWidth - padding.left - padding.right; double maxHeight = constraints.maxHeight - padding.top - padding.bottom; if (maxWidth > maxHeight) { return Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Artwork( setShowLyrics: setShowLyrics, showLyrics: showLyrics, width: maxWidth / 2.3, song: currentSong, onImageReady: updateBackgroundColor, ), NameAndControls( song: currentSong, width: maxWidth - (maxWidth / 2.3), height: maxHeight, isRow: true, ), ], ); } return Stack( children: [ Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ Artwork( setShowLyrics: setShowLyrics, showLyrics: showLyrics, width: min(maxWidth, maxHeight / 2.2) - 24, song: currentSong, onImageReady: updateBackgroundColor, ), NameAndControls( song: currentSong, width: maxWidth, height: maxHeight - min(maxWidth, maxHeight / 2.2) - 24, ), ], ), SlidingUpPanel( controller: panelController, color: Colors.transparent, padding: EdgeInsets.zero, margin: EdgeInsets.zero, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), boxShadow: const [], minHeight: 50 + MediaQuery.of(context).viewPadding.bottom, panel: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), child: Container( width: constraints.maxWidth, alignment: Alignment.center, decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ ClipRRect( child: BackdropFilter( filter: ImageFilter.blur( sigmaX: 3, sigmaY: 3, ), child: Container( height: 50 + MediaQuery.of( context, ).viewPadding.bottom, width: double.maxFinite, decoration: BoxDecoration( color: Theme.of(context) .scaffoldBackgroundColor .withAlpha(70), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( height: 5, width: 50, decoration: BoxDecoration( color: greyColor, borderRadius: BorderRadius.circular( 20, ), ), ), const SizedBox(height: 8), Text( S.of(context).Next_Up, style: textStyle( context, bold: true, ), ), ], ), ), ), ), const Expanded(child: QueueList()), ], ), ), ), ), ], ); }, ), ), ), ), ), ), ), ); } } class Artwork extends StatelessWidget { const Artwork({ this.song, required this.width, required this.showLyrics, required this.setShowLyrics, this.onImageReady, super.key, }); final double width; final MediaItem? song; final bool showLyrics; final Function setShowLyrics; final void Function(ImageProvider)? onImageReady; @override Widget build(BuildContext context) { return SizedBox( width: width, height: width, child: Padding( padding: const EdgeInsets.all(8.0), child: song == null ? Icon(Icons.music_note, size: width * 0.5) : Padding( padding: MediaQuery.of(context).viewPadding, child: LayoutBuilder( builder: (context, constraints) { return GestureDetector( onTap: () { setShowLyrics(); }, child: Center( child: showLyrics ? LyricsBox( currentSong: song!, size: Size(width, width), ) : Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(30), spreadRadius: 10, blurRadius: 10, offset: const Offset(0, 3), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: SongThumbnail( song: song!.extras!, onImageReady: onImageReady, ), ), ), ), ); }, ), ), ), ); } } class NameAndControls extends StatelessWidget { const NameAndControls({ this.song, required this.height, required this.width, this.isRow = false, super.key, }); final double width; final double height; final MediaItem? song; final bool isRow; @override Widget build(BuildContext context) { MediaPlayer mediaPlayer = context.watch(); return SizedBox( height: height, width: width, child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ TextScroll( song?.title ?? 'Title', key: Key(song?.title ?? 'Title'), style: bigTextStyle(context, bold: true), mode: TextScrollMode.endless, ), Text( song?.artist ?? song?.album ?? song?.extras?['subtitle'] ?? '', style: smallTextStyle(context), ), ], ), Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: mediaPlayer.progressBarState, builder: (context, ProgressBarState value, child) { return ProgressBar( progress: value.current, total: value.total, buffered: value.buffered, barHeight: 3, thumbRadius: 5, onSeek: (value) => mediaPlayer.player.seek(value), ); }, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ListenableBuilder( listenable: GetIt.I().listenable, builder: (context, child) { return AdaptiveIconButton( icon: Icon( GetIt.I().isFavourite( song?.extras, ) ? AdaptiveIcons.heart_fill : AdaptiveIcons.heart, size: 30, ), onPressed: () async { GetIt.I().addOrRemove( song?.extras, ); }, ); }, ), AdaptiveIconButton( onPressed: () { mediaPlayer.player.seekToPrevious(); }, icon: Icon(AdaptiveIcons.skip_previous, size: 30), ), const PlayPauseButton(size: 40), AdaptiveIconButton( onPressed: () { mediaPlayer.player.seekToNext(); }, icon: Icon(AdaptiveIcons.skip_next, size: 30), ), ValueListenableBuilder( valueListenable: mediaPlayer.loopMode, builder: (context, value, child) { return AdaptiveIconButton( onPressed: () { mediaPlayer.changeLoopMode(); }, isSelected: value != LoopMode.off, icon: Icon( value == LoopMode.off || value == LoopMode.all ? AdaptiveIcons.repeat_all : AdaptiveIcons.repeat_one, size: 30, color: value == LoopMode.off ? Colors.white.withValues(alpha: 0.3) : null, ), ); }, ), ], ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (song != null) RepaintBoundary( child: ListenableBuilder( listenable: GetIt.I().songListenable( song!.id, ), builder: (context, child) { final Map? item = GetIt.I() .getDownload(song!.id); if (item != null) { if (item['status'] == 'DOWNLOADING') { final notifier = GetIt.I().getProgressNotifier( song!.id, ) ?? ValueNotifier(0.0); return ValueListenableBuilder( valueListenable: notifier, builder: (context, double progress, child) { return CircularProgressIndicator( value: item['status'] == 'DOWNLOADING' && progress > 0.0 ? progress : null, color: Colors.white, backgroundColor: Colors.black, ); }, ); } else if (item['status'] == 'DOWNLOADED') { return const Icon(Icons.download_done_outlined); } } return AdaptiveIconButton( onPressed: () { GetIt.I().downloadSong( song!.extras!, ); }, icon: Icon(AdaptiveIcons.download, size: 30), ); }, ), ), AdaptiveIconButton( onPressed: () { Modals.showPlayerOptionsModal( context, mediaPlayer.currentSongNotifier.value!.extras!, ); }, icon: Icon(AdaptiveIcons.more_vertical, size: 30), ), ], ), if (song != null && !isRow) SizedBox(height: 55 + MediaQuery.of(context).viewPadding.bottom), ], ), ), ); } } ================================================ FILE: lib/screens/player/widgets/lyrics_box.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_lyric/lyrics_reader.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/lyrics.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:gyawun/services/settings_manager.dart'; import 'package:loading_indicator_m3e/loading_indicator_m3e.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class LyricsBox extends StatefulWidget { const LyricsBox({required this.currentSong, required this.size, super.key}); final MediaItem currentSong; final Size size; @override State createState() => _LyricsBoxState(); } class _LyricsBoxState extends State { Future? _fetchLyricsFuture; bool _lyricsLoaded = false; @override void initState() { super.initState(); _initFetchLyrics(); _initWakelock(); } void _initFetchLyrics() { GetIt.I().progressBarState.addListener(_progressListener); if (GetIt.I().progressBarState.value.total.inSeconds > 0) { _fetchLyrics(); } } void _progressListener() { if (GetIt.I().progressBarState.value.total.inSeconds > 0) { _fetchLyrics(); GetIt.I().progressBarState.removeListener(_progressListener); } } void _initWakelock() { GetIt.I().buttonState.addListener(_updateWakelock); } void _updateWakelock() { if (!mounted) return; final isPlaying = GetIt.I().buttonState.value == ButtonState.playing; if (isPlaying && _lyricsLoaded) { WakelockPlus.enable(); } else { WakelockPlus.disable(); } } @override void didUpdateWidget(covariant LyricsBox oldWidget) { super.didUpdateWidget(oldWidget); if (widget.currentSong != oldWidget.currentSong) { _initFetchLyrics(); } } void _fetchLyrics() { if (context.mounted) { setState(() { _fetchLyricsFuture = GetIt.I().getLyrics( videoId: widget.currentSong.id, title: widget.currentSong.title, artist: widget.currentSong.artist, album: widget.currentSong.album, durationInSeconds: GetIt.I().progressBarState.value.total.inSeconds, translation: context.read().translateLyrics ? context.read().language['value'] : null, ); _lyricsLoaded = false; _fetchLyricsFuture! .then((lyrics) { _lyricsLoaded = lyrics['syncedLyrics'] != null || lyrics['plainLyrics'] != null; _updateWakelock(); }) .catchError((_) { _lyricsLoaded = false; _updateWakelock(); }); }); } } @override void dispose() { GetIt.I().progressBarState.removeListener(_progressListener); GetIt.I().buttonState.removeListener(_updateWakelock); WakelockPlus.disable(); super.dispose(); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), child: Center( child: ValueListenableBuilder( valueListenable: GetIt.I().progressBarState, builder: (context, progress, child) { return progress.total.inSeconds > 0 && _fetchLyricsFuture != null ? FutureBuilder( future: _fetchLyricsFuture, builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data == null) { return const Text('No Lyrics'); } if (snapshot.data!['success'] == false) { return const Text('No Lyrics'); } Map lyrics = snapshot.data!; return ValueListenableBuilder( valueListenable: GetIt.I().progressBarState, builder: (context, progress, child) { try { return LyricsReader( padding: EdgeInsets.zero, position: progress.current.inMilliseconds, playing: context .watch() .player .playing, lyricUi: UINetease( highlight: false, defaultSize: 19, ), model: LyricsModelBuilder.create() .bindLyricToMain(lyrics['syncedLyrics']) .bindLyricToExt(lyrics['transLyrics']) .getModel(), emptyBuilder: () => SingleChildScrollView( child: Center( child: Text( lyrics['plainLyrics'] ?? "No Lyrics", style: UINetease( highlight: false, defaultSize: 19, ).getOtherMainTextStyle(), textAlign: TextAlign.center, ), ), ), size: widget.size, ); } catch (e) { debugPrint("Error parsing lyrics: $e"); return SingleChildScrollView( child: Center( child: Text( lyrics['plainLyrics'] ?? "No Lyrics", style: UINetease( highlight: false, defaultSize: 19, ).getOtherMainTextStyle(), textAlign: TextAlign.center, ), ), ); } }, ); } if (snapshot.hasError) { return const Text('No Lyrics'); } return const ExpressiveLoadingIndicator(); }, ) : const ExpressiveLoadingIndicator(); }, ), ), ); } } ================================================ FILE: lib/screens/player/widgets/play_pause_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/extensions.dart'; import 'package:loading_indicator_m3e/loading_indicator_m3e.dart'; class PlayPauseButton extends StatefulWidget { const PlayPauseButton({ super.key, this.size = 30, }); final double size; @override State createState() => _PlayPauseButtonState(); } class _PlayPauseButtonState extends State with TickerProviderStateMixin { late AnimationController _animationController; bool playing = false; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { GetIt.I().player.playing ? GetIt.I().player.pause() : GetIt.I().player.play(); }, child: ValueListenableBuilder( valueListenable: GetIt.I().buttonState, builder: (context, buttonState, child) { if (GetIt.I().player.playing != playing) { playing = GetIt.I().player.playing; playing ? _animationController.forward() : _animationController.reverse(); } return AnimatedContainer( duration: const Duration(milliseconds: 200), height: 60, width: 60, alignment: Alignment.center, decoration: BoxDecoration( color: (context.isDarkMode ? Colors.white : Colors.black) .withAlpha(50), borderRadius: BorderRadius.circular( buttonState == ButtonState.playing ? 15 : 40), ), child: (buttonState == ButtonState.loading) ? const ExpressiveLoadingIndicator() : AnimatedIcon( icon: AnimatedIcons.play_pause, progress: _animationController, size: 40, ), ); }, ), ); } } ================================================ FILE: lib/screens/player/widgets/queue_list.dart ================================================ import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/song_thumbnail.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; class QueueList extends StatelessWidget { const QueueList({super.key}); @override Widget build(BuildContext context) { final mediaPlayer = GetIt.I(); final player = mediaPlayer.player; return StreamBuilder( stream: mediaPlayer.currentTrackStream, builder: (context, snapshot) { final sequence = snapshot.data?.sequence ?? []; final currentIndex = snapshot.data?.currentIndex ?? 0; return Container( decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor.withAlpha(70), ), child: ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: SafeArea( top: false, child: Column( children: [ Expanded( child: ReorderableListView( onReorder: (oldIndex, newIndex) async { if (newIndex > oldIndex) newIndex -= 1; await player.moveAudioSource(oldIndex, newIndex); }, children: [ for (int i = 0; i < sequence.length; i++) QueueTile( key: Key('${sequence[i].tag?.id ?? "unknown"}_$i'), index: i, isCurrent: i == currentIndex, source: sequence[i], ), ], ), ), if (Platform.isAndroid) StreamBuilder( stream: player.shuffleModeEnabledStream, builder: (context, snapshot) { final shuffle = snapshot.data ?? false; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( style: ButtonStyle( backgroundColor: WidgetStatePropertyAll( shuffle ? Colors.white : Colors.white.withAlpha(50), ), foregroundColor: WidgetStatePropertyAll( shuffle ? Theme.of(context) .scaffoldBackgroundColor : Colors.white, ), ), onPressed: () { player.setShuffleModeEnabled(!shuffle); }, icon: const Icon(Icons.shuffle_outlined), label: Text(S.of(context).Shuffle), ), ], ), ); }, ), ], ), ), ), ), ); }, ); } } class QueueTile extends StatelessWidget { final int index; final bool isCurrent; final IndexedAudioSource source; const QueueTile({ super.key, required this.index, required this.isCurrent, required this.source, }); @override Widget build(BuildContext context) { final player = GetIt.I().player; final MediaItem? song = source.tag as MediaItem?; if (song == null) return const SizedBox(); return Dismissible( key: Key(song.id), direction: DismissDirection.endToStart, confirmDismiss: (_) async { await player.removeAudioSourceAt(index); return true; }, child: ListTile( key: Key(index.toString()), title: Text(song.title, maxLines: 1), leading: ArtworkWidget(song: song, isCurrent: isCurrent), subtitle: Text( song.artist ?? song.album ?? song.extras?['subtitle'] ?? '', maxLines: 1, ), trailing: const Icon(Icons.drag_handle), onTap: () { player.seek(Duration.zero, index: index); }, ), ); } } class ArtworkWidget extends StatelessWidget { final MediaItem song; final bool isCurrent; const ArtworkWidget({ super.key, required this.song, required this.isCurrent, }); @override Widget build(BuildContext context) { final double dp = MediaQuery.of(context).devicePixelRatio; return ClipRRect( borderRadius: BorderRadius.circular(8), child: Stack( children: [ SongThumbnail( song: song.extras!, dp: dp, height: 50, width: 50, fit: BoxFit.fill, errorWidget: (_, _, _) => const Icon(Icons.music_note, size: 32), ), if (isCurrent) Container( height: 50, width: 50, color: Colors.black.withValues(alpha:0.6), ), if (isCurrent) const Positioned( width: 34, height: 34, left: 8, top: 8, child: Center( child: Icon( Icons.music_note_outlined, color: Colors.white, ), ), ), ], ), ); } } ================================================ FILE: lib/screens/search/cubit/search_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:yt_music/ytmusic.dart'; import '../../../services/history_manager.dart'; part 'search_state.dart'; class SearchCubit extends Cubit { final YTMusic _ytmusic; Map? endpoint; late final SearchHistory _searchHistory; SearchCubit(this._ytmusic, {this.endpoint}) : super(SearchInitial()) { _searchHistory = GetIt.I().searches; if (endpoint != null) { search(''); } } Future search(String query) async { emit(const SearchLoading()); try { await _searchHistory.add(query); final feed = await _ytmusic.search(query, endpoint: endpoint); emit( SearchSuccess( sections: feed['sections'], continuation: feed['continuation'], loadingMore: false, ), ); } catch (e, st) { debugPrint(e.toString()); debugPrint(st.toString()); emit(SearchError(e.toString(), st.toString())); } } Future fetchNext() async { final current = state; if (current is! SearchSuccess) return; if (current.loadingMore || current.continuation == null) return; emit(current.copyWith(loadingMore: true)); try { final feed = await _ytmusic.search( '', endpoint: endpoint, additionalParams: current.continuation!, ); SearchSuccess( sections: [...current.sections, ...feed['sections']], continuation: feed['continuation'], loadingMore: false, ); } catch (e, st) { emit(SearchError(e.toString(), st.toString())); } } Future>> getSuggestions(String query) async { try { List> suggestions = await _ytmusic .getSearchSuggestions(query); return suggestions.isNotEmpty ? suggestions : _searchHistory.getList(); } catch (e) { return []; } } } ================================================ FILE: lib/screens/search/cubit/search_state.dart ================================================ part of 'search_cubit.dart'; @immutable sealed class SearchState { const SearchState(); } final class SearchInitial extends SearchState{ const SearchInitial(); } final class SearchLoading extends SearchState { const SearchLoading(); } final class SearchError extends SearchState { final String? message; final String? stackTrace; const SearchError([this.message, this.stackTrace]); } final class SearchSuccess extends SearchState { final List sections; final bool loadingMore; final String? continuation; const SearchSuccess({ required this.sections, required this.continuation, required this.loadingMore, }); SearchSuccess copyWith({ List? sections, String? continuation, bool? loadingMore, }) { return SearchSuccess( sections: sections ?? this.sections, continuation: continuation ?? this.continuation, loadingMore: loadingMore ?? this.loadingMore, ); } } ================================================ FILE: lib/screens/search/search_page.dart ================================================ import 'dart:io'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/internet_guard.dart'; import 'package:gyawun/core/utils/service_locator.dart'; import 'package:gyawun/screens/search/cubit/search_cubit.dart'; import 'package:loading_indicator_m3e/loading_indicator_m3e.dart'; import '../../../generated/l10n.dart'; import '../../../services/media_player.dart'; import '../../../utils/adaptive_widgets/adaptive_widgets.dart'; import '../../../utils/bottom_modals.dart'; import '../../core/widgets/tiles/section_list_tile.dart'; class SearchPage extends StatelessWidget { const SearchPage({super.key, this.endpoint, this.isMore = false}); final Map? endpoint; final bool isMore; @override Widget build(BuildContext context) { return BlocProvider( create: (_) => SearchCubit(sl(), endpoint: endpoint), child: _SearchPage(title: endpoint?['query'], isMore: isMore), ); } } class _SearchPage extends StatefulWidget { const _SearchPage({this.title, this.isMore = false}); final String? title; final bool isMore; @override State<_SearchPage> createState() => _SearchPageState(); } class _SearchPageState extends State<_SearchPage> with WidgetsBindingObserver { late ScrollController _scrollController; final TextEditingController _textEditingController = TextEditingController(); final SuggestionsController _suggestionsController = SuggestionsController(); final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); _scrollController = ScrollController(); _scrollController.addListener(_scrollListener); WidgetsBinding.instance.addObserver(this); _focusNode.requestFocus(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _scrollController.dispose(); _textEditingController.dispose(); _suggestionsController.dispose(); _focusNode.dispose(); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); if (_focusNode.hasFocus) { Future.delayed(const Duration(milliseconds: 300), () { if (mounted) { _suggestionsController.resize(); } }); } } Future _scrollListener() async { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { await context.read().fetchNext(); } } Future onSubmit(String query) async { if (query.trim() == '') return; _focusNode.unfocus(); await context.read().search(query); } @override Widget build(BuildContext context) { return InternetGuard( onConnectivityRestored: () { if (_textEditingController.text.isNotEmpty) { context.read().search(_textEditingController.text); } }, child: Scaffold( appBar: PreferredSize( preferredSize: const AdaptiveAppBar().preferredSize, child: LayoutBuilder( builder: (context, constraints) { return AdaptiveAppBar( title: widget.title != null ? Text(widget.title!) : Material( color: Colors.transparent, child: Row( children: [ Expanded( child: TypeAheadField( focusNode: _focusNode, controller: _textEditingController, suggestionsController: _suggestionsController, suggestionsCallback: (query) => context .read() .getSuggestions(query), loadingBuilder: (context) => Container( height: 60, alignment: Alignment.center, child: ExpressiveLoadingIndicator(), ), builder: (context, controller, focusNode) { return AdaptiveTextField( focusNode: focusNode, controller: controller, onSubmitted: onSubmit, keyboardType: TextInputType.text, maxLines: 1, autofocus: true, textInputAction: TextInputAction.search, fillColor: Theme.of( context, ).colorScheme.surfaceContainer, contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 8, ), borderRadius: BorderRadius.circular( Platform.isWindows ? 4.0 : 35, ), hintText: S.of(context).Search_Gyawun, prefix: constraints.maxWidth > 400 ? null : const AdaptiveBackButton(), suffix: GestureDetector( onTap: () { setState(() { _textEditingController.text = ''; }); }, child: const Icon( FluentIcons.dismiss_24_filled, ), ), ); }, decorationBuilder: (context, child) { if (Platform.isWindows) { return Ink( padding: EdgeInsets.zero, decoration: BoxDecoration( color: AdaptiveTheme.of( context, ).inactiveBackgroundColor, borderRadius: BorderRadius.circular(4), ), child: child, ); } else { return Material( elevation: 5, color: Theme.of( context, ).colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(10), child: child, ); } }, itemBuilder: (context, value) { if (value['type'] == 'TEXT') { return AdaptiveListTile( leading: value['isHistory'] != null ? const Icon(Icons.history) : null, title: Text(value['query']), onTap: () { setState(() { _textEditingController.text = value['query']; }); onSubmit(value['query']); }, ); } return _SearchListTile(item: value); }, onSelected: (value) => (), hideOnEmpty: true, ), ), ], ), ), automaticallyImplyLeading: (constraints.maxWidth <= 400) ? false : true, ); }, ), ), body: BlocBuilder( builder: (context, state) { switch (state) { case SearchInitial(): return SizedBox.shrink(); case SearchLoading(): return Center(child: LoadingIndicatorM3E()); case SearchError(): return Center(child: Text(state.message ?? '')); case SearchSuccess(): return SingleChildScrollView( controller: _scrollController, child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), padding: const EdgeInsets.all(8), child: Column( children: [ ...state.sections.asMap().entries.map((entry) { int index = entry.key; var section = entry.value; if (Platform.isWindows) { return Center( child: Adaptivecard( borderRadius: BorderRadius.circular(8), child: _SearchSectionItem( section: section, isFirst: index == 0, isMore: widget.isMore, ), ), ); } return _SearchSectionItem( section: section, isFirst: index == 0, isMore: widget.isMore, ); }), if (state.loadingMore) const Center(child: ExpressiveLoadingIndicator()), ], ), ), ), ); } }, ), ), ); } } class _SearchSectionItem extends StatelessWidget { const _SearchSectionItem({ required this.section, this.isFirst = false, this.isMore = false, }); final Map section; final bool isFirst; final bool isMore; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( children: [ if (!isMore) AdaptiveListTile( contentPadding: const EdgeInsets.symmetric( vertical: 4, horizontal: 4, ), title: Text( section['title'] ?? isFirst ? S().Top_Results : S().Other_Results, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w700, fontSize: 18, ), ), trailing: section['trailing']?['text'] != null ? Padding( padding: EdgeInsets.symmetric( vertical: Platform.isAndroid ? 12 : 0, ), child: AdaptiveOutlinedButton( onPressed: () { context.push( '/search', extra: { 'endpoint': section['trailing']['endpoint'], 'isMore': true, }, ); }, child: Text( section['trailing']['text'], style: const TextStyle(fontSize: 12), ), ), ) : null, ), ...section['contents'].asMap().entries.map((entry) { int index = entry.key; var item = entry.value; return Padding( padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 4), child: SectionListTile( item: item, isFirst: index == 0, isLast: index == section['contents'].length - 1, ), ); }).toList(), ], ), ); } } class _SearchListTile extends StatelessWidget { const _SearchListTile({required this.item}); final Map item; @override Widget build(BuildContext context) { return AdaptiveListTile( onSecondaryTap: () { if (item['videoId'] != null) { Modals.showSongBottomModal(context, item); } else if (item['endpoint'] != null) { Modals.showPlaylistBottomModal(context, item); } }, onTap: () async { if (item['videoId'] != null) { await GetIt.I().playSong(Map.from(item)); } else if (item['endpoint'] != null && item['videoId'] == null) { context.push('/browse', extra: {'endpoint': item['endpoint']}); } }, onLongPress: () { if (item['videoId'] != null) { Modals.showSongBottomModal(context, item); } else if (item['endpoint'] != null) { Modals.showPlaylistBottomModal(context, item); } }, dense: false, title: Text(item['title'], maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: item['subtitle'] != null ? Text( item['subtitle'], maxLines: 1, style: TextStyle(color: Colors.grey.withValues(alpha: 0.9)), overflow: TextOverflow.ellipsis, ) : null, leading: item['thumbnails'] != null && item['thumbnails'].isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular( ['ARTIST', 'PROFILE'].contains(item['type']) ? 30 : 3, ), child: Image.network(item['thumbnails'].first['url'], width: 50), ) : null, trailing: item['videoId'] == null && item['endpoint'] != null ? const Icon(CupertinoIcons.chevron_right) : null, ); } } ================================================ FILE: lib/screens/settings/about/about_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../generated/l10n.dart'; import '../widgets/color_icon.dart'; class AboutPage extends StatefulWidget { const AboutPage({super.key}); @override State createState() => _AboutPageState(); } class _AboutPageState extends State { String? _version; @override void initState() { super.initState(); _loadVersion(); } Future _loadVersion() async { final info = await PackageInfo.fromPlatform(); if (!mounted) return; setState(() { // Example: 2.0.16-beta.3 or 2.0.16 _version = info.version; }); } void _open(String url) { launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar(title: S.of(context).About, hasLeading: true), ]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ Center( child: Column( children: [ Container( height: 100, width: 100, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], ), clipBehavior: Clip.antiAlias, child: Image.asset( 'assets/images/icon.png', height: 100, width: 100, fit: BoxFit.cover, errorBuilder: (_, _, _) => Icon( Icons.music_note_rounded, size: 48, color: colorScheme.primary, ), ), ), const SizedBox(height: 16), Text( 'Gyawun Music', style: textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w700, color: colorScheme.onSurface, letterSpacing: -0.5, ), ), const SizedBox(height: 8), if (_version != null) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: colorScheme.tertiaryContainer, ), child: Text( 'Version $_version', style: textTheme.labelLarge?.copyWith( color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ), ], ), ), const SizedBox(height: 16), ExpressiveListGroup( title: "About", children: [ ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.person), title: const Text("Developer"), subtitle: const Text("Sheikh Haziq"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open('https://github.com/sheikhhaziq'), ), ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.link), title: const Text("Website"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open('https://gyawunmusic.vercel.app'), ), ], ), const SizedBox(height: 24), ExpressiveListGroup( title: "Community", children: [ ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.people), title: const Text("Contributors"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open( 'https://github.com/jhelumcorp/gyawun/contributors', ), ), ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.send), title: const Text("Telegram"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open('https://t.me/jhelumcorp'), ), ], ), const SizedBox(height: 24), ExpressiveListGroup( title: "Development", children: [ ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.code), title: const Text("Source Code"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open('https://github.com/jhelumcorp/gyawun'), ), ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.bug_report), title: const Text("Bug Report"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open( 'https://github.com/sheikhhaziq/gyawun_music/issues/new?template=bug_report.yml', ), ), ExpressiveListTile( leading: const SettingsColorIcon(icon: Icons.description), title: const Text("Feature Request"), trailing: const Icon(FluentIcons.chevron_right_24_filled), onTap: () => _open( 'https://github.com/sheikhhaziq/gyawun_music/discussions', ), ), ], ), const SizedBox(height: 16), ], ), ), ), ), ); } } ================================================ FILE: lib/screens/settings/appearance/appearance_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/core/extensions/string_extensions.dart'; import 'package:gyawun/core/utils/expressive_sheet.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/core/widgets/expressive_switch_list_tile.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import 'package:gyawun/services/settings_manager.dart'; import '../../../generated/l10n.dart'; import 'cubit/appearance_cubit.dart'; class AppearancePage extends StatelessWidget { const AppearancePage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => AppearanceCubit(), child: Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar( title: S.of(context).Appearence, hasLeading: true, ), ]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: BlocBuilder( builder: (context, state) { final s = state as AppearanceLoaded; return ListView( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), children: [ ExpressiveListGroup( title: 'Theme', children: [ ExpressiveListTile( title: Text(S.of(context).Theme_Mode), subtitle: Text(s.themeMode.name.capitalize()), leading: SettingsColorIcon( icon: FluentIcons.dark_theme_24_filled, ), onTap: () async { final selected = await ExpressiveSheet.showSelection( context, title: "Choose Theme", options: [ ExpressiveSheetOption( label: "System Default", icon: FluentIcons.system_24_filled, value: ThemeMode.system, ), ExpressiveSheetOption( label: "Light Mode", icon: FluentIcons.lightbulb_24_filled, value: ThemeMode.light, ), ExpressiveSheetOption( label: "Dark Mode", icon: FluentIcons.dark_theme_24_filled, value: ThemeMode.dark, ), ], ); if (selected == null) return; if (context.mounted) { context.read().setThemeMode( selected, ); } }, ), ExpressiveListTile( title: Text('Accent Color'), leading: SettingsColorIcon( icon: FluentIcons.color_24_filled, ), trailing: CircleAvatar( radius: 20, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Row( children: [ Container( color: s.accentColor ?? Colors.black, width: 20, ), Container( color: s.accentColor ?? Colors.white, width: 20, ), ], ), ), ), onTap: () async { final selected = await ExpressiveSheet.showColorSelection( context, title: 'Select Accent Color', ); if (selected != null) { GetIt.I().accentColor = selected; } }, ), ExpressiveSwitchListTile( title: Text('Amoled Black'), leading: const SettingsColorIcon( icon: FluentIcons.drop_24_filled, ), value: s.amoledBlack, onChanged: (value) { context.read().setAmoledBlack( value, ); }, ), /// Dynamic colors ExpressiveSwitchListTile( title: Text(S.of(context).Dynamic_Colors), leading: const SettingsColorIcon( icon: FluentIcons.color_background_24_filled, ), value: s.dynamicColors, onChanged: (value) { context.read().setDynamicColors( value, ); }, ), ], ), ], ); }, ), ), ), ), ), ); } } ================================================ FILE: lib/screens/settings/appearance/cubit/appearance_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/settings_manager.dart'; part 'appearance_state.dart'; class AppearanceCubit extends Cubit { final SettingsManager _settings = GetIt.I(); late final VoidCallback _listener; AppearanceCubit() : super( AppearanceLoaded( themeMode: GetIt.I().themeMode, accentColor: GetIt.I().accentColor, amoledBlack: GetIt.I().amoledBlack, dynamicColors: GetIt.I().dynamicColors, ), ) { _listener = () { if (!isClosed) { _emitState(); } }; _settings.addListener(_listener); } void _emitState() { if (isClosed) return; emit( AppearanceLoaded( themeMode: _settings.themeMode, accentColor: _settings.accentColor, amoledBlack: _settings.amoledBlack, dynamicColors: _settings.dynamicColors, ), ); } Future setThemeMode(ThemeMode mode) async { await _settings.setThemeMode(mode); // listener will emit } void setAmoledBlack(bool value) { _settings.amoledBlack = value; } void setDynamicColors(bool value) { _settings.dynamicColors = value; } @override Future close() { _settings.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/settings/appearance/cubit/appearance_state.dart ================================================ part of 'appearance_cubit.dart'; @immutable sealed class AppearanceState { const AppearanceState(); } class AppearanceLoaded extends AppearanceState { final ThemeMode themeMode; final Color? accentColor; final bool amoledBlack; final bool dynamicColors; const AppearanceLoaded({ required this.themeMode, required this.accentColor, required this.amoledBlack, required this.dynamicColors, }); } ================================================ FILE: lib/screens/settings/backup_storage/backup_storage_page.dart ================================================ import 'dart:io'; import 'package:easy_folder_picker/FolderPicker.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import '../../../generated/l10n.dart'; import '../../../themes/text_styles.dart'; import '../../../utils/bottom_modals.dart'; import '../../../services/bottom_message.dart'; import 'cubit/backup_storage_cubit.dart'; class BackupStoragePage extends StatelessWidget { const BackupStoragePage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => BackupStorageCubit(), child: BlocListener( listenWhen: (_, state) => state.lastResult != null, listener: (context, state) { final result = state.lastResult; if (result == null) return; if (result is BackupSuccess) { BottomMessage.showText( context, '${S.of(context).Backup_Success} ${result.path}', ); } else if (result is BackupFailure) { BottomMessage.showText(context, S.of(context).Backup_Failed); } else if (result is RestoreSuccess) { BottomMessage.showText(context, S.of(context).Restore_Success); } else if (result is RestoreFailure) { BottomMessage.showText(context, S.of(context).Restore_Failed); } }, child: const _BackupStoragePage(), ), ); } } class _BackupStoragePage extends StatelessWidget { const _BackupStoragePage(); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar( title: S.of(context).Backup_And_Restore, hasLeading: true, ), ]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: BlocBuilder( builder: (context, state) { final cubit = context.read(); return ListView( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), children: [ if (Platform.isAndroid) ...[ ExpressiveListGroup( title: 'Storage', children: [ ExpressiveListTile( title: Text("App Folder"), leading: const SettingsColorIcon( icon: FluentIcons.folder_24_filled, ), subtitle: Text(state.appFolder), trailing: FilledButton.tonal( child: const Text('Change'), onPressed: () async { final appFolder = Directory(state.appFolder); final rootDirectory = await appFolder.exists() ? appFolder : Directory(state.defaultPath); if (!context.mounted) return; final dir = await FolderPicker.pick( context: context, allowFolderCreation: true, rootDirectory: rootDirectory, ); if (dir != null) { cubit.setAppFolder(dir.path); } }, ), ), ], ), ], SizedBox(height: 24), ExpressiveListGroup( title: S.of(context).Backup_And_Restore, children: [ ExpressiveListTile( title: Text(S.of(context).Backup), leading: const SettingsColorIcon( icon: Icons.backup_rounded, ), onTap: () async { final result = await showBackupSelector(context); if (result == null) return; cubit.backup(action: result.$1, items: result.$2); }, ), ExpressiveListTile( title: Text(S.of(context).Restore), leading: const SettingsColorIcon( icon: Icons.restore_rounded, ), onTap: cubit.restore, ), ], ), ], ); }, ), ), ), ), ); } } /* ──────────────────────────────────────────────────────────────── */ /* BACKUP SELECTION MODAL (UI ONLY) */ /* ──────────────────────────────────────────────────────────────── */ Future<(String, List)?> showBackupSelector(BuildContext context) async { return await showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) { final items = ValueNotifier>>([ {'name': 'Favourites', 'selected': false}, {'name': 'Playlists', 'selected': false}, {'name': 'Settings', 'selected': false}, {'name': 'Song History', 'selected': false}, {'name': 'Downloads', 'selected': false}, ]); return BottomModalLayout( title: Text( S.of(context).Select_Backup, style: mediumTextStyle(context), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Divider(), ValueListenableBuilder( valueListenable: items, builder: (_, backups, _) { return Column( children: backups.indexed.map((el) { final index = el.$1; final item = el.$2; return CheckboxListTile( title: Text(item['name']), value: item['selected'], onChanged: (val) { final newItems = List>.from( items.value, ); newItems[index]['selected'] = val; items.value = newItems; }, ); }).toList(), ); }, ), Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _backupActionButton( context, label: S.of(context).Share, action: 'Share', items: items, ), const SizedBox(width: 20), _backupActionButton( context, label: S.of(context).Save, action: 'Save', items: items, ), ], ), ), ], ), ), ); }, ); } Widget _backupActionButton( BuildContext context, { required String label, required String action, required ValueNotifier>> items, }) { return MaterialButton( color: Theme.of(context).colorScheme.primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), onPressed: () { final selected = items.value .where((e) => e['selected'] == true) .map((e) => e['name'].toLowerCase()) .toList(); Navigator.pop(context, selected.isEmpty ? null : (action, selected)); }, child: Text( label, style: TextStyle(color: Theme.of(context).scaffoldBackgroundColor), ), ); } ================================================ FILE: lib/screens/settings/backup_storage/cubit/backup_storage_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import '../../../../../../services/file_storage.dart'; import '../../../../../../services/library.dart'; import '../../../../../../services/settings_manager.dart'; import '../../../../../../services/favourites_manager.dart'; import '../../../../services/download_manager.dart'; import '../../../../services/history_manager.dart'; part 'backup_storage_state.dart'; class BackupStorageCubit extends Cubit { final SettingsManager _settingsManager = GetIt.I(); final FileStorage _fileStorage = GetIt.I(); final DownloadManager _downloadsManager = GetIt.I(); final LibraryService _library = GetIt.I(); late final VoidCallback _listener; BackupStorageCubit() : super( BackupStorageState( appFolder: GetIt.I().appFolder, defaultPath: FileStorage.defaultPath, ), ) { _listener = _emit; _settingsManager.addListener(_listener); } void _emit() { if (isClosed) return; emit( state.copyWith( appFolder: _settingsManager.appFolder, lastResult: null, // clear one-shot result ), ); } Future setAppFolder(String path) async { _settingsManager.appFolder = path; await _fileStorage.setupPaths(); } Future restore() async { final success = await _fileStorage.loadBackup(); if (success) { await _downloadsManager.reInit(); await _library.reInit(); } emit( state.copyWith( lastResult: success ? const RestoreSuccess() : const RestoreFailure(), ), ); } Future backup({required String action, required List items}) async { final Map backup = { 'name': 'Gyawun', 'type': 'backup', 'version': 1, 'data': {}, }; if (items.contains('playlists')) { backup['data']['playlists'] = GetIt.I().playlists; } if (items.contains('settings')) { final settings = Map.from( GetIt.I().settings, ); settings.remove('YTMUSIC_AUTH'); backup['data']['settings'] = settings; } if (items.contains('favourites')) { Map favourites = GetIt.I().songs; backup['data']['favourites'] = favourites; } if (items.contains('song history')) { Map history = GetIt.I().songs.all; backup['data']['song_history'] = history; } if (items.contains('downloads')) { Map downloads = GetIt.I().downloads; backup['data']['downloads'] = downloads; } String? path; if (action == 'Save') { path = await _fileStorage.saveBackUp(backup); } else { path = await _fileStorage.shareBackUp(backup); } emit( state.copyWith( lastResult: (path.isEmpty) ? const BackupFailure() : BackupSuccess(path), ), ); } @override Future close() { _settingsManager.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/settings/backup_storage/cubit/backup_storage_state.dart ================================================ part of 'backup_storage_cubit.dart'; @immutable class BackupStorageState { final String appFolder; final String defaultPath; /// one-shot results final BackupResult? lastResult; const BackupStorageState({ required this.appFolder, required this.defaultPath, this.lastResult, }); BackupStorageState copyWith({ String? appFolder, String? defaultPath, BackupResult? lastResult, }) { return BackupStorageState( appFolder: appFolder ?? this.appFolder, defaultPath: defaultPath ?? this.defaultPath, lastResult: lastResult, ); } } sealed class BackupResult { const BackupResult(); } class BackupSuccess extends BackupResult { final String path; const BackupSuccess(this.path); } class BackupFailure extends BackupResult { const BackupFailure(); } class RestoreSuccess extends BackupResult { const RestoreSuccess(); } class RestoreFailure extends BackupResult { const RestoreFailure(); } ================================================ FILE: lib/screens/settings/cubit/settings_system_cubit.dart ================================================ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; part 'settings_system_state.dart'; class SettingsSystemCubit extends Cubit { SettingsSystemCubit() : super(const SettingsSystemInitial()); Future load() async { if (!Platform.isAndroid) { emit(const SettingsSystemLoaded( isBatteryOptimizationDisabled: null, )); return; } final granted = await Permission.ignoreBatteryOptimizations.isGranted; emit( SettingsSystemLoaded( isBatteryOptimizationDisabled: granted, ), ); } Future requestBatteryOptimizationIgnore() async { if (!Platform.isAndroid) return; await Permission.ignoreBatteryOptimizations.request(); await load(); } } ================================================ FILE: lib/screens/settings/cubit/settings_system_state.dart ================================================ part of 'settings_system_cubit.dart'; @immutable sealed class SettingsSystemState { const SettingsSystemState(); } class SettingsSystemInitial extends SettingsSystemState { const SettingsSystemInitial(); } class SettingsSystemLoaded extends SettingsSystemState { final bool? isBatteryOptimizationDisabled; const SettingsSystemLoaded({ required this.isBatteryOptimizationDisabled, }); } ================================================ FILE: lib/screens/settings/player/cubit/player_settings_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import '../../../../../../services/media_player.dart'; import '../../../../../../services/settings_manager.dart'; part 'player_settings_state.dart'; class PlayerSettingsCubit extends Cubit { final SettingsManager _settings = GetIt.I(); final MediaPlayer _player = GetIt.I(); late final VoidCallback _listener; PlayerSettingsCubit() : super( PlayerSettingsLoaded( skipSilence: GetIt.I().skipSilence, ), ) { _listener = () { if (!isClosed) { _emitState(); } }; _settings.addListener(_listener); } void _emitState() { if (isClosed) return; emit( PlayerSettingsLoaded( skipSilence: _settings.skipSilence, ), ); } Future setSkipSilence(bool value) async { await _player.skipSilence(value); _settings.skipSilence = value; // listener will re-emit } @override Future close() { _settings.removeListener(_listener); return super.close(); } } ================================================ FILE: lib/screens/settings/player/cubit/player_settings_state.dart ================================================ part of 'player_settings_cubit.dart'; @immutable sealed class PlayerSettingsState { const PlayerSettingsState(); } class PlayerSettingsLoaded extends PlayerSettingsState { final bool skipSilence; const PlayerSettingsLoaded({ required this.skipSilence, }); } ================================================ FILE: lib/screens/settings/player/equalizer/cubit/equalizer_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/services/settings_manager.dart'; import 'equalizer_state.dart'; class EqualizerCubit extends Cubit { final MediaPlayer _mediaPlayer = GetIt.I(); EqualizerCubit() : super(EqualizerLoading()) { _getEqualizerParameters(); } Future _getEqualizerParameters() async { if (isClosed) return; final parameters = await _mediaPlayer.getEqualizerParameters(); emit( EqualizerLoaded( enabled: GetIt.I().equalizerEnabled, maxDb: parameters['maxDecibels'], minDb: parameters['minDecibels'], bands: parameters['bands'], ), ); } Future toggle(bool enabled) async { if (state is EqualizerLoading) return; await _mediaPlayer.setEqualizerEnabled(enabled); _getEqualizerParameters(); } Future setBandGain(int index, double gain) async { if (state is EqualizerLoading) return; await _mediaPlayer.setEqualizerBandGain(index, gain); _getEqualizerParameters(); } } ================================================ FILE: lib/screens/settings/player/equalizer/cubit/equalizer_state.dart ================================================ import 'package:flutter/foundation.dart'; @immutable class EqualizerState { const EqualizerState(); } class EqualizerLoading extends EqualizerState { const EqualizerLoading(); } class EqualizerLoaded extends EqualizerState { final bool enabled; final double minDb; final double maxDb; final List bands; const EqualizerLoaded({ required this.enabled, required this.minDb, required this.maxDb, required this.bands, }); } ================================================ FILE: lib/screens/settings/player/equalizer/cubit/loudness_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/services/settings_manager.dart'; import 'loudness_state.dart'; class LoudnessCubit extends Cubit { final MediaPlayer _mediaPlayer = GetIt.I(); LoudnessCubit() : super( LoudnessState( enabled: GetIt.I().loudnessEnabled, targetGain: GetIt.I().loudnessTargetGain, ), ); Future toggle(bool enabled) async { await _mediaPlayer.setLoudnessEnabled(enabled); emit(state.copyWith(enabled: enabled)); } Future setTargetGain(double gain) async { await _mediaPlayer.setLoudnessTargetGain(gain); emit(state.copyWith(targetGain: gain)); } } ================================================ FILE: lib/screens/settings/player/equalizer/cubit/loudness_state.dart ================================================ import 'package:flutter/foundation.dart'; @immutable class LoudnessState { final bool enabled; final double targetGain; const LoudnessState({ required this.enabled, required this.targetGain, }); LoudnessState copyWith({ bool? enabled, double? targetGain, }) { return LoudnessState( enabled: enabled ?? this.enabled, targetGain: targetGain ?? this.targetGain, ); } } ================================================ FILE: lib/screens/settings/player/equalizer/equalizer_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gyawun/screens/settings/player/equalizer/cubit/equalizer_cubit.dart'; import 'package:gyawun/screens/settings/player/equalizer/cubit/equalizer_state.dart'; import 'package:gyawun/screens/settings/player/equalizer/cubit/loudness_cubit.dart'; import 'package:gyawun/screens/settings/player/equalizer/cubit/loudness_state.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/screens/settings/widgets/setting_item.dart'; import 'package:gyawun/themes/text_styles.dart'; import 'package:gyawun/utils/adaptive_widgets/slider.dart'; class EqualizerPage extends StatelessWidget { const EqualizerPage({super.key}); @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider(create: (_) => EqualizerCubit()), BlocProvider(create: (_) => LoudnessCubit()), ], child: const _EqualizerView(), ); } } class _EqualizerView extends StatelessWidget { const _EqualizerView(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( S.of(context).Loudness_And_Equalizer, style: mediumTextStyle(context, bold: false), ), centerTitle: true, ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ /// LOUDNESS GroupTitle(title: "Loudness"), BlocBuilder( builder: (context, state) { return SettingSwitchTile( leading: const Icon(Icons.volume_up), title: S.of(context).Loudness_Enhancer, isFirst: true, value: state.enabled, onChanged: (val) async { context.read().toggle(val); }, ); }, ), SettingEmptyTile( isLast: true, child: BlocBuilder( builder: (context, state) { return Slider( min: -1, max: 1, value: state.targetGain, onChanged: state.enabled ? (val) async { context.read().setTargetGain(val); } : null, ); }, ), ), /// EQUALIZER GroupTitle(title: "Equalizer"), BlocBuilder( builder: (context, state) { return SettingSwitchTile( leading: const Icon(Icons.equalizer), title: S.of(context).Enable_Equalizer, isFirst: true, value: state is EqualizerLoaded ? state.enabled : false, onChanged: (val) async { context.read().toggle(val); }, ); }, ), SettingEmptyTile( isLast: true, child: BlocBuilder( builder: (context, state) { if (state is EqualizerLoaded) { if (!state.enabled) { return const SizedBox(); } return SizedBox( height: 250, child: Row( children: [ for (final band in state.bands) Expanded( child: Column( children: [ Text(band['gain'].toStringAsFixed(1)), Expanded( child: AdaptiveSlider( vertical: true, min: state.minDb, max: state.maxDb, value: band['gain'], onChanged: (val) async { context .read() .setBandGain(band['index'], val); }, ), ), Text('${band['centerFrequency'].round()} Hz'), ], ), ), ], ), ); } else { return SizedBox( child: Text( S.of(context).View_Equalizer, textAlign: TextAlign.center, style: TextStyle( color: Colors.grey.withValues(alpha: 0.5), fontStyle: FontStyle.italic, ), ), ); } }, ), ), ], ), ); } } ================================================ FILE: lib/screens/settings/player/player_settings_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/core/widgets/expressive_switch_list_tile.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import '../../../generated/l10n.dart'; import 'cubit/player_settings_cubit.dart'; class PlayerSettingsPage extends StatelessWidget { const PlayerSettingsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => PlayerSettingsCubit(), child: Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ExpressiveAppBar(title: "Player", hasLeading: true)]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: BlocBuilder( builder: (context, state) { final s = state as PlayerSettingsLoaded; return ListView( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), children: [ ExpressiveListGroup( children: [ ExpressiveListTile( title: Text(S.of(context).Loudness_And_Equalizer), leading: SettingsColorIcon( icon: Icons.equalizer_rounded, ), trailing: Icon(FluentIcons.chevron_right_24_filled), onTap: () => context.go('/settings/player/equalizer'), ), ExpressiveSwitchListTile( title: Text(S.of(context).Skip_Silence), leading: SettingsColorIcon( icon: FluentIcons.fast_forward_24_filled, ), value: s.skipSilence, onChanged: (value) { context .read() .setSkipSilence(value); }, ), ], ), ], ); }, ), ), ), ), ), ); } } ================================================ FILE: lib/screens/settings/privacy/cubit/privacy_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/settings_manager.dart'; import '../../../../services/history_manager.dart'; part 'privacy_state.dart'; class PrivacyCubit extends Cubit { late final SettingsManager _settingsManager; late final HistoryManager _historyManager; PrivacyCubit() : super(PrivacyState.initial()) { _settingsManager = GetIt.I(); _historyManager = GetIt.I(); _load(); } void _load() { emit( state.copyWith( playbackHistory: _settingsManager.playbackHistory, searchHistory: _settingsManager.searchHistory, ), ); } Future togglePlaybackHistory(bool value) async { _settingsManager.playbackHistory = value; emit(state.copyWith(playbackHistory: value)); } Future toggleSearchHistory(bool value) async { _settingsManager.searchHistory = value; emit(state.copyWith(searchHistory: value)); } Future clearPlaybackHistory() async { await _historyManager.songs.clear(); emit(state.copyWith(lastAction: PrivacyAction.playbackDeleted)); } Future clearSearchHistory() async { await _historyManager.searches.clear(); emit(state.copyWith(lastAction: PrivacyAction.searchDeleted)); } void consumeAction() { emit(state.copyWith(lastAction: null)); } } ================================================ FILE: lib/screens/settings/privacy/cubit/privacy_state.dart ================================================ part of 'privacy_cubit.dart'; enum PrivacyAction { playbackDeleted, searchDeleted, } class PrivacyState { final bool playbackHistory; final bool searchHistory; final PrivacyAction? lastAction; const PrivacyState({ required this.playbackHistory, required this.searchHistory, this.lastAction, }); factory PrivacyState.initial() => const PrivacyState( playbackHistory: true, searchHistory: true, ); PrivacyState copyWith({ bool? playbackHistory, bool? searchHistory, PrivacyAction? lastAction, }) { return PrivacyState( playbackHistory: playbackHistory ?? this.playbackHistory, searchHistory: searchHistory ?? this.searchHistory, lastAction: lastAction, ); } } ================================================ FILE: lib/screens/settings/privacy/privacy_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/core/widgets/expressive_switch_list_tile.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import '../../../../generated/l10n.dart'; import '../../../../utils/bottom_modals.dart'; import '../../../../services/bottom_message.dart'; import 'cubit/privacy_cubit.dart'; class PrivacyPage extends StatelessWidget { const PrivacyPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => PrivacyCubit(), child: BlocListener( listenWhen: (_, state) => state.lastAction != null, listener: (context, state) { final action = state.lastAction; if (action == null) return; if (action == PrivacyAction.playbackDeleted) { BottomMessage.showText( context, S.of(context).Playback_History_Deleted, ); } else if (action == PrivacyAction.searchDeleted) { BottomMessage.showText( context, S.of(context).Search_History_Deleted, ); } context.read().consumeAction(); }, child: const _PrivacyView(), ), ); } } class _PrivacyView extends StatelessWidget { const _PrivacyView(); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ExpressiveAppBar(title: "Privacy", hasLeading: true)]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: BlocBuilder( builder: (context, state) { final cubit = context.read(); return ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ ExpressiveListGroup( title: "Playback", children: [ ExpressiveSwitchListTile( title: Text(S.of(context).Enable_Playback_History), leading: const SettingsColorIcon( icon: Icons.play_arrow_rounded, ), value: state.playbackHistory, onChanged: cubit.togglePlaybackHistory, ), ExpressiveListTile( title: Text(S.of(context).Delete_Playback_History), leading: const SettingsColorIcon( icon: FluentIcons.history_dismiss_24_filled, ), onTap: () async { final confirm = await Modals.showConfirmBottomModal( context, message: S .of(context) .Delete_Playback_History_Confirm_Message, isDanger: true, ); if (confirm == true) { cubit.clearPlaybackHistory(); } }, ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: "Search", children: [ ExpressiveSwitchListTile( title: Text(S.of(context).Enable_Search_History), leading: const SettingsColorIcon( icon: Icons.saved_search_rounded, ), value: state.searchHistory, onChanged: cubit.toggleSearchHistory, ), ExpressiveListTile( title: Text(S.of(context).Delete_Search_History), leading: const SettingsColorIcon( icon: Icons.manage_search_rounded, ), onTap: () async { final confirm = await Modals.showConfirmBottomModal( context, message: S .of(context) .Delete_Search_History_Confirm_Message, isDanger: true, ); if (confirm == true) { cubit.clearSearchHistory(); } }, ), ], ), ], ); }, ), ), ), ), ); } } ================================================ FILE: lib/screens/settings/services/yt_music/cubit/ytmusic_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:yt_music/client.dart'; import 'package:yt_music/ytmusic.dart'; import '../../../../../../services/settings_manager.dart'; part 'ytmusic_state.dart'; class YTMusicCubit extends Cubit { late final SettingsManager _settingsManager; late final YTMusic _ytmusic; late final VoidCallback _settingsListener; List> get locations => _settingsManager.locations; List> get languages => _settingsManager.languages; List get audioQualities => _settingsManager.audioQualities; YTMusicCubit() : super( YTMusicState( location: GetIt.I().location, language: GetIt.I().language, autofetchSongs: GetIt.I().autofetchSongs, streamingQuality: GetIt.I().streamingQuality, downloadQuality: GetIt.I().downloadQuality, translateLyrics: GetIt.I().translateLyrics, personalisedContent: GetIt.I().personalisedContent, visitorId: GetIt.I().visitorId!, ), ) { _settingsManager = GetIt.I(); _ytmusic = GetIt.I(); _settingsListener = _emit; _settingsManager.addListener(_settingsListener); } void _emit() { if (isClosed) return; emit( state.copyWith( location: _settingsManager.location, language: _settingsManager.language, autofetchSongs: _settingsManager.autofetchSongs, streamingQuality: _settingsManager.streamingQuality, downloadQuality: _settingsManager.downloadQuality, translateLyrics: _settingsManager.translateLyrics, personalisedContent: _settingsManager.personalisedContent, visitorId: _settingsManager.visitorId, ), ); } void setLocation(Map location) { _settingsManager.location = location; } void setLanguage(Map language) { _settingsManager.language = language; } void setAutofetchSongs(bool value) { _settingsManager.autofetchSongs = value; } void setStreamingQuality(AudioQuality quality) { _settingsManager.streamingQuality = quality; } void setDownloadQuality(AudioQuality quality) { _settingsManager.downloadQuality = quality; } Future setTranslateLyrics(bool value) async { _settingsManager.translateLyrics = value; } Future setPersonalisedContent(bool value) async { _settingsManager.personalisedContent = value; final config = await YTClient.getConfig(); if (config != null) { _settingsManager.visitorId = config.visitorData; } } Future setVisitorId(String id) async { _settingsManager.visitorId = id; _ytmusic.updateConfig(visitorData: id); } Future resetVisitorId() async { final config = await YTClient.getConfig(); if (config != null) { _settingsManager.visitorId = config.visitorData; } } @override Future close() { _settingsManager.removeListener(_settingsListener); return super.close(); } } ================================================ FILE: lib/screens/settings/services/yt_music/cubit/ytmusic_state.dart ================================================ part of 'ytmusic_cubit.dart'; @immutable class YTMusicState { final Map location; final Map language; final bool autofetchSongs; final AudioQuality streamingQuality; final AudioQuality downloadQuality; final bool translateLyrics; final bool personalisedContent; final String visitorId; const YTMusicState({ required this.location, required this.language, required this.autofetchSongs, required this.streamingQuality, required this.downloadQuality, required this.translateLyrics, required this.personalisedContent, required this.visitorId, }); YTMusicState copyWith({ Map? location, Map? language, bool? autofetchSongs, dynamic streamingQuality, dynamic downloadQuality, bool? translateLyrics, bool? personalisedContent, String? visitorId, }) { return YTMusicState( location: location ?? this.location, language: language ?? this.language, autofetchSongs: autofetchSongs ?? this.autofetchSongs, streamingQuality: streamingQuality ?? this.streamingQuality, downloadQuality: downloadQuality ?? this.downloadQuality, translateLyrics: translateLyrics ?? this.translateLyrics, personalisedContent: personalisedContent ?? this.personalisedContent, visitorId: visitorId ?? this.visitorId, ); } } ================================================ FILE: lib/screens/settings/services/yt_music/yt_music_page.dart ================================================ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/extensions/string_extensions.dart'; import 'package:gyawun/core/utils/expressive_sheet.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/core/widgets/expressive_switch_list_tile.dart'; import 'package:gyawun/generated/l10n.dart'; import 'package:gyawun/screens/settings/widgets/color_icon.dart'; import 'package:gyawun/services/settings_manager.dart'; import 'package:gyawun/utils/bottom_modals.dart'; import 'cubit/ytmusic_cubit.dart'; class YTMusicPage extends StatelessWidget { const YTMusicPage({super.key}); Future _setLocation( BuildContext context, Map location, ) async { { final selected = await ExpressiveSheet.showSelection( context, title: "Choose Country", options: context .read() .locations .map( (l) => ExpressiveSheetOption( value: l, label: l['name']!.trim(), selected: l == location, ), ) .toList(), ); if (selected == null) return; if (context.mounted) { context.read().setLocation(selected); } } } Future _setLanguage( BuildContext context, Map language, ) async { { final selected = await ExpressiveSheet.showSelection( context, title: "Choose Language", options: context .read() .languages .map( (l) => ExpressiveSheetOption( value: l, label: l['name']!.trim(), selected: l == language, ), ) .toList(), ); if (selected == null) return; if (context.mounted) { context.read().setLanguage(selected); } } } Future _setStreamingQuality( BuildContext context, AudioQuality quality, ) async { { final selected = await ExpressiveSheet.showSelection( context, title: "Choose Streaming Quality", options: context .read() .audioQualities .map( (l) => ExpressiveSheetOption( value: l, label: l.name.capitalize(), selected: l == quality, ), ) .toList(), ); if (selected == null) return; if (context.mounted) { context.read().setStreamingQuality(selected); } } } Future _setDownloadingQuality( BuildContext context, AudioQuality quality, ) async { { final selected = await ExpressiveSheet.showSelection( context, title: "Choose Downloading Quality", options: context .read() .audioQualities .map( (l) => ExpressiveSheetOption( value: l, label: l.name.capitalize(), selected: l == quality, ), ) .toList(), ); if (selected == null) return; if (context.mounted) { context.read().setDownloadQuality(selected); } } } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => YTMusicCubit(), child: Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ ExpressiveAppBar(title: S.of(context).YTMusic, hasLeading: true), ]; }, body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: BlocBuilder( builder: (context, state) { final cubit = context.read(); return ListView( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), children: [ ExpressiveListGroup( title: "General", children: [ ExpressiveListTile( title: Text(S.of(context).Country), leading: SettingsColorIcon( icon: FluentIcons.location_24_filled, ), subtitle: state.location['name'] != null ? Text(state.location['name']!) : null, onTap: () => _setLocation(context, state.location), ), ExpressiveListTile( title: Text(S.of(context).Language), leading: SettingsColorIcon( icon: FluentIcons.local_language_24_filled, ), subtitle: state.language['name'] != null ? Text(state.language['name']!) : null, onTap: () => _setLanguage(context, state.language), ), ExpressiveSwitchListTile( title: Text(S.of(context).Translate_Lyrics), leading: SettingsColorIcon( icon: FluentIcons.translate_24_filled, ), value: state.translateLyrics, onChanged: cubit.setTranslateLyrics, ), ExpressiveSwitchListTile( title: Text(S.of(context).Autofetch_Songs), leading: SettingsColorIcon( icon: FluentIcons .arrow_rotate_counterclockwise_24_filled, ), value: state.autofetchSongs, onChanged: cubit.setAutofetchSongs, ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: 'Playback & download', children: [ ExpressiveListTile( title: Text(S.of(context).Streaming_Quality), leading: SettingsColorIcon( icon: Icons.spatial_audio_rounded, ), subtitle: Text( state.streamingQuality.name.capitalize(), ), onTap: () => _setStreamingQuality( context, state.streamingQuality, ), ), ExpressiveListTile( title: Text(S.of(context).DOwnload_Quality), leading: SettingsColorIcon( icon: FluentIcons.cloud_arrow_down_24_filled, ), subtitle: Text( state.downloadQuality.name.capitalize(), ), onTap: () => _setDownloadingQuality( context, state.downloadQuality, ), ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: 'Privacy', children: [ ExpressiveSwitchListTile( title: Text(S.of(context).Personalised_Content), leading: const SettingsColorIcon( icon: Icons.recommend_rounded, ), value: state.personalisedContent, onChanged: (v) async { Modals.showCenterLoadingModal(context); await cubit.setPersonalisedContent(v); if (context.mounted) context.pop(); }, ), ExpressiveListTile( title: Text(S.of(context).Enter_Visitor_Id), leading: const SettingsColorIcon( icon: FluentIcons.edit_24_filled, ), onTap: () async { final text = await Modals.showTextField( context, title: S.of(context).Enter_Visitor_Id, hintText: S.of(context).Visitor_Id, ); if (text != null) { cubit.setVisitorId(text); } }, ), ExpressiveListTile( title: Text(S.of(context).Reset_Visitor_Id), leading: const SettingsColorIcon( icon: FluentIcons.key_reset_24_filled, ), subtitle: Text(state.visitorId), trailing: state.visitorId.isEmpty ? null : IconButton( icon: const Icon(Icons.copy), onPressed: () { Clipboard.setData( ClipboardData(text: state.visitorId), ); }, ), onTap: () async { Modals.showCenterLoadingModal(context); await cubit.resetVisitorId(); if (context.mounted) context.pop(); }, ), ], ), ], ); }, ), ), ), ), ), ); } } ================================================ FILE: lib/screens/settings/settings_page.dart ================================================ import 'dart:io'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/core/widgets/expressive_app_bar.dart'; import 'package:gyawun/core/widgets/expressive_list_group.dart'; import 'package:gyawun/core/widgets/expressive_list_tile.dart'; import 'package:gyawun/services/update_service/update_service.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../generated/l10n.dart'; import '../../themes/text_styles.dart'; import '../../utils/adaptive_widgets/adaptive_widgets.dart'; import '../../utils/bottom_modals.dart'; import 'widgets/color_icon.dart'; import 'cubit/settings_system_cubit.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => SettingsSystemCubit()..load(), child: AdaptiveScaffold( body: BlocBuilder( builder: (context, state) { final bool? batteryDisabled = state is SettingsSystemLoaded ? state.isBatteryOptimizationDisabled : null; return Center( child: Container( constraints: const BoxConstraints(maxWidth: 1000), child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ExpressiveAppBar(title: S.of(context).Settings)]; }, body: ListView( padding: .symmetric(horizontal: 16, vertical: 8), children: [ if (Platform.isAndroid && batteryDisabled != true) _BatteryWarningTile(), if (Platform.isAndroid && batteryDisabled != true) SizedBox(height: 24), ExpressiveListGroup( title: "General", children: [ ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.color_background_24_filled, color: const Color.fromARGB(155, 183, 86, 118), ), title: Text(S.of(context).Appearence), subtitle: Text('Themes, layout, and visual style'), onTap: () => context.go('/settings/appearance'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.play_24_filled, color: const Color.fromARGB(155, 70, 92, 141), ), title: Text('Player'), subtitle: Text('Audio effects & playback'), onTap: () => context.go('/settings/player'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: "Services", children: [ ExpressiveListTile( leading: SettingsColorIcon( icon: Icons.play_circle_fill, color: const Color.fromARGB(155, 181, 54, 54), ), title: Text('Youtube Music'), subtitle: Text( 'Content region, language, audio quality', ), onTap: () => context.go('/settings/services/ytmusic'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: "Storage & Privacy", children: [ ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.storage_24_filled, color: const Color.fromARGB(155, 130, 146, 66), ), title: Text('Backup and storage'), subtitle: Text('App folder, backup, and restore'), onTap: () => context.go('/settings/backup_storage'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.shield_keyhole_24_filled, color: const Color.fromARGB(155, 46, 115, 76), ), title: Text('Privacy'), subtitle: Text('Playback & search history'), onTap: () => context.go('/settings/privacy'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ], ), SizedBox(height: 24), ExpressiveListGroup( title: "Updates & About", children: [ ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.info_24_filled, color: const Color.fromARGB(155, 115, 84, 46), ), title: Text(S.of(context).About), subtitle: Text('App info, support & links'), onTap: () => context.go('/settings/about'), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.arrow_circle_up_24_filled, color: const Color.fromARGB(155, 115, 46, 62), ), title: Text(S.of(context).Check_For_Update), subtitle: Text('Check GitHub for releases'), onTap: () => UpdateService.manualCheck(context), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ExpressiveListTile( leading: SettingsColorIcon( icon: FluentIcons.money_24_filled, color: const Color.fromARGB(155, 46, 100, 115), ), title: Text(S.of(context).Donate), subtitle: Text(S.of(context).Donate_Message), onTap: () => showPaymentsModal(context), trailing: const Icon( FluentIcons.chevron_right_24_filled, ), ), ], ), ], ), ), ), ); }, ), ), ); } } class _BatteryWarningTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( tileColor: Theme.of(context).colorScheme.errorContainer.withAlpha(200), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), leading: ColorIcon( icon: Icons.battery_alert, boxColor: Colors.red, iconColor: Colors.white.withAlpha(255), ), title: Text( S.of(context).Battery_Optimisation_title, style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), ), subtitle: Text( S.of(context).Battery_Optimisation_message, style: tinyTextStyle(context).copyWith( color: Theme.of( context, ).colorScheme.onErrorContainer.withValues(alpha: 0.7), ), ), onTap: () { context.read().requestBatteryOptimizationIgnore(); }, ); } } void showPaymentsModal(BuildContext context) { Widget title = AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text(S.of(context).Payment_Methods, style: mediumTextStyle(context)), leading: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 40, width: 40, child: ColorIcon( boxColor: Colors.accents[14], iconColor: Colors.white.withAlpha(255), icon: Icons.money, ), ), ], ), ); Widget child = Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.asset('assets/images/upi.jpg', height: 30, width: 30), ), title: Text( S.of(context).Pay_With_UPI, style: subtitleTextStyle(context), ), onTap: () async { Navigator.pop(context); await Clipboard.setData( const ClipboardData(text: 'sheikhhaziq76@okaxis'), ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Copied UPI ID to clipboard!")), ); } }, ), AdaptiveListTile( leading: Container( decoration: BoxDecoration( color: const Color.fromRGBO(19, 195, 255, 1), borderRadius: BorderRadius.circular(6), ), child: Image.asset('assets/images/kofi.png', height: 30, width: 30), ), title: Text( S.of(context).Support_Me_On_Kofi, style: subtitleTextStyle(context), ), onTap: () async { Navigator.pop(context); await launchUrl( Uri.parse('https://ko-fi.com/sheikhhaziq'), mode: LaunchMode.externalApplication, ); }, ), AdaptiveListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.asset('assets/images/coffee.png', height: 30, width: 30), ), title: Text( S.of(context).Buy_Me_A_Coffee, style: subtitleTextStyle(context), ), onTap: () async { Navigator.pop(context); await launchUrl( Uri.parse('https://buymeacoffee.com/sheikhhaziq'), mode: LaunchMode.externalApplication, ); }, ), ], ); showModalBottomSheet( useSafeArea: true, backgroundColor: Colors.transparent, context: context, builder: (context) => BottomModalLayout(title: title, child: child), ); } ================================================ FILE: lib/screens/settings/widgets/color_icon.dart ================================================ import 'package:flutter/material.dart'; class ColorIcon extends StatelessWidget { const ColorIcon({ required this.icon, required this.boxColor, required this.iconColor, this.size, this.borderRadius, this.padding, super.key, }); final IconData icon; final Color? boxColor; final Color? iconColor; final double? size; final double? borderRadius; final double? padding; @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(padding ?? 6), decoration: BoxDecoration( color: boxColor, borderRadius: BorderRadius.circular(borderRadius ?? 8), ), child: Icon(icon, color: iconColor, size: size ?? 20), ); } } class SettingsColorIcon extends StatelessWidget { const SettingsColorIcon({super.key, required this.icon, this.color}); final IconData icon; final Color? color; @override Widget build(BuildContext context) { return ColorIcon( icon: icon, boxColor: color ?? Theme.of(context).colorScheme.primaryContainer.withAlpha(150), iconColor: color != null ? Colors.white.withAlpha(255) : Theme.of(context).colorScheme.onPrimaryContainer, borderRadius: 24, padding: 12, size: 24, ); } } ================================================ FILE: lib/screens/settings/widgets/setting_item.dart ================================================ import 'package:flutter/material.dart'; class GroupTitle extends StatelessWidget { const GroupTitle({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 8), child: Text( title, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), ); } } class SettingEmptyTile extends StatelessWidget { final bool isFirst; final bool isLast; final Widget? leading; final Widget? child; const SettingEmptyTile( {super.key, this.isFirst = false, this.isLast = false, this.leading, this.child}); @override Widget build(BuildContext context) { return ListTile( shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(isFirst ? 20 : 4), topRight: Radius.circular(isFirst ? 20 : 4), bottomLeft: Radius.circular(isLast ? 20 : 4), bottomRight: Radius.circular(isLast ? 20 : 4), ), ), tileColor: Theme.of(context).colorScheme.surfaceContainer, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: leading == null ? null : Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primaryContainer.withAlpha(150), borderRadius: BorderRadius.circular(12), ), child: leading, ), title: child, ); } } class SettingTile extends StatelessWidget { final String title; final String? subtitle; final int subtitleLines; final Widget leading; final Widget? trailing; final void Function()? onTap; final bool isFirst; final bool isLast; const SettingTile({ super.key, required this.title, this.subtitle, this.subtitleLines = 1, required this.leading, this.trailing, this.onTap, this.isFirst = false, this.isLast = false, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 2), child: Material( color: Colors.transparent, shadowColor: Colors.transparent, elevation: 0, child: ListTile( onTap: onTap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(isFirst ? 20 : 4), topRight: Radius.circular(isFirst ? 20 : 4), bottomLeft: Radius.circular(isLast ? 20 : 4), bottomRight: Radius.circular(isLast ? 20 : 4), ), ), tileColor: Theme.of(context).colorScheme.surfaceContainer, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), title: Text( title, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 16, fontWeight: FontWeight.w500, ), ), leading: Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primaryContainer.withAlpha(150), borderRadius: BorderRadius.circular(12), ), child: leading, ), subtitle: subtitle != null ? Text( subtitle!, style: Theme.of(context).textTheme.labelLarge, maxLines: subtitleLines, overflow: TextOverflow.ellipsis, ) : null, trailing: trailing, ), ), ); } } class SettingSwitchTile extends StatelessWidget { final bool value; final String title; final Widget leading; final String? subtitle; final ValueChanged? onChanged; final bool isFirst; final bool isLast; const SettingSwitchTile({ super.key, required this.value, required this.title, required this.leading, this.onChanged, this.isFirst = false, this.isLast = false, this.subtitle, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 1), child: SwitchListTile( value: value, onChanged: onChanged, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(isFirst ? 20 : 4), topRight: Radius.circular(isFirst ? 20 : 4), bottomLeft: Radius.circular(isLast ? 20 : 4), bottomRight: Radius.circular(isLast ? 20 : 4), ), ), tileColor: Theme.of(context).colorScheme.surfaceContainer, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), title: Text( title, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 16, fontWeight: FontWeight.w500, ), ), subtitle: subtitle != null ? Text(subtitle!, style: Theme.of(context).textTheme.labelLarge) : null, secondary: Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primaryContainer.withAlpha(150), borderRadius: BorderRadius.circular(12), ), child: leading, ), ), ); } } ================================================ FILE: lib/screens/shell/app_shell.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/services/update_service/update_service.dart'; import 'package:navigation_rail_m3e/navigation_rail_m3e.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import '../../generated/l10n.dart'; import 'widgets/bottom_player.dart'; class AppShell extends StatefulWidget { const AppShell({Key? key, required this.navigationShell}) : super(key: key ?? const ValueKey('AppShell')); final StatefulNavigationShell navigationShell; @override State createState() => _AppShellState(); } class _AppShellState extends State { late StreamSubscription _intentSub; @override void initState() { super.initState(); if (Platform.isAndroid) { _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen(( value, ) { if (value.isNotEmpty) _handleIntent(value.first); }); ReceiveSharingIntent.instance.getInitialMedia().then((value) { if (value.isNotEmpty) _handleIntent(value.first); ReceiveSharingIntent.instance.reset(); }); } UpdateService.autoCheck(context); } void _handleIntent(SharedMediaFile value) { if (value.mimeType == 'text/plain' && value.path.contains('music.youtube.com')) { Uri? uri = Uri.tryParse(value.path); if (uri != null) { if (uri.pathSegments.first == 'watch' && uri.queryParameters['v'] != null) { context.push('/player', extra: uri.queryParameters['v']); } else if (uri.pathSegments.first == 'playlist' && uri.queryParameters['list'] != null) { String id = uri.queryParameters['list']!; context.push( '/browse', extra: { 'endpoint': {'browseId': id.startsWith('VL') ? id : 'VL$id'}, }, ); } } } } @override void dispose() { _intentSub.cancel(); super.dispose(); } void _goBranch(int index) { widget.navigationShell.goBranch( index, initialLocation: index == widget.navigationShell.currentIndex, ); } @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; return Scaffold( body: Column( children: [ Expanded( child: Row( children: [ if (screenWidth >= 450) NavigationRailM3E( type: screenWidth > 1000 ? NavigationRailM3EType.expanded : NavigationRailM3EType.collapsed, onDestinationSelected: _goBranch, sections: [ NavigationRailM3ESection( destinations: [ NavigationRailM3EDestination( selectedIcon: const Icon( FluentIcons.home_24_filled, ), icon: const Icon(FluentIcons.home_24_regular), label: S.of(context).Home, ), NavigationRailM3EDestination( selectedIcon: const Icon( FluentIcons.library_24_filled, ), icon: const Icon(FluentIcons.library_24_regular), label: 'Library', ), NavigationRailM3EDestination( selectedIcon: const Icon( FluentIcons.settings_24_filled, ), icon: const Icon(FluentIcons.settings_24_regular), label: S.of(context).Settings, ), ], ), ], selectedIndex: widget.navigationShell.currentIndex, ), Expanded(child: widget.navigationShell), ], ), ), const BottomPlayer(), ], ), bottomNavigationBar: screenWidth < 450 ? NavigationBar( selectedIndex: widget.navigationShell.currentIndex, labelBehavior: .onlyShowSelected, destinations: [ NavigationDestination( selectedIcon: const Icon(FluentIcons.home_24_filled), icon: const Icon(FluentIcons.home_24_regular), label: S.of(context).Home, ), NavigationDestination( selectedIcon: const Icon(FluentIcons.library_24_filled), icon: const Icon(FluentIcons.library_24_regular), label: 'Library', ), NavigationDestination( selectedIcon: const Icon(FluentIcons.settings_24_filled), icon: const Icon(FluentIcons.settings_24_regular), label: S.of(context).Settings, ), ], backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerLow, // colo: Theme.of(context).colorScheme.onSurface, onDestinationSelected: _goBranch, ) : null, ); } } ================================================ FILE: lib/screens/shell/widgets/bottom_player.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/services/media_player.dart'; import 'package:gyawun/utils/adaptive_widgets/buttons.dart'; import 'package:gyawun/utils/adaptive_widgets/listtile.dart'; import 'package:gyawun/utils/song_thumbnail.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:provider/provider.dart'; class BottomPlayer extends StatelessWidget { const BottomPlayer({super.key}); @override Widget build(BuildContext context) { final mediaPlayer = GetIt.I(); return StreamBuilder( stream: mediaPlayer.currentTrackStream, builder: (context, snapshot) { final data = snapshot.data; final currentSong = data?.currentItem; if (currentSong == null) { return const SizedBox(); // or loading indicator } return Container( color: Theme.of(context).colorScheme.surfaceContainerLow, child: GestureDetector( onTap: () { context.push('/player'); }, child: SafeArea( top: false, child: Dismissible( key: Key('bottomplayer${currentSong.id}'), direction: DismissDirection.down, confirmDismiss: (direction) async { await GetIt.I().stop(); return true; }, child: Dismissible( key: Key(currentSong.id), confirmDismiss: (direction) async { if (direction == DismissDirection.startToEnd) { await GetIt.I().player.seekToPrevious(); } else { await GetIt.I().player.seekToNext(); } return Future.value(false); }, child: AdaptiveListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: SongThumbnail( song: currentSong.extras!, dp: MediaQuery.of(context).devicePixelRatio, height: 50, width: 50, fit: BoxFit.fill, ), ), title: Text( currentSong.title, // style: textStyle(context, bold: true), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: (currentSong.artist != null || currentSong.extras!['subtitle'] != null) ? Text( currentSong.artist ?? currentSong.extras!['subtitle'], maxLines: 1, overflow: TextOverflow.ellipsis, ) : null, trailing: Row( children: [ ValueListenableBuilder( valueListenable: GetIt.I().buttonState, builder: (context, buttonState, child) { return (buttonState == ButtonState.loading) ? const ExpressiveLoadingIndicator() : AdaptiveIconButton( onPressed: () { GetIt.I().player.playing ? GetIt.I().player .pause() : GetIt.I().player .play(); }, icon: Icon( buttonState == ButtonState.playing ? Icons.pause : Icons.play_arrow, size: 30, ), ); }, ), StreamBuilder( stream: context .watch() .player .sequenceStateStream, builder: (context, snapshot) { if (context.watch().player.hasNext) { return AdaptiveIconButton( onPressed: () { GetIt.I().player.seekToNext(); }, icon: Icon(Icons.skip_next, size: 25), ); } return const SizedBox.shrink(); }, ), ], ), ), ), ), ), ), ); }, ); } } ================================================ FILE: lib/services/bottom_message.dart ================================================ import 'package:fl_toast/fl_toast.dart'; import 'package:flutter/material.dart'; import '../themes/text_styles.dart'; import '../utils/adaptive_widgets/theme.dart'; class BottomMessage { static void showText(BuildContext context, String text, {Duration duration = const Duration(milliseconds: 1500)}) { showPlatformToast( child: Text( text, style: smallTextStyle(context, bold: false, opacity: 0.8) .copyWith(color: AdaptiveTheme.of(context).inactiveBackgroundColor), ), context: context, duration: duration, ); } } ================================================ FILE: lib/services/custom_audio_stream.dart ================================================ // import 'dart:async'; // import 'package:gyawun/services/stream_client.dart'; // import 'package:just_audio/just_audio.dart'; // import 'package:youtube_explode_dart/youtube_explode_dart.dart'; // mixin HlsStreamInfo on StreamInfo { // /// The tag of the audio stream related to this stream. // int? get audioItag => null; // } // class CustomAudioStream extends StreamAudioSource { // late YoutubeExplode ytExplode; // StreamInfo? streamInfo; // String videoId; // String quality; // CustomAudioStream(this.videoId, this.quality, {super.tag}) { // ytExplode = YoutubeExplode(); // } // Future _loadStreamInfo() async { // StreamManifest manifest = // await ytExplode.videos.streamsClient.getManifest(videoId); // List streamInfos = manifest.audioOnly // .sortByBitrate() // .reversed // .where((stream) => stream.container == StreamContainer.mp4) // .toList(); // int qualityIndex = quality == 'low' ? 0 : streamInfos.length - 1; // streamInfo = streamInfos[qualityIndex]; // if (streamInfo!.fragments.isNotEmpty) { // print("Using DASH Fragmented Stream"); // } else if (streamInfo is HlsStreamInfo) { // print("Using HLS Stream"); // } else { // print("Using Normal Stream"); // } // print(streamInfo!.url.toString()); // } // @override // Future request([int? start, int? end]) async { // if (streamInfo == null) { // await _loadStreamInfo(); // } // start ??= 0; // int size = 10379935; // end = start + size.clamp(0, streamInfo!.size.totalBytes - start); // // if (!streamInfo!.isThrottled) { // // } // // end ??= streamInfo!.size.totalBytes; // if (end > streamInfo!.size.totalBytes) { // end = streamInfo!.size.totalBytes; // } // print('$start-$end'); // final response = ytExplode.videos.streams.get(streamInfo!, start, end); // return StreamAudioResponse( // sourceLength: streamInfo!.size.totalBytes, // contentLength: null, // offset: null, // stream: response, // contentType: streamInfo!.codec.type, // ); // } // } ================================================ FILE: lib/services/download_manager.dart ================================================ import 'dart:collection'; import 'package:collection/collection.dart'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:http/http.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:yt_music/ytmusic.dart'; import 'file_storage.dart'; import 'settings_manager.dart'; import 'favourites_manager.dart'; import 'stream_client.dart'; YoutubeExplode ytExplode = YoutubeExplode(); class DownloadCanceledException implements Exception {} class DownloadManager { final Box _box; Client client = Client(); ValueNotifier> downloadsNotifier = ValueNotifier([]); ValueNotifier> playlistsNotifier = ValueNotifier({}); final Map> _activeDownloadProgress = {}; static const String songsPlaylistId = 'SNGS'; final int maxConcurrentDownloads = 3; // Limit concurrent downloads final Queue _activeDownloads = Queue(); // Currently active downloads final Queue _downloadQueue = Queue(); // Queue for pending downloads Map get downloads => _box.toMap(); Listenable songListenable(String songId) { return _box.listenable(keys: [songId]); } Map? getDownload(String songId) { return _box.get(songId); } Map getCleanSong(Map song) { final Map clean = Map.from(song); clean.remove('status'); clean.remove('path'); clean.remove('playlists'); return clean; } List? getDownloadedSongs(String? playlistId) { List? allSongs; if (playlistId == null) { allSongs = downloadsNotifier.value; } else { allSongs = playlistsNotifier.value[playlistId]?["songs"]; } return allSongs ?.where((s) => getDownload(s['videoId'])?['status'] == 'DOWNLOADED') .map((s) => getCleanSong(s)) .toList(); } DownloadManager._(this._box) { _cleanAndMigrateData(); _refreshData(); _box.listenable().addListener(() { _refreshData(); }); } Future reInit() async { await _cleanAndMigrateData(); await _refreshData(); } static Future create() async { final boxName = 'DOWNLOADS'; await Hive.openBox(boxName); final instance = DownloadManager._(Hive.box(boxName)); return instance; } Future _cleanAndMigrateData() async { final activeIds = _activeDownloads.toSet(); final queuedIds = _downloadQueue.map((e) => e['videoId']).toSet(); final mapToUpdate = {}; for (final key in _box.keys) { final Map song = Map.from(_box.get(key) as Map); final id = song['videoId'] ?? key.toString(); String status = song['status'] ?? ''; // 1) CHECK INTERRUPTED DOWNLOADS final isInvalidDownloading = status == 'DOWNLOADING' && !activeIds.contains(id); final isInvalidQueued = status == 'QUEUED' && !queuedIds.contains(id); if (isInvalidDownloading || isInvalidQueued) { debugPrint("Cleaning up interrupted download: ${song['title']}"); song['status'] = 'DELETED'; mapToUpdate[key.toString()] = song; } // 2) MIGRATE OLD DOWNLOADS TO SONGS PLAYLIST if (song["playlists"] == null || song["playlists"] is! Map) { song["playlists"] = { songsPlaylistId: { "id": songsPlaylistId, "title": "Songs", "timestamp": song["downloadedAt"] ?? song["timestamp"] ?? DateTime.now().millisecondsSinceEpoch, }, }; mapToUpdate[key.toString()] = song; } else if (song["playlists"] is Map && (song["playlists"] as Map).keys.contains("songs")) { // 2) RENAME OLD SONGS PLAYLIST final pl = song["playlists"].remove("songs"); song["playlists"][songsPlaylistId] = pl; mapToUpdate[key.toString()] = song; } } // 1) UPDATE DOWNLOADS if (mapToUpdate.isNotEmpty) { await _box.putAll(mapToUpdate); } } Future _refreshData() async { // 1) LOAD DOWNLOADS FROM HIVE downloadsNotifier.value = _box.values.toList().cast(); // 2) BUILD PLAYLIST MAP final Map> playlists = {}; for (final song in downloadsNotifier.value) { final Map songPlaylists = Map.from(song["playlists"] ?? {}); for (final entry in songPlaylists.entries) { final String id = entry.key; final value = entry.value; if (value is! Map) continue; final String title = value["title"] ?? "Unknown"; playlists .putIfAbsent( id, () => { "id": id, "title": title, "type": id == songsPlaylistId || id == FavouritesManager.playlistId ? "PLAYLIST" : "ALBUM", "songs": >[], }, )["songs"] .add(Map.from(song)); // ALBUM → PLAYLIST upgrade logic (unchanged, but safe) if (playlists[id]!["type"] == "ALBUM" && playlists[id]!["title"] != song["album"]?["name"]) { playlists[id]!["type"] = "PLAYLIST"; } } } // 3) SORT SONGS INSIDE PLAYLISTS for (final playlist in playlists.values) { final String playlistId = playlist["id"]; (playlist["songs"] as List).sort((a, b) { final aTs = a["playlists"]?[playlistId]?["timestamp"] ?? 0; final bTs = b["playlists"]?[playlistId]?["timestamp"] ?? 0; return aTs.compareTo(bTs); }); } // 4) UPDATE STATE IF CHANGED if (!const DeepCollectionEquality().equals( playlistsNotifier.value, playlists, )) { playlistsNotifier.value = playlists; } } List getDownloadQueue() { return _downloadQueue.toList(); } ValueNotifier? getProgressNotifier(String videoId) { return _activeDownloadProgress[videoId]; } void _startTrackingProgress(String videoId) { _activeDownloadProgress[videoId]?.dispose(); _activeDownloadProgress[videoId] = ValueNotifier(0.0); } void _updateTrackingProgress(String videoId, double value) { _activeDownloadProgress[videoId]?.value = value; } void _stopTrackingProgress(String videoId) { if (_activeDownloadProgress.containsKey(videoId)) { _activeDownloadProgress[videoId]!.dispose(); _activeDownloadProgress.remove(videoId); } } Future restoreDownloads({List? songs}) async { final songsToRestore = songs ?? downloadsNotifier.value; for (var song in songsToRestore) { final storedSong = _box.get(song['videoId']); if (storedSong != null) { final status = storedSong['status']; final path = storedSong['path']; final isFileMissing = status == 'DOWNLOADED' && (path == null || !(await File(path).exists())); final isDeleted = status == 'DELETED'; if (isDeleted || isFileMissing) { downloadSong(storedSong); } } } } Future setDownloads(Map downloads) async { await Future.forEach(downloads.entries, (entry) async { _box.put(entry.key, entry.value); }); } Future downloadSong(Map songToDownaload) async { // Added "songs" playlist if needed final Map song = { ...songToDownaload, 'playlists': songToDownaload['playlists'] ?? { songsPlaylistId: { 'title': 'Songs', 'timestamp': DateTime.now().millisecondsSinceEpoch, }, }, }; // Check downloaded songs final Map? downloadSong = _box.get(song['videoId']); if (downloadSong != null) { final queueSong = _downloadQueue.firstWhereOrNull( (item) => item['videoId'] == song['videoId'], ); if (_activeDownloads.contains(song['videoId']) || queueSong != null) { // Already downloading, just update metadata await _updateSongMetadata(song['videoId'], {...song}); _downloadNext(); return; } else { final String? path = downloadSong['path']; if (path != null) { final file = File(path); final exists = await file.exists(); if (exists) { // Already downloaded, just update metadata await _updateSongMetadata(song['videoId'], { ...song, 'status': 'DOWNLOADED', }); _downloadNext(); return; } } } } // Execute download process if (!await _downloadStart(song)) return; await _downloadSong(song); _downloadEnd(song); _downloadNext(); } Future _downloadSong(Map song) async { try { await _updateSongMetadata(song['videoId'], { ...song, 'status': 'DOWNLOADING', }); _startTrackingProgress(song['videoId']); if (!(await GetIt.I().requestPermissions())) { throw Exception('Storage permissions not granted.'); } AudioOnlyStreamInfo audioSource = await _getSongInfo( song['videoId'], quality: GetIt.I().downloadQuality.name.toLowerCase(), ); _ensureActive(song); int total = audioSource.size.totalBytes; BytesBuilder received = BytesBuilder(); Stream> stream = AudioStreamClient().getAudioStream( audioSource, start: 0, end: total, ); _ensureActive(song); await for (var data in stream) { _ensureActive(song); received.add(data); _updateTrackingProgress(song['videoId'], received.length / total); } File? file = await GetIt.I().saveMusic( received.takeBytes(), song, ); _ensureActive(song); if (file != null) { await _updateSongMetadata(song['videoId'], { 'status': 'DOWNLOADED', 'path': file.path, }); } else { throw Exception("File saving failed"); } } on DownloadCanceledException { debugPrint("Download cancelled by user: ${song['videoId']}"); } catch (e) { debugPrint("Error in _downloadSong: $e"); await _updateSongMetadata(song['videoId'], {'status': 'DELETED'}); } finally { _stopTrackingProgress(song['videoId']); } } Future _updateSongMetadata(String key, Map newMetadata) async { Map? song = _box.get(key); if (song != null) { if (newMetadata.containsKey('playlists')) { Map mergedPlaylists = {}; if (song['playlists'] != null) { (song['playlists'] as Map).forEach((k, v) { mergedPlaylists[k] = Map.from(v); }); } (newMetadata['playlists'] as Map).forEach((k, v) { mergedPlaylists[k] = Map.from(v); }); song['playlists'] = mergedPlaylists; newMetadata.remove('playlists'); } await _box.put(key, {...song, ...newMetadata}); } else { await _box.put(key, newMetadata); } } Future _downloadStart(Map song) async { if (_activeDownloads.length >= maxConcurrentDownloads) { _downloadQueue.add(song); await _updateSongMetadata(song['videoId'], {...song, 'status': 'QUEUED'}); return false; } _activeDownloads.add(song['videoId']); return true; } void _ensureActive(Map song) { if (!_activeDownloads.contains(song['videoId'])) { throw DownloadCanceledException(); } } void _downloadEnd(Map song) { if (_activeDownloads.isNotEmpty) { _activeDownloads.remove(song['videoId']); } } void _downloadNext() { if (_downloadQueue.isNotEmpty && _activeDownloads.length < maxConcurrentDownloads) { downloadSong(_downloadQueue.removeFirst()); } } Future _deleteSongInstance(Map song) async { // Remove Song from Queue if (song['status'] == "QUEUED") { _downloadQueue.removeWhere((item) => item['videoId'] == song['videoId']); } // Stop in-progress download else if (song['status'] == "DOWNLOADING") { _downloadEnd(song); } // Delete Song from box await _box.delete(song['videoId']); // Remove file if exists if (song['path'] != null && await File(song['path']).exists()) { await File(song['path']).delete(); } } Future deleteSong({ required String key, String playlistId = songsPlaylistId, }) async { Map? song = _box.get(key); final Map playlists = song?['playlists']; if (song != null && (playlists.keys.contains(playlistId))) { song['playlists'].remove(playlistId); if (song['playlists'].isEmpty) { await _deleteSongInstance(song); } else { if (song['status'] == "QUEUED") { for (var item in _downloadQueue) { if (item['videoId'] == song['videoId']) { item['playlists'] = playlists; break; } } } await _box.put(key, song); } } return 'Song deleted successfully.'; } Future deleteAllSongs() async { List songs = _box.values.toList().cast(); for (Map song in songs) { await _deleteSongInstance(song); } } Future updateStatus(String key, String status) async { Map? song = _box.get(key); if (song != null) { song['status'] = status; await _box.put(key, song); } } Future _getSongs({ String? playlistId, int maxContinuations = 50, // playlist and albums with up to 24 * 51 songs }) async { final songs = []; if (playlistId != null) { Map result = await GetIt.I().getNextSongList( playlistId: playlistId, ); songs.addAll(result['contents']); String? continuation = result['continuation']; while (maxContinuations > 0 && continuation != null) { result = await GetIt.I().getNextSongList( continuation: continuation, ); songs.addAll(result['contents']); continuation = result['continuation']; maxContinuations -= 1; } } return songs; } Future downloadPlaylist(Map playlist) async { List songs = playlist['isPredefined'] == false ? playlist['songs'] : await _getSongs( playlistId: playlist['playlistId'], maxContinuations: playlist['type'] == 'ARTIST' ? 0 : 50, ); int timestamp = DateTime.now().millisecondsSinceEpoch; for (Map song in songs) { downloadSong({ ...song, 'playlists': { playlist['playlistId']: { 'title': playlist['title'], 'timestamp': timestamp++, }, }, }); // Queue each song download } } Future _getSongInfo( String videoId, { String quality = 'high', }) async { try { StreamManifest manifest = await ytExplode.videos.streamsClient .getManifest( videoId, requireWatchPage: true, ytClients: [YoutubeApiClient.androidVr], ); List streamInfos = manifest.audioOnly .where((a) => a.container == StreamContainer.mp4) .sortByBitrate() .reversed .toList(); return quality == 'low' ? streamInfos.first : streamInfos.last; } catch (e) { rethrow; } } } ================================================ FILE: lib/services/favourites_manager.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; class FavouritesManager { final Box _box; static const playlistId = 'FVRTS'; Listenable get listenable => _box.listenable(); Map get songs => _box.toMap(); int get songsCount => _box.length; Map get playlist => { 'title': "Favourites", 'playlistId': playlistId, 'type': 'PLAYLIST', 'isPredefined': false, 'songs': getOrderedSongs(), }; FavouritesManager._(this._box); static Future create() async { final boxName = 'FAVOURITES'; await Hive.openBox(boxName); final instance = FavouritesManager._(Hive.box(boxName)); return instance; } List getOrderedSongs() { final list = _box.values.toList(); list.sort((a, b) => (a['createdAt'] ?? 0).compareTo(b['createdAt'] ?? 0)); return list; } bool isFavourite(Map? song) { if (song == null || song['videoId'] == null) return false; return _box.containsKey(song['videoId']); } Future add(Map? song) async { if (song != null) { await _box.put(song['videoId'], { ...song, 'createdAt': song['createdAt'] ?? DateTime.now().millisecondsSinceEpoch, }); } } Future remove(Map? song) async { if (song != null) { await _box.delete(song['videoId']); } } Future addOrRemove(Map? song) async { if (song != null) { if (isFavourite(song)) { await remove(song); } else { await add(song); } } } Future setFavourites(Map favourites) async { await Future.forEach(favourites.values, (song) async { await add(song); }); } } ================================================ FILE: lib/services/file_storage.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:audiotags/audiotags.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path/path.dart' as path; import 'package:share_plus/share_plus.dart'; import '../utils/enhanced_image.dart'; import 'history_manager.dart'; import 'library.dart'; import 'settings_manager.dart'; import 'favourites_manager.dart'; class FileStorage { static final String defaultPath = '/storage/emulated/0/Download/'; late StoragePaths _storagePaths; FileStorage._(); static Future create() async { final instance = FileStorage._(); await instance.setupPaths(); return instance; } Future setupPaths() async { Directory directory = await _getAppDirectory(); _storagePaths = StoragePaths( basePath: directory.path, backupPath: path.join(directory.path, 'Back Up'), musicPath: path.join(directory.path, 'Music'), ); await _getDirectory(_storagePaths.backupPath); await _getDirectory(_storagePaths.musicPath); } Future saveBackUp(Map data) async { String timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); String fileName = '${timestamp}_backup'; if (!(await requestPermissions())) return ""; Directory directory = await _getDirectory(_storagePaths.backupPath); String filePath = path.join(directory.path, '$fileName.json'); File file = File(filePath); try { if (await file.exists()) { await file.delete(); } File result = await file.writeAsString(jsonEncode(data), flush: true); return result.path; } catch (e) { rethrow; } } Future shareBackUp(Map data) async { String timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); String fileName = '${timestamp}_backup.json'; try { final params = ShareParams( files: [ XFile.fromData(utf8.encode(jsonEncode(data)), mimeType: 'text/plain'), ], fileNameOverrides: [fileName], ); ShareResult result = await SharePlus.instance.share(params); return result.raw; } catch (e) { rethrow; } } Future saveMusic(List data, Map song, {extension = 'm4a'}) async { String fileName = song['title']; final RegExp avoid = RegExp(r'[\.\\\*\:\(\)\"\?#/;\|]'); fileName = fileName.replaceAll(avoid, '').replaceAll("'", ''); //fileName = Uri.decodeFull(fileName); if (!(await requestPermissions())) return null; Directory directory = await _getDirectory(_storagePaths.musicPath); File file = File(path.join(directory.path, '$fileName.$extension')); int number = 1; while (file.existsSync()) { file = File((path.join(directory.path, '$fileName($number).$extension'))); number++; } try { if (await file.exists()) { await file.delete(); } await file.writeAsBytes(data, flush: true); try { Response res = await get( Uri.parse(getEnhancedImage(song['thumbnails'].first['url'])), ); Tag tag = Tag( title: song['title'], trackArtist: song['artists'] ?.map((artist) => artist['name']) .join(','), album: song['album']?['name'], pictures: [ Picture(bytes: res.bodyBytes, pictureType: PictureType.coverFront), ], ); await AudioTags.write(file.path, tag); } catch (e) { await file.writeAsBytes(data, flush: true); } return file; } catch (e) { return null; } } Future loadBackup() async { if (!(await requestPermissions())) return false; FilePickerResult? picker = await FilePicker.platform.pickFiles( initialDirectory: _storagePaths.backupPath, allowMultiple: false, withData: true, type: FileType.custom, allowedExtensions: ['json'], ); if (picker == null) return false; final file = picker.files[0].xFile; String data = await file.readAsString(); Map backup = jsonDecode(data); if (backup['name'] != 'Gyawun' && backup['type'] != 'backup') { return false; } Map? settings = backup['data']?['settings']; Map? favourites = backup['data']?['favourites']; Map? playlists = backup['data']?['playlists']; Map? history = backup['data']?['song_history']; Map? downloads = backup['data']?['downloads']; if (settings != null) { await GetIt.I().setSettings(settings); // await GetIt.I().refreshHeaders(); } if (favourites != null) { await GetIt.I().setFavourites(favourites); } if (playlists != null) { await GetIt.I().setPlaylists(playlists); } if (history != null) { await GetIt.I().songs.setHistory(history); } if (downloads != null) { await GetIt.I().setDownloads(downloads); } return true; } Future _getAppDirectory() async { if (Platform.isAndroid) { return Directory( path.join(GetIt.I().appFolder, 'Gyawun'), ); } if (Platform.isWindows) { return Directory( path.join((await getDownloadsDirectory())!.path, 'Gyawun'), ); } return await getApplicationDocumentsDirectory(); } Future _getDirectory(String pathString) async { Directory dir = Directory(pathString); if (!(await dir.exists())) { await dir.create(recursive: true); } return dir; } Future requestPermissions() async { if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { // Desktop platforms do not need permission return true; } if (Platform.isIOS) { // iOS app-specific storage doesn't require permission return true; } bool isGranted = false; if (Platform.isAndroid) { int sdkInt = (await _getAndroidSdkInt()) ?? 30; if (sdkInt >= 30) { // Android 11+ requires MANAGE_EXTERNAL_STORAGE for arbitrary access isGranted = await Permission.manageExternalStorage.isGranted; if (!isGranted) { await Permission.manageExternalStorage.request(); isGranted = await Permission.manageExternalStorage.isGranted; } } else { // Android < 11 uses standard storage permission isGranted = await Permission.storage.isGranted; if (!isGranted) { await Permission.storage.request(); isGranted = await Permission.storage.isGranted; } } if (!isGranted) { // Open settings if permission denied await openAppSettings(); } } return isGranted; } // Helper function to get Android SDK version Future _getAndroidSdkInt() async { try { final String? sdkString = await MethodChannel( 'flutter/platform', ).invokeMethod('SystemNavigator.getPlatformVersion'); if (sdkString != null) { final match = RegExp(r'Android (\d+)').firstMatch(sdkString); if (match != null) return int.tryParse(match.group(1)!); } } catch (_) {} return null; } } class StoragePaths { String basePath; String backupPath; String musicPath; StoragePaths({ required this.basePath, required this.backupPath, required this.musicPath, }); } ================================================ FILE: lib/services/history_manager.dart ================================================ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:gyawun/services/settings_manager.dart'; class HistoryManager { final SearchHistory searches; final SongHistory songs; HistoryManager._(this.searches, this.songs); static Future create() async { final searches = await SearchHistory.create(); final songs = await SongHistory.create(); final instance = HistoryManager._(searches, songs); return instance; } } class SearchHistory { final Box _box; SearchHistory._(this._box); static Future create() async { final boxName = 'SEARCH_HISTORY'; await Hive.openBox(boxName); final instance = SearchHistory._(Hive.box(boxName)); return instance; } Map get all => _box.toMap(); List> getList({String? filter}) { final searchHistory = filter == null ? _box.values : _box.values.where( (el) => el.toLowerCase().contains(filter.toLowerCase()), ); return searchHistory .toList() .map((el) => {'type': 'TEXT', 'query': el, 'isHistory': true}) .toList(); } Future add(String value) async { if (GetIt.I().searchHistory) { await _box.delete(value.toLowerCase()); await _box.put(value.toLowerCase(), value); } } Future clear() async { await _box.clear(); } } class SongHistory { final Box _box; SongHistory._(this._box); static Future create() async { final boxName = 'SONG_HISTORY'; await Hive.openBox(boxName); final instance = SongHistory._(Hive.box(boxName)); return instance; } Listenable get listenable => _box.listenable(); int get count => _box.length; Map get all => _box.toMap(); List getList() { final list = _box.values.toList(); list.sort((a, b) => (b['updatedAt'] ?? 0).compareTo(a['updatedAt'] ?? 0)); return list; } Future add(Map song) async { if (GetIt.I().playbackHistory) { Map? oldState = _box.get(song['videoId']); int timestamp = DateTime.now().millisecondsSinceEpoch; if (oldState != null) { await _box.put(song['videoId'], { ...oldState, 'plays': oldState['plays'] + 1, 'updatedAt': timestamp, }); } else { await _box.put(song['videoId'], { ...song, 'plays': 1, 'createdAt': timestamp, 'updatedAt': timestamp, }); } } } Future remove(Map song) async { await _box.delete(song['videoId']); } Future clear() async { await _box.clear(); } Future setHistory(Map history) async { await Future.forEach(history.entries, (entry) async { _box.put(entry.key, entry.value); }); } } ================================================ FILE: lib/services/library.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:get_it/get_it.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:yt_music/ytmusic.dart'; class LibraryService extends ChangeNotifier { final Box _box; late Map _playlists; LibraryService._(this._box) { _init(); } static Future create() async { final boxName = 'LIBRARY'; await Hive.openBox(boxName); final instance = LibraryService._(Hive.box(boxName)); await instance._migrateLibIcons(); return instance; } void _init() { _playlists = _box.toMap(); _box.listenable().addListener(() { _playlists = _box.toMap(); notifyListeners(); }); } Map get playlists => _playlists; Map get userPlaylists => Map.fromEntries( _playlists.entries.where((item) => item.value['isPredefined'] == false), ); Map? getPlaylist(String playlistId) => _box.get(playlistId); Future reInit() async { await _migrateLibIcons(); _playlists = _box.toMap(); notifyListeners(); } Future _migrateLibIcons() async { for (final entry in userPlaylists.entries) { final id = entry.key; final item = entry.value; if (!item.keys.contains("iconId")) { await _box.put(id, {...item, 'iconId': 'musicNoteList'}); } } } Future createPlaylist( String title, String iconId, { Map? item, }) async { if (title.trim().isEmpty) { return "Playlist name can't be empty"; } await _box.put("CSTMPL${DateTime.now().millisecondsSinceEpoch}", { 'title': title, 'iconId': iconId, 'isPredefined': false, 'songs': item != null ? [item] : [], 'createdAt': DateTime.now().millisecondsSinceEpoch, }); notifyListeners(); if (item != null) { return '${item['title']} added to $title'; } else { return 'Playlist $title created'; } } Future importPlaylist(String playlistUrl) async { try { Uri uri = Uri.parse(playlistUrl); String? playlistId = uri.queryParameters['list']; if (!uri.host.contains('youtube.com') || playlistId == null) { return 'Invalid Url'; } String browseId = playlistId.startsWith("VL") ? playlistId : "VL$playlistId"; Map playlist = await GetIt.I().importPlaylist( browseId, ); String id = playlistId; if (_playlists[id] != null) { await _box.delete(id); notifyListeners(); return 'Playlist is already added'; } else { await _box.put(id, { ...playlist, 'iconId': 'musicNoteList', 'isPredefined': true, 'createdAt': DateTime.now().millisecondsSinceEpoch, }); notifyListeners(); return 'Added to Library'; } } catch (e) { return "Error importing playlist"; } } Future addToOrRemoveFromLibrary(Map item) async { String id = item['playlistId']; if (_playlists[id] != null) { await _box.delete(id); notifyListeners(); return 'Removed from Library'; } else { await _box.put(id, { ...item, 'isPredefined': true, 'createdAt': DateTime.now().millisecondsSinceEpoch, }); notifyListeners(); return 'Added to Library'; } } Future editPlaylist({ String? title, required String playlistId, required String iconId, }) async { if (_playlists[playlistId] == null) { return 'Playlist does not exist'; } if (title == null || title.trim().isEmpty) { return "Playlist name can't be empty"; } Map playlist = await _box.get(playlistId); playlist['title'] = title; playlist['iconId'] = iconId; await _box.put(playlistId, playlist); notifyListeners(); return 'Playlist updated'; } Future removeFromLibrary(String key) async { if (_playlists.containsKey(key)) { await _box.delete(key); notifyListeners(); return 'Removed from Library'; } else { return 'Does not exist in Library'; } } Future addToPlaylist({required Map item, required String key}) async { Map? playlist = await _box.get(key); if (playlist == null) return 'Playlist does not exist'; List songs = playlist['songs'] ?? []; if (songs.contains(item)) { return 'Already present in Playlist'; } songs.add(item); playlist['songs'] = songs; await _box.put(key, playlist); notifyListeners(); return 'Added to Playlist'; } Future removeFromPlaylist({ required Map item, required String playlistId, }) async { Map? playlist = await _box.get(playlistId); if (playlist == null) return 'Playlist does not exist'; List songs = playlist['songs'] ?? []; if (songs.remove(item)) { playlist['songs'] = songs; await _box.put(playlistId, playlist); notifyListeners(); return 'Removed from Playlist'; } else { return 'An error occured'; } } Future setPlaylists(Map value) async { await Future.forEach(value.entries, (entry) async { await _box.put(entry.key, entry.value); }); notifyListeners(); } } ================================================ FILE: lib/services/lyrics.dart ================================================ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:translator/translator.dart'; import 'package:language_detector/language_detector.dart'; class Lyrics { Map lyricsList = {}; Future> getLyrics({ required String videoId, required String title, required int durationInSeconds, String? artist, String? album, String? translation, }) async { if (lyricsList[videoId] != null) { return lyricsList[videoId]; } Map response = await fetchLyrics( videoId: videoId, title: title, durationInSeconds: durationInSeconds, album: album, artist: artist, ); if (response["syncedLyrics"] != null || response['plainLyrics'] != null) { final lyricsData = { "syncedLyrics": response["syncedLyrics"].toString(), "plainLyrics": response["plainLyrics"], "transLyrics": "", }; if (translation != null) { String lyricsLang = await LanguageDetector.getLanguageCode( content: response["plainLyrics"]); if (lyricsLang != translation) { if (response["syncedLyrics"] != null) { lyricsData["transLyrics"] = await translateSyncLyrics( response["syncedLyrics"].toString(), lyricsLang, translation, ); } else { lyricsData["plainLyrics"] = await translatePlainLyrics( response['plainLyrics'], lyricsLang, translation, ); } } } lyricsList[videoId] = lyricsData; return lyricsData; } return {'success': false}; } void fixLrcFormat(Map lrc) { if (lrc.containsKey('syncedLyrics') && lrc['syncedLyrics'] != null) { lrc['syncedLyrics'] = (lrc['syncedLyrics'] as String) .replaceAllMapped(RegExp(r'\[(\d{2}):(\d{2}):(\d{2,3})\]'), (match) { return '[${match.group(1)}:${match.group(2)}.${match.group(3)}]'; }); } } Future fetchLyrics({ required String videoId, required String title, required int durationInSeconds, String? artist, String? album, }) async { try { Uri uri; bool isSpecificSearch = false; if (artist != null && album != null) { uri = Uri.https('lrclib.net', '/api/get', { 'artist_name': artist, 'track_name': title, 'album_name': album, 'duration': durationInSeconds.toString(), }); isSpecificSearch = true; } else { final params = {'track_name': title}; if (artist != null) params['artist_name'] = artist; if (album != null) params['album_name'] = album; uri = Uri.https('lrclib.net', '/api/search', params); } final response = await get(uri).timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { debugPrint("Error in lrclib get : ${response.statusCode}"); return {}; } final decoded = jsonDecode(utf8.decode(response.bodyBytes)); Map lyric = {}; if (isSpecificSearch) { if (decoded is Map) lyric = decoded; } else { if (decoded is List && decoded.isNotEmpty) { decoded.sort((a, b) { final durA = (a['duration'] as num).toDouble(); final durB = (b['duration'] as num).toDouble(); final target = durationInSeconds.toDouble(); return (durA - target).abs().compareTo((durB - target).abs()); }); lyric = decoded.first; } } fixLrcFormat(lyric); return lyric; } catch (e) { debugPrint("Error in fetchLyrics: $e"); return {}; } } Future translateSyncLyrics( String lyric, String from, String to) async { try { Translation trans = await lyric.translate(from: from, to: to); final transLines = trans.text.split("\n"); final lyricLines = lyric.split("\n"); if (lyricLines.length != transLines.length) { throw Exception("Translation lines do not match original lines"); } String transLyric = ""; for (int i = 0; i < transLines.length; i++) { transLyric += "${lyricLines[i].split("]")[0]}]${transLines[i].split("]")[1]}\n"; } return transLyric; } catch (e) { return ""; } } Future translatePlainLyrics( String lyric, String from, String to) async { try { Translation trans = await lyric.translate(from: from, to: to); final lines = lyric.split("\n"); final transLines = trans.text.split("\n"); if (lines.length != transLines.length) { throw Exception("Translation lines do not match original lines"); } String transLyric = ''; for (int i = 0; i < lines.length; i++) { transLyric += "${lines[i]}\n[${transLines[i]}]\n\n"; } return transLyric; } catch (e) { return ""; } } } ================================================ FILE: lib/services/media_player.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/services/download_manager.dart'; import 'package:gyawun/services/yt_audio_stream.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:rxdart/rxdart.dart'; import 'package:yt_music/ytmusic.dart'; import '../utils/add_history.dart'; import 'settings_manager.dart'; class MediaPlayer extends ChangeNotifier { late final AudioPlayer _player; final _loudnessEnhancer = AndroidLoudnessEnhancer(); AndroidEqualizer? _equalizer; AndroidEqualizerParameters? _equalizerParams; List _songList = []; final ValueNotifier _currentSongNotifier = ValueNotifier(null); final ValueNotifier _currentIndex = ValueNotifier(null); final ValueNotifier _buttonState = ValueNotifier( ButtonState.loading, ); Timer? _timer; final ValueNotifier _timerDuration = ValueNotifier(null); final ValueNotifier _loopMode = ValueNotifier(LoopMode.off); final ValueNotifier _progressBarState = ValueNotifier( ProgressBarState(), ); bool _shuffleModeEnabled = false; Object? _activeSession; MediaPlayer() { if (Platform.isAndroid) { _equalizer = AndroidEqualizer(); } final AudioPipeline pipeline = AudioPipeline( androidAudioEffects: [ if (Platform.isAndroid && _equalizer != null) _equalizer!, _loudnessEnhancer, ], ); _player = AudioPlayer(audioPipeline: pipeline); GetIt.I.registerSingleton(_loudnessEnhancer); if (Platform.isAndroid && _equalizer != null) { GetIt.I.registerSingleton(_equalizer!); } _init(); } AudioPlayer get player => _player; List get songList => List.unmodifiable(_songList); ValueNotifier get currentSongNotifier => _currentSongNotifier; ValueNotifier get currentIndex => _currentIndex; ValueNotifier get buttonState => _buttonState; ValueNotifier get progressBarState => _progressBarState; bool get shuffleModeEnabled => _shuffleModeEnabled; ValueNotifier get loopMode => _loopMode; ValueNotifier get timerDuration => _timerDuration; Object _startSession() => _activeSession = Object(); bool _isSessionValid(Object? session) => _activeSession == session; Stream< ({ List? sequence, int? currentIndex, MediaItem? currentItem, }) > get currentTrackStream => Rx.combineLatest2< List?, int?, ({ List? sequence, int? currentIndex, MediaItem? currentItem, }) >(_player.sequenceStream, _player.currentIndexStream, ( sequence, currentIndex, ) { MediaItem? currentItem; if (sequence != null && currentIndex != null && currentIndex >= 0 && currentIndex < sequence.length) { final tag = sequence[currentIndex].tag; if (tag is MediaItem) currentItem = tag; } return ( sequence: sequence, currentIndex: currentIndex, currentItem: currentItem, ); }); Future _init() async { await _loadLoudnessEnhancer(); await _loadEqualizer(); // Start with an empty queue await _player.setAudioSources([]); _listenToChangesInPlaylist(); _listenToPlaybackState(); _listenToCurrentPosition(); _listenToBufferedPosition(); _listenToTotalDuration(); _listenToChangesInSong(); _listenToShuffle(); _listenToAutofetch(); Timer.periodic(const Duration(seconds: 10), (timer) { if (currentSongNotifier.value != null && _player.playing) { GetIt.I().addPlayingStats( currentSongNotifier.value!.id, _player.position, ); } }); } Future _loadLoudnessEnhancer() async { await _loudnessEnhancer.setEnabled( GetIt.I().loudnessEnabled, ); await _loudnessEnhancer.setTargetGain( GetIt.I().loudnessTargetGain, ); } Future getEqualizerParameters() async { Map storedParams = GetIt.I().equalizerParameters; if (storedParams.isNotEmpty) return storedParams; _equalizerParams = await _equalizer!.parameters; await GetIt.I().setEqualizerParameters( _equalizerParams!.toMap(), ); return GetIt.I().equalizerParameters; } Future _loadEqualizer() async { if (!Platform.isAndroid || _equalizer == null) return; await _equalizer!.setEnabled(GetIt.I().equalizerEnabled); _equalizer!.parameters.then((value) async { _equalizerParams ??= value; if (GetIt.I().equalizerParameters.isEmpty) { GetIt.I().setEqualizerParameters( _equalizerParams!.toMap(), ); } else { List storedBandsGain = GetIt.I().equalizerBandsGain; final List bands = _equalizerParams!.bands; for (var e in bands) { final gain = storedBandsGain.isNotEmpty ? storedBandsGain[e.index] : 0.0; _equalizerParams!.bands[e.index].setGain(gain); } } }); } Future setLoudnessEnabled(bool value) async { await _loudnessEnhancer.setEnabled(value); GetIt.I().loudnessEnabled = value; } Future setEqualizerEnabled(bool value) async { await _equalizer?.setEnabled(value); GetIt.I().equalizerEnabled = value; } Future setLoudnessTargetGain(double value) async { await _loudnessEnhancer.setTargetGain(value); GetIt.I().loudnessTargetGain = value; } Future setEqualizerBandGain(int bandIndex, double gain) async { await GetIt.I().setEqualizerBandsGain(bandIndex, gain); _equalizerParams = await _equalizer!.parameters; await _equalizerParams!.bands[bandIndex].setGain(gain); } void _listenToChangesInPlaylist() { _player.sequenceStream.listen((playlist) { final List newList = (playlist) .cast(); if (listEquals(newList, _songList)) return; final bool shouldAdd = (_songList.isEmpty && newList.isNotEmpty); if (newList.isEmpty) { _currentSongNotifier.value = null; _currentIndex.value = null; _songList = []; } else { _songList = newList; final currentIndex = _currentIndex.value ??= 0; _currentSongNotifier.value = (_songList.length > currentIndex) ? _songList[currentIndex].tag : null; } if (shouldAdd == true && _currentSongNotifier.value != null) { addHistory(_currentSongNotifier.value!.extras!); } notifyListeners(); }); } void _listenToPlaybackState() { _player.playerStateStream.listen((event) { final isPlaying = event.playing; final processingState = event.processingState; if (processingState == ProcessingState.loading || processingState == ProcessingState.buffering) { _buttonState.value = ButtonState.loading; } else if (!isPlaying || processingState == ProcessingState.idle) { _buttonState.value = ButtonState.paused; } else if (processingState != ProcessingState.completed) { _buttonState.value = ButtonState.playing; } else { _player.seek(Duration.zero); _player.pause(); } }); } void _listenToCurrentPosition() { _player.positionStream.listen((position) { final oldState = _progressBarState.value; if (oldState.current != position) { _progressBarState.value = ProgressBarState( current: position, buffered: oldState.buffered, total: oldState.total, ); } }); } void _listenToBufferedPosition() { _player.bufferedPositionStream.listen((position) { final oldState = _progressBarState.value; if (oldState.buffered != position) { _progressBarState.value = ProgressBarState( current: oldState.current, buffered: position, total: oldState.total, ); } }); } void _listenToTotalDuration() { _player.durationStream.listen((position) { final oldState = _progressBarState.value; if (oldState.total != position) { _progressBarState.value = ProgressBarState( current: oldState.current, buffered: oldState.buffered, total: position ?? Duration.zero, ); } }); } void _listenToShuffle() { _player.shuffleModeEnabledStream.listen((data) { _shuffleModeEnabled = data; notifyListeners(); }); } void _listenToChangesInSong() { _player.currentIndexStream.listen((index) { if (_songList.isNotEmpty && _currentIndex.value != index) { _currentIndex.value = index; _currentSongNotifier.value = index != null && _songList.isNotEmpty && index < _songList.length ? _songList[index].tag : null; if (_songList.isNotEmpty && _currentIndex.value != null) { final MediaItem item = _songList[_currentIndex.value!].tag; addHistory(item.extras!); } notifyListeners(); } }); } Future _fetchAndQueueSongs({ String? videoId, String? playlistId, String continuation = '', String? params, bool radio = false, bool shuffle = false, bool isNext = false, int offset = 0, int maxContinuations = 50, // playlist and albums with up to 24 * 51 songs Object? session, }) async { Map songs = await GetIt.I().getNextSongList( videoId: videoId, playlistId: playlistId, continuation: continuation, params: params, radio: radio, shuffle: shuffle, ); if (!_isSessionValid(session)) return []; if (songs["continuation"] != null && maxContinuations > 0) { final newOffset = offset + songs["contents"].length as int; _fetchAndQueueSongs( continuation: songs["continuation"], isNext: isNext, offset: newOffset, maxContinuations: maxContinuations - 1, session: session, ).then((s) async { if (!_isSessionValid(session)) return; await _addSongListToQueue(s, isNext: isNext, offset: newOffset); }); } return songs["contents"]; } void changeLoopMode() { switch (_loopMode.value) { case LoopMode.off: _loopMode.value = LoopMode.all; break; case LoopMode.all: _loopMode.value = LoopMode.one; break; default: _loopMode.value = LoopMode.off; break; } _player.setLoopMode(_loopMode.value); } Future skipSilence(bool value) async { await _player.setSkipSilenceEnabled(value); GetIt.I().skipSilence = value; } Future _getAudioSource(Map song) async { MediaItem tag = MediaItem( id: song['videoId'], title: song['title'] ?? 'Title', album: song['album']?['name'], artUri: Uri.parse( song['thumbnails']?.first['url'].replaceAll('w60-h60', 'w225-h225'), ), artist: song['artists']?.map((artist) => artist['name']).join(','), extras: song, ); final downloadSong = GetIt.I().getDownload( song['videoId'], ); final bool isDownloaded = downloadSong != null && downloadSong['status'] == 'DOWNLOADED' && downloadSong['path'] != null && (await File(downloadSong['path']).exists()); if (isDownloaded) { return AudioSource.file(downloadSong['path'], tag: tag); } else { return YouTubeAudioSource( videoId: song['videoId'], quality: GetIt.I().streamingQuality.name.toLowerCase(), tag: tag, ); } } Future> _getAudioSources(List songs) async { return await Future.wait( songs.map((song) async { final mapSong = Map.from(song); return await _getAudioSource(mapSong); }), ); } Future _getPlaylistSongs({ required Map mediaItem, required Object? session, bool isNext = false, }) async { if (mediaItem['songs'] != null) { // Get Custom or Downloaded Playlist songs return mediaItem['songs']; } else { // Get Online Playlist songs return await _fetchAndQueueSongs( playlistId: mediaItem['playlistId'], isNext: isNext, maxContinuations: mediaItem['type'] == 'ARTIST' ? 0 : 50, session: session, ); } } Future playSong(Map song) async { final session = _startSession(); if (song['videoId'] == null) return; // clear sources and set the tapped song as the single source so it plays immediately await _player.clearAudioSources(); final source = await _getAudioSource(song); if (!_isSessionValid(session)) return; await _player.setAudioSources([source]); await _player.play(); } Future playNext(Map mediaItem) async { final session = _startSession(); // Case 1: A single video/song if (mediaItem['videoId'] != null) { final audioSource = await _getAudioSource(mediaItem); // Determine insertion position final currentIndex = _player.currentIndex ?? -1; final sequenceLength = _player.sequence.length; final insertIndex = (currentIndex + 1).clamp(0, sequenceLength); if (!_isSessionValid(session)) return; // If player already has something in the queue if (sequenceLength > 0) { await _player.insertAudioSource(insertIndex, audioSource); } else { // If queue is empty, just set audio source await _player.setAudioSource(audioSource); } } else { // Case 2: Playlist List songs = await _getPlaylistSongs( mediaItem: mediaItem, session: _activeSession, isNext: true, ); if (!_isSessionValid(session)) return; await _addSongListToQueue(songs, isNext: true); } } Future playAll(List songs, {int index = 0}) async { final session = _startSession(); await _player.clearAudioSources(); // Build full list and set atomically final List sources = await _getAudioSources(songs); if (!_isSessionValid(session)) return; await _player.setAudioSources(sources); await _player.seek(Duration.zero, index: index); if (!_player.playing) await _player.play(); } Future addToQueue(Map mediaItem) async { final session = _startSession(); // Case 1: A single video/song if (mediaItem['videoId'] != null) { final audioSource = await _getAudioSource(mediaItem); if (!_isSessionValid(session)) return; if (_player.sequence.isEmpty) { // If queue is empty, just set audio source await _player.setAudioSource(audioSource); } else { // If player already has something in the queue await _player.addAudioSource(audioSource); } // Case 2: Playlist } else { List songs = await _getPlaylistSongs( mediaItem: mediaItem, session: _activeSession, ); if (!_isSessionValid(session)) return; await _addSongListToQueue(songs, isNext: false); } } Future startRelated( Map song, { bool radio = false, bool shuffle = false, bool isArtist = false, }) async { final session = _startSession(); await _player.clearAudioSources(); if (!isArtist) { await addToQueue(song); } List songs = await _fetchAndQueueSongs( videoId: song['videoId'], playlistId: song['playlistRadioId'], radio: radio, shuffle: shuffle, maxContinuations: 0, session: session, ); if (!_isSessionValid(session)) return; if (songs.isNotEmpty) songs.removeAt(0); await _addSongListToQueue(songs); await _player.play(); } Future startPlaylistSongs(Map endpoint) async { final session = _startSession(); await _player.clearAudioSources(); List songs = await _fetchAndQueueSongs( playlistId: endpoint['playlistId'], params: endpoint['params'], maxContinuations: endpoint['type'] == 'ARTIST' ? 0 : 50, session: session, ); if (songs.isNotEmpty && songs.first['videoId'] == null) { // if API returned a placeholder, convert or handle accordingly } if (!_isSessionValid(session)) return; await _addSongListToQueue(songs); await _player.play(); } Future stop() async { _activeSession = null; await _player.stop(); await _player.clearAudioSources(); await _player.seek(Duration.zero, index: 0); _currentIndex.value = null; _currentSongNotifier.value = null; notifyListeners(); } Future _addSongListToQueue( List songs, { bool isNext = false, int offset = 0, }) async { if (songs.isEmpty) return; // Convert your song objects into AudioSources final newSources = await _getAudioSources(songs); // Current queue length final queueLength = _player.sequence.length; if (queueLength > 0) { if (isNext) { // Insert immediately after the current index final currentIndex = _player.currentIndex ?? -1; int insertIndex = (currentIndex + offset + 1).clamp(0, queueLength); await _player.insertAudioSources(insertIndex, newSources); } else { // Append to the end await _player.addAudioSources(newSources); } } else { // If queue is empty, just set audio sources await _player.setAudioSources(newSources); } } void _listenToAutofetch() { player.playerStateStream.listen((state) async { if (state.processingState == ProcessingState.completed && _songList.isNotEmpty && GetIt.I().autofetchSongs) { final session = _startSession(); List songs = await _fetchAndQueueSongs( videoId: _songList[_currentIndex.value ?? 0].tag.id, maxContinuations: 0, session: session, ); if (!_isSessionValid(session)) return; if (songs.isNotEmpty) songs.removeAt(0); await _player.clearAudioSources(); await _addSongListToQueue(songs); await _player.play(); } }); } void setTimer(Duration duration) { int seconds = duration.inSeconds; _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { seconds--; _timerDuration.value = Duration(seconds: seconds); if (seconds == 0) { cancelTimer(); _player.pause(); } notifyListeners(); }); } void cancelTimer() { _timerDuration.value = null; _timer?.cancel(); notifyListeners(); } } enum ButtonState { loading, paused, playing } enum LoopState { off, all, one } class ProgressBarState { Duration current; Duration buffered; Duration total; ProgressBarState({ this.current = Duration.zero, this.buffered = Duration.zero, this.total = Duration.zero, }); } extension on AndroidEqualizerParameters { Map toMap() { return { 'maxDecibels': maxDecibels, 'minDecibels': minDecibels, 'bands': bands .map( (e) => { 'centerFrequency': e.centerFrequency, 'gain': e.gain, 'index': e.index, }, ) .toList(), }; } } ================================================ FILE: lib/services/settings_manager.dart ================================================ import 'package:flutter/material.dart'; import 'package:gyawun/services/file_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; class SettingsManager extends ChangeNotifier { final Box _box; ThemeMode _themeMode = ThemeMode.system; final List _themeModes = [ ThemeMode.system, ThemeMode.light, ThemeMode.dark, ]; late Map _location; late Map _language; bool _autofetchSongs = true; final List _audioQualities = [ AudioQuality.high, AudioQuality.low, ]; AudioQuality _streamingQuality = AudioQuality.high; AudioQuality _downloadQuality = AudioQuality.high; bool _skipSilence = false; Color? _accentColor; bool _amoledBlack = true; bool _dynamicColors = false; bool _equalizerEnabled = false; Map _equalizerParameters = {}; bool _loudnessEnabled = false; double _loudnessTargetGain = 0.0; bool _searchHistory = true; bool _translateLyrics = false; bool _playbackHistory = true; bool _personalisedContent = true; String? _visitorId; String? _apiKey; String? _clientName; String? _clientVersion; String _appFolder = FileStorage.defaultPath; ThemeMode get themeMode => _themeMode; List get themeModes => _themeModes; Map get location => _location; List> get locations => _countries; Map get language => _language; bool get autofetchSongs => _autofetchSongs; List> get languages => _languages; List get audioQualities => _audioQualities; AudioQuality get streamingQuality => _streamingQuality; AudioQuality get downloadQuality => _downloadQuality; bool get skipSilence => _skipSilence; Color? get accentColor => _accentColor; bool get amoledBlack => _amoledBlack; bool get dynamicColors => _dynamicColors; bool get loudnessEnabled => _loudnessEnabled; double get loudnessTargetGain => _loudnessTargetGain; bool get equalizerEnabled => _equalizerEnabled; Map get equalizerParameters => _equalizerParameters; List get equalizerBandsGain => (equalizerParameters['bands'] as List?) ?.map((e) => (e['gain'] as num).toDouble()) .toList() ?? []; bool get searchHistory => _searchHistory; bool get translateLyrics => _translateLyrics; bool get playbackHistory => _playbackHistory; bool get personalisedContent => _personalisedContent; String? get visitorId => _visitorId; String? get apiKey => _apiKey; String? get clientName => _clientName; String? get clientVersion => _clientVersion; String get appFolder => _appFolder; Map get settings => _box.toMap(); SettingsManager._(this._box) { _init(); } static Future create() async { final boxName = 'SETTINGS'; await Hive.openBox(boxName); final instance = SettingsManager._(Hive.box(boxName)); return instance; } void _init() { _themeMode = _themeModes[_box.get('THEME_MODE', defaultValue: 0)]; _language = _languages.firstWhere( (language) => language['value'] == _box.get('LANGUAGE', defaultValue: 'en-IN'), ); _autofetchSongs = _box.get('AUTOFETCH_SONGS', defaultValue: true); _accentColor = _box.get('ACCENT_COLOR') != null ? Color(_box.get('ACCENT_COLOR')) : null; _amoledBlack = _box.get('AMOLED_BLACK', defaultValue: true); _dynamicColors = _box.get('DYNAMIC_COLORS', defaultValue: false); _location = _countries.firstWhere( (country) => country['value'] == _box.get('LOCATION', defaultValue: 'IN'), ); _streamingQuality = _audioQualities[_box.get('STREAMING_QUALITY', defaultValue: 0)]; _downloadQuality = _audioQualities[_box.get('DOWNLOAD_QUALITY', defaultValue: 0)]; _skipSilence = _box.get('SKIP_SILENCE', defaultValue: false); _equalizerEnabled = _box.get('EQUALIZER_ENABLED', defaultValue: false); _loudnessEnabled = _box.get('LOUDNESS_ENABLED', defaultValue: false); _loudnessTargetGain = _box.get('LOUDNESS_TARGET_GAIN', defaultValue: 0.0); _equalizerParameters = _box.get('EQUALIZER_PARAMETERS', defaultValue: {}); _searchHistory = _box.get('SEARCH_HISTORY', defaultValue: true); _translateLyrics = _box.get('TRANSLATE_LYRICS', defaultValue: false); _playbackHistory = _box.get('PLAYBACK_HISTORY', defaultValue: true); _personalisedContent = _box.get('PERSONALISED_CONTENT', defaultValue: true); _visitorId = _box.get('YT_VISITOR_ID'); _apiKey = _box.get('YT_API_KEY'); _clientName = _box.get('YT_CLIENT_NAME'); _clientVersion = _box.get('YT_CLIENT_VERSION'); _appFolder = _box.get('APP_FOLDER', defaultValue: FileStorage.defaultPath); } Future setThemeMode(ThemeMode mode) async { _box.put('THEME_MODE', _themeModes.indexOf(mode)); _themeMode = mode; notifyListeners(); } set searchHistory(bool value) { _box.put('SEARCH_HISTORY', value); _searchHistory = value; notifyListeners(); } set translateLyrics(bool value) { _box.put('TRANSLATE_LYRICS', value); _translateLyrics = value; notifyListeners(); } set playbackHistory(bool value) { _box.put('PLAYBACK_HISTORY', value); _playbackHistory = value; notifyListeners(); } set personalisedContent(bool value) { _box.put('PERSONALISED_CONTENT', value); _personalisedContent = value; notifyListeners(); } set visitorId(String? value) { _box.put('YT_VISITOR_ID', value); _visitorId = value; notifyListeners(); } set apiKey(String? value) { _box.put('YT_API_KEY', value); _apiKey = value; notifyListeners(); } set clientName(String? value) { _box.put('YT_CLIENT_NAME', value); _clientName = value; notifyListeners(); } set clientVersion(String? value) { _box.put('YT_CLIENT_VERSION', value); _clientVersion = value; notifyListeners(); } set appFolder(String value) { _box.put('APP_FOLDER', value); _appFolder = value; notifyListeners(); } set location(Map value) { _box.put('LOCATION', value['value']); _location = value; notifyListeners(); } set language(Map value) { _box.put('LANGUAGE', value['value']); _language = value; notifyListeners(); } set autofetchSongs(bool value) { _box.put('AUTOFETCH_SONGS', value); _autofetchSongs = value; notifyListeners(); } set streamingQuality(AudioQuality value) { _box.put('STREAMING_QUALITY', _audioQualities.indexOf(value)); _streamingQuality = value; notifyListeners(); } set downloadQuality(AudioQuality value) { _box.put('DOWNLOAD_QUALITY', _audioQualities.indexOf(value)); _downloadQuality = value; notifyListeners(); } set skipSilence(bool value) { _box.put('SKIP_SILENCE', value); _skipSilence = value; notifyListeners(); } set accentColor(Color? color) { int? c = color?.toARGB32(); _box.put('ACCENT_COLOR', c); _accentColor = color; notifyListeners(); } set amoledBlack(bool val) { _box.put('AMOLED_BLACK', val); _amoledBlack = val; notifyListeners(); } set dynamicColors(bool isMaterial) { _box.put('DYNAMIC_COLORS', isMaterial); _dynamicColors = isMaterial; notifyListeners(); } set equalizerEnabled(bool enabled) { _box.put('EQUALIZER_ENABLED', enabled); _equalizerEnabled = enabled; notifyListeners(); } Future setEqualizerParameters(Map value) async { await _box.put('EQUALIZER_PARAMETERS', value); _equalizerParameters = value; notifyListeners(); } Future setEqualizerBandsGain(int index, double value) async { _equalizerParameters['bands'][index]['gain'] = value; await _box.put('EQUALIZER_PARAMETERS', _equalizerParameters); notifyListeners(); } // ignore: strict_top_level_inference set loudnessEnabled(enabled) { _box.put('LOUDNESS_ENABLED', enabled); _loudnessEnabled = enabled; notifyListeners(); } set loudnessTargetGain(double value) { _box.put('LOUDNESS_TARGET_GAIN', value); _loudnessTargetGain = value; notifyListeners(); } Future setSettings(Map value) async { await Future.forEach(value.entries, (entry) async { await _box.put(entry.key, entry.value); }); notifyListeners(); _init(); } } bool getDarkness(int themeMode) { if (themeMode == 0) { return MediaQueryData.fromView( WidgetsBinding.instance.platformDispatcher.views.first, ).platformBrightness == Brightness.dark ? true : false; } else if (themeMode == 2) { return true; } return false; } enum AudioQuality { high, low } List> _countries = [ {"name": "Algeria", "value": "DZ"}, {"name": "Argentina", "value": "AR"}, {"name": "Australia", "value": "AU"}, {"name": "Austria", "value": "AT"}, {"name": "Azerbaijan", "value": "AZ"}, {"name": "Bahrain", "value": "BH"}, {"name": "Bangladesh", "value": "BD"}, {"name": "Belarus", "value": "BY"}, {"name": "Belgium", "value": "BE"}, {"name": "Bolivia", "value": "BO"}, {"name": "Bosnia and Herzegovina", "value": "BA"}, {"name": "Brazil", "value": "BR"}, {"name": "Bulgaria", "value": "BG"}, {"name": "Cambodia", "value": "KH"}, {"name": "Canada", "value": "CA"}, {"name": "Chile", "value": "CL"}, {"name": "Colombia", "value": "CO"}, {"name": "Costa Rica", "value": "CR"}, {"name": "Croatia", "value": "HR"}, {"name": "Cyprus", "value": "CY"}, {"name": "Czechia", "value": "CZ"}, {"name": "Denmark", "value": "DK"}, {"name": "Dominican Republic", "value": "DO"}, {"name": "Ecuador", "value": "EC"}, {"name": "Egypt", "value": "EG"}, {"name": "El Salvador", "value": "SV"}, {"name": "Estonia", "value": "EE"}, {"name": "Finland", "value": "FI"}, {"name": "France", "value": "FR"}, {"name": "Georgia", "value": "GE"}, {"name": "Germany", "value": "DE"}, {"name": "Ghana", "value": "GH"}, {"name": "Greece", "value": "GR"}, {"name": "Guatemala", "value": "GT"}, {"name": "Honduras", "value": "HN"}, {"name": "Hong Kong", "value": "HK"}, {"name": "Hungary", "value": "HU"}, {"name": "Iceland", "value": "IS"}, {"name": "India", "value": "IN"}, {"name": "Indonesia", "value": "ID"}, {"name": "Iraq", "value": "IQ"}, {"name": "Ireland", "value": "IE"}, {"name": "Israel", "value": "IL"}, {"name": "Italy", "value": "IT"}, {"name": "Jamaica", "value": "JM"}, {"name": "Japan", "value": "JP"}, {"name": "Jordan", "value": "JO"}, {"name": "Kazakhstan", "value": "KZ"}, {"name": "Kenya", "value": "KE"}, {"name": "Kuwait", "value": "KW"}, {"name": "Laos", "value": "LA"}, {"name": "Latvia", "value": "LV"}, {"name": "Lebanon", "value": "LB"}, {"name": "Libya", "value": "LY"}, {"name": "Liechtenstein", "value": "LI"}, {"name": "Lithuania", "value": "LT"}, {"name": "Luxembourg", "value": "LU"}, {"name": "Malaysia", "value": "MY"}, {"name": "Malta", "value": "MT"}, {"name": "Mexico", "value": "MX"}, {"name": "Moldova", "value": "MD"}, {"name": "Montenegro", "value": "ME"}, {"name": "Morocco", "value": "MA"}, {"name": "Nepal", "value": "NP"}, {"name": "Netherlands", "value": "NL"}, {"name": "New Zealand", "value": "NZ"}, {"name": "Nicaragua", "value": "NI"}, {"name": "Nigeria", "value": "NG"}, {"name": "North Macedonia", "value": "MK"}, {"name": "Norway", "value": "NO"}, {"name": "Oman", "value": "OM"}, {"name": "Pakistan", "value": "PK"}, {"name": "Panama", "value": "PA"}, {"name": "Papua New Guinea", "value": "PG"}, {"name": "Paraguay", "value": "PY"}, {"name": "Peru", "value": "PE"}, {"name": "Philippines", "value": "PH"}, {"name": "Poland", "value": "PL"}, {"name": "Portugal", "value": "PT"}, {"name": "Puerto Rico", "value": "PR"}, {"name": "Qatar", "value": "QA"}, {"name": "Romania", "value": "RO"}, {"name": "Russia", "value": "RU"}, {"name": "Saudi Arabia", "value": "SA"}, {"name": "Senegal", "value": "SN"}, {"name": "Serbia", "value": "RS"}, {"name": "Singapore", "value": "SG"}, {"name": "Slovakia", "value": "SK"}, {"name": "Slovenia", "value": "SI"}, {"name": "South Africa", "value": "ZA"}, {"name": "South Korea", "value": "KR"}, {"name": "Spain", "value": "ES"}, {"name": "Sri Lanka", "value": "LK"}, {"name": "Sweden", "value": "SE"}, {"name": "Switzerland", "value": "CH"}, {"name": "Taiwan", "value": "TW"}, {"name": "Tanzania", "value": "TZ"}, {"name": "Thailand", "value": "TH"}, {"name": "Tunisia", "value": "TN"}, {"name": "Turkey", "value": "TR"}, {"name": "Uganda", "value": "UG"}, {"name": "Ukraine", "value": "UA"}, {"name": "United Arab Emirates", "value": "AE"}, {"name": "United Kingdom", "value": "GB"}, {"name": "United States", "value": "US"}, {"name": "Uruguay", "value": "UY"}, {"name": "Venezuela", "value": "VE"}, {"name": "Vietnam", "value": "VN"}, {"name": "Yemen", "value": "YE"}, {"name": "Zimbabwe", "value": "ZW"}, ]; List> _languages = [ {"name": "Afrikaans", "value": "af"}, {"name": "Azərbaycan", "value": "az"}, {"name": "Bahasa Indonesia", "value": "id"}, {"name": "Bahasa Malaysia", "value": "ms"}, {"name": "Bosanski", "value": "bs"}, {"name": "Català", "value": "ca"}, {"name": "Čeština", "value": "cs"}, {"name": "Dansk", "value": "da"}, {"name": "Deutsch", "value": "de"}, {"name": "Eesti", "value": "et"}, {"name": "English (India)", "value": "en-IN"}, {"name": "English (UK)", "value": "en-GB"}, {"name": "English (US)", "value": "en"}, {"name": "Español (España)", "value": "es"}, {"name": "Español (Latinoamérica)", "value": "es-419"}, {"name": "Español (US)", "value": "es-US"}, {"name": "Euskara", "value": "eu"}, {"name": "Filipino", "value": "fil"}, {"name": "Français", "value": "fr"}, {"name": "Français (Canada)", "value": "fr-CA"}, {"name": "Galego", "value": "gl"}, {"name": "Hrvatski", "value": "hr"}, {"name": "IsiZulu", "value": "zu"}, {"name": "Íslenska", "value": "is"}, {"name": "Italiano", "value": "it"}, {"name": "Kiswahili", "value": "sw"}, {"name": "Latviešu valoda", "value": "lv"}, {"name": "Lietuvių", "value": "lt"}, {"name": "Magyar", "value": "hu"}, {"name": "Nederlands", "value": "nl"}, {"name": "Norsk", "value": "no"}, {"name": "O‘zbek", "value": "uz"}, {"name": "Polski", "value": "pl"}, {"name": "Português", "value": "pt-PT"}, {"name": "Português (Brasil)", "value": "pt"}, {"name": "Română", "value": "ro"}, {"name": "Shqip", "value": "sq"}, {"name": "Slovenčina", "value": "sk"}, {"name": "Slovenščina", "value": "sl"}, {"name": "Srpski", "value": "sr-Latn"}, {"name": "Suomi", "value": "fi"}, {"name": "Svenska", "value": "sv"}, {"name": "Tiếng Việt", "value": "vi"}, {"name": "Türkçe", "value": "tr"}, {"name": "Беларуская", "value": "be"}, {"name": "Български", "value": "bg"}, {"name": "Кыргызча", "value": "ky"}, {"name": "Қазақ Тілі", "value": "kk"}, {"name": "Македонски", "value": "mk"}, {"name": "Монгол", "value": "mn"}, {"name": "Русский", "value": "ru"}, {"name": "Српски", "value": "sr"}, {"name": "Українська", "value": "uk"}, {"name": "Ελληνικά", "value": "el"}, {"name": "Հայերեն", "value": "hy"}, {"name": "עברית", "value": "iw"}, {"name": "اردو", "value": "ur"}, {"name": "العربية", "value": "ar"}, {"name": "فارسی", "value": "fa"}, {"name": "नेपाली", "value": "ne"}, {"name": "मराठी", "value": "mr"}, {"name": "हिन्दी", "value": "hi"}, {"name": "অসমীয়া", "value": "as"}, {"name": "বাংলা", "value": "bn"}, {"name": "ਪੰਜਾਬੀ", "value": "pa"}, {"name": "ગુજરાતી", "value": "gu"}, {"name": "ଓଡ଼ିଆ", "value": "or"}, {"name": "தமிழ்", "value": "ta"}, {"name": "తెలుగు", "value": "te"}, {"name": "ಕನ್ನಡ", "value": "kn"}, {"name": "മലയാളം", "value": "ml"}, {"name": "සිංහල", "value": "si"}, {"name": "ภาษาไทย", "value": "th"}, {"name": "ລາວ", "value": "lo"}, {"name": "ဗမာ", "value": "my"}, {"name": "ქართული", "value": "ka"}, {"name": "አማርኛ", "value": "am"}, {"name": "ខ្មែរ", "value": "km"}, {"name": "中文 (简体)", "value": "zh-CN"}, {"name": "中文 (繁體)", "value": "zh-TW"}, {"name": "中文 (香港)", "value": "zh-HK"}, {"name": "日本語", "value": "ja"}, {"name": "한국어", "value": "ko"}, ]; ================================================ FILE: lib/services/stream_client.dart ================================================ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class AudioStreamClient { final http.Client _httpClient = http.Client(); AudioStreamClient(); static const Map _defaultHeaders = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', 'cookie': 'CONSENT=YES+cb', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'accept-language': 'en-US,en;q=0.9', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'sec-gpc': '1', 'upgrade-insecure-requests': '1', }; Stream> getAudioStream(StreamInfo streamInfo, {required int start, required int end}) => _getStream(streamInfo, streamClient: this, start: start, end: end); Stream> _getStream( StreamInfo streamInfo, { Map headers = const {}, bool validate = true, required int start, required int end, int errorCount = 0, required AudioStreamClient streamClient, }) async* { var url = streamInfo.url; var bytesCount = start; while (bytesCount != end) { try { final response = await retry(this, () async { final from = bytesCount; final to = end - 1; late final http.Request request; if (url.queryParameters['c'] == 'ANDROID') { request = http.Request('get', url); request.headers['Range'] = 'bytes=$from-$to'; } else { url = url.replace(queryParameters: { ...url.queryParameters, 'range': '$from-$to' }); request = http.Request('get', url); } return send(request); }); if (validate) { try { _validateResponse(response, response.statusCode); } on FatalFailureException { continue; } } final stream = StreamController>(); response.stream.listen( (data) { bytesCount += data.length; stream.add(data); }, onError: (_) => null, onDone: stream.close, cancelOnError: false, ); errorCount = 0; yield* stream.stream; } on HttpClientClosedException { break; } on Exception { if (errorCount == 5) { rethrow; } await Future.delayed(const Duration(milliseconds: 500)); yield* _getStream( streamInfo, streamClient: streamClient, headers: headers, validate: validate, start: bytesCount, end: end, errorCount: errorCount + 1, ); break; } } } void _validateResponse(http.BaseResponse response, int statusCode) { final request = response.request!; if (request.url.host.endsWith('.google.com') && request.url.path.startsWith('/sorry/')) { throw RequestLimitExceededException.httpRequest(response); } if (statusCode >= 500) { throw TransientFailureException.httpRequest(response); } if (statusCode == 429) { throw RequestLimitExceededException.httpRequest(response); } if (statusCode >= 400) { throw FatalFailureException.httpRequest(response); } } Future retry( AudioStreamClient? client, FutureOr Function() function, ) async { var retryCount = 5; // ignore: literal_only_boolean_expressions while (true) { try { return await function(); // ignore: avoid_catches_without_on_clauses } on Exception catch (e) { retryCount -= getExceptionCost(e); if (retryCount <= 0) { rethrow; } await Future.delayed(const Duration(milliseconds: 500)); } } } int getExceptionCost(Exception e) { if (e is RequestLimitExceededException) { return 2; } if (e is FatalFailureException) { return 3; } return 1; } Future send(http.BaseRequest request) async { // Apply default headers if they are not already present _defaultHeaders.forEach((key, value) { if (request.headers[key] == null) { request.headers[key] = _defaultHeaders[key]!; } }); // print(request); // print(StackTrace.current); return _httpClient.send(request); } } ================================================ FILE: lib/services/update_service/models/update_info.dart ================================================ import 'package:pub_semver/pub_semver.dart'; class UpdateInfo { final Version version; final String name; final String body; final String publishedAt; final String downloadUrl; UpdateInfo({ required this.version, required this.name, required this.body, required this.publishedAt, required this.downloadUrl, }); } ================================================ FILE: lib/services/update_service/update_service.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:gyawun/services/update_service/models/update_info.dart'; import 'package:gyawun/services/update_service/widgets/update_checking.dart'; import 'package:gyawun/services/update_service/widgets/update_dialog.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pub_semver/pub_semver.dart'; class UpdateService { static const String owner = 'sheikhhaziq'; static const String repo = 'gyawun_music'; /* ───────────────────────────────────────────── * UPDATE CHECK (CHANNEL AWARE) * ───────────────────────────────────────────── */ static Future checkForUpdate() async { try { final package = await PackageInfo.fromPlatform(); final currentVersion = Version.parse(package.version); final bool isAlpha = package.version.contains('-alpha.'); final bool isBeta = package.version.contains('-beta.'); final uri = Uri.parse( 'https://api.github.com/repos/$owner/$repo/releases', ); final response = await http.get( uri, headers: {'Accept': 'application/vnd.github+json'}, ); if (response.statusCode != 200) return null; final List releases = jsonDecode(response.body); Iterable> channelReleases; if (isAlpha) { channelReleases = releases .where( (r) => r['prerelease'] == true && r['tag_name'].toString().contains('-alpha.'), ) .whereType>(); } else if (isBeta) { channelReleases = releases .where( (r) => r['prerelease'] == true && r['tag_name'].toString().contains('-beta.'), ) .whereType>(); } else { channelReleases = releases .where((r) => r['prerelease'] == false) .whereType>(); } if (channelReleases.isEmpty) return null; final sorted = channelReleases.toList() ..sort((a, b) { final va = Version.parse( a['tag_name'].toString().replaceFirst('v', ''), ); final vb = Version.parse( b['tag_name'].toString().replaceFirst('v', ''), ); return vb.compareTo(va); }); final release = sorted.first; final remoteVersion = Version.parse( release['tag_name'].toString().replaceFirst('v', ''), ); if (remoteVersion <= currentVersion) return null; final asset = await _selectAsset(release['assets']); if (asset == null) return null; return UpdateInfo( version: remoteVersion, name: release['name'] ?? '', body: release['body'] ?? '', publishedAt: release['published_at'] ?? '', downloadUrl: asset['browser_download_url'], ); } catch (_) { return null; } } /* ───────────────────────────────────────────── */ static Future autoCheck(BuildContext context) async { final update = await checkForUpdate(); if (update == null || !context.mounted) return; await showUpdateDialog(context, update); } static Future manualCheck(BuildContext context) async { showDialog( context: context, barrierDismissible: false, useRootNavigator: false, builder: (_) => const UpdateCheckingDialog(), ); final update = await checkForUpdate(); if (!context.mounted) return; Navigator.pop(context); if (update != null) { await showUpdateDialog(context, update); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('You are already on the latest version')), ); } } static Future showUpdateDialog(BuildContext context, UpdateInfo info) { return showDialog( context: context, useRootNavigator: false, builder: (_) => UpdateDialog(info), ); } /* ───────────────────────────────────────────── * ASSET SELECTION * ───────────────────────────────────────────── */ static Future _selectAsset(List assets) async { final deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { final android = await deviceInfo.androidInfo; for (final abi in android.supportedAbis) { final match = assets.where((a) => a['name'].contains(abi)).toList(); if (match.isNotEmpty) return match.first; } } // if (Platform.isWindows) { // return assets.firstWhere( // (a) => a['name'].toString().endsWith('.exe'), // orElse: () => null, // ); // } return null; } } ================================================ FILE: lib/services/update_service/widgets/update_checking.dart ================================================ import 'package:flutter/material.dart'; class UpdateCheckingDialog extends StatelessWidget { const UpdateCheckingDialog({super.key}); @override Widget build(BuildContext context) { return const Dialog( insetPadding: EdgeInsets.all(48), child: Padding( padding: EdgeInsets.all(24), child: Row( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(width: 16), Text('Checking for updates…'), ], ), ), ); } } ================================================ FILE: lib/services/update_service/widgets/update_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:gyawun/services/update_service/models/update_info.dart'; import 'package:url_launcher/url_launcher.dart'; class UpdateDialog extends StatelessWidget { final UpdateInfo info; const UpdateDialog(this.info, {super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( title: const Text('Update Available'), content: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 400), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Version ${info.version}', style: theme.textTheme.titleMedium, ), const SizedBox(height: 12), MarkdownBody( data: info.body.isNotEmpty ? info.body : '_No changelog provided._', selectable: true, styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith( p: theme.textTheme.bodyMedium, h1: theme.textTheme.titleLarge, h2: theme.textTheme.titleMedium, h3: theme.textTheme.titleSmall, blockquoteDecoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), ), onTapLink: (text, href, title) async { if (href == null) return; final uri = Uri.parse(href); if (await canLaunchUrl(uri)) { await launchUrl( uri, mode: LaunchMode.externalApplication, ); } }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Later'), ), FilledButton( onPressed: () async { final uri = Uri.parse(info.downloadUrl); await launchUrl( uri, mode: LaunchMode.externalApplication, ); }, child: const Text('Update'), ), ], ); } } ================================================ FILE: lib/services/yt_audio_stream.dart ================================================ // ignore_for_file: experimental_member_use import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class YouTubeAudioSource extends StreamAudioSource { final String videoId; final String quality; // 'high' or 'low' final YoutubeExplode ytExplode; YouTubeAudioSource({required this.videoId, required this.quality, super.tag}) : ytExplode = YoutubeExplode(); @override Future request([int? start, int? end]) async { try { final manifest = await ytExplode.videos.streams.getManifest( videoId, requireWatchPage: true, ytClients: [YoutubeApiClient.androidVr], ); final supportedStreams = manifest.audioOnly.sortByBitrate(); final audioStream = quality == 'high' ? supportedStreams.firstOrNull : supportedStreams.lastOrNull; if (audioStream == null) { throw Exception('No audio stream available for this video.'); } start ??= 0; end ??= (audioStream.isThrottled ? (end ?? (start + 10379935)) : audioStream.size.totalBytes); if (end > audioStream.size.totalBytes) { end = audioStream.size.totalBytes; } final stream = ytExplode.videos.streams.get(audioStream, start, end); return StreamAudioResponse( sourceLength: audioStream.size.totalBytes, contentLength: end - start, offset: start, stream: stream, contentType: audioStream.codec.mimeType, ); } catch (e) { throw Exception('Failed to load audio: $e'); } } } final YoutubeExplode ytExplode = YoutubeExplode(); /// Starts a generic HTTP server that listens for requests to stream YouTube audio. /// /// Clients must pass 'id' and 'quality' as URL query parameters. /// The server binds to a random available port. /// Returns the base URL for the streaming endpoint. Future createAudioStreamServer() async { // Bind to a random port (port 0) on the loopback interface. final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); // Listen for requests and dispatch them to the handler server.listen((HttpRequest request) { // Pass only the request object to the handler handleAudioRequest(request); }); // Construct the base streaming URL final host = server.address.host; final port = server.port; final url = 'http://$host:$port/audio'; debugPrint( 'Generic streaming server started on $url. Use ?id=...&quality=... to stream.', ); // You would typically return the server instance here too, if you needed // to close it later (e.g., return {'server': server, 'url': url}) return url; } // ---------------------------------------------------------------------------- // HANDLER FUNCTION (Modified to read parameters from the request URI) // ---------------------------------------------------------------------------- Future handleAudioRequest(HttpRequest request) async { final response = request.response; // 1. Check the path and extract parameters from URL query if (request.uri.path != '/audio') { response.statusCode = HttpStatus.notFound; response.write('404 Not Found'); await response.close(); return; } final queryParams = request.uri.queryParameters; final videoId = queryParams['id']; final quality = queryParams['quality'] ?? 'high'; // Default to 'high' if (videoId == null || videoId.isEmpty) { response.statusCode = HttpStatus.badRequest; response.write('Missing required query parameter: id'); await response.close(); return; } debugPrint('Processing request for video ID: $videoId (Quality: $quality)'); try { // 2. Get the Stream Manifest and select the audio stream final manifest = await ytExplode.videos.streamsClient.getManifest( videoId, requireWatchPage: true, ytClients: [YoutubeApiClient.androidVr], ); final supportedStreams = manifest.audioOnly.sortByBitrate(); final audioStreamInfo = quality == 'high' ? supportedStreams.firstOrNull : supportedStreams.lastOrNull; if (audioStreamInfo == null) { response.statusCode = HttpStatus.internalServerError; response.write('No audio stream available for video $videoId.'); await response.close(); return; } final totalLength = audioStreamInfo.size.totalBytes; // 3. Parse the client's 'Range' header (same logic) (int start, int end)? parseRange(String rangeHeader, int totalLength) { // Expected format: "bytes=start-end" or "bytes=start-" if (!rangeHeader.startsWith('bytes=')) return null; final parts = rangeHeader.substring(6).split('-'); if (parts.length != 2) return null; final startStr = parts[0]; final endStr = parts[1]; final start = int.tryParse(startStr) ?? 0; // If end is missing (e.g., "bytes=1000-"), it means until the end of the file final end = endStr.isEmpty ? totalLength - 1 : int.tryParse(endStr); if (end == null || end >= totalLength || start >= totalLength || start > end) { return null; // Invalid range or past the end of the file } return (start, end); } int start = 0; int end = totalLength - 1; bool isPartial = false; final rangeHeader = request.headers.value(HttpHeaders.rangeHeader); if (rangeHeader != null) { final range = parseRange(rangeHeader, totalLength); if (range != null) { start = range.$1; end = range.$2; isPartial = true; } } // 4. Get the *actual* byte stream from YouTube final stream = ytExplode.videos.streamsClient.get( audioStreamInfo, start, end + 1, ); // 5. Set the HTTP headers and pipe the stream (same logic) final contentLength = end - start + 1; final mimeType = audioStreamInfo.codec.mimeType; response.statusCode = isPartial ? HttpStatus.partialContent : HttpStatus.ok; response.headers.set(HttpHeaders.acceptRangesHeader, 'bytes'); response.headers.contentType = ContentType.parse(mimeType); response.headers.contentLength = contentLength; if (isPartial) { response.headers.set( HttpHeaders.contentRangeHeader, 'bytes $start-$end/$totalLength', ); } response.bufferOutput = false; await response.addStream(stream); await response.close(); debugPrint( '[$videoId] Served ${isPartial ? 'partial' : 'full'} stream: bytes $start-$end', ); } catch (e) { debugPrint('Error serving audio for ID $videoId: $e'); } } ================================================ FILE: lib/themes/colors.dart ================================================ import 'package:flutter/material.dart'; Color greyColor = Colors.grey.withAlpha(100); Color darkGreyColor = Colors.grey.withAlpha(70); const MaterialColor primaryBlack = MaterialColor( 0xFF000000, { 50: Color.fromRGBO(0, 0, 0, .1), 100: Color.fromRGBO(0, 0, 0, .2), 200: Color.fromRGBO(0, 0, 0, .3), 300: Color.fromRGBO(0, 0, 0, .4), 400: Color.fromRGBO(0, 0, 0, .5), 500: Color.fromRGBO(0, 0, 0, .6), 600: Color.fromRGBO(0, 0, 0, .7), 700: Color.fromRGBO(0, 0, 0, .8), 800: Color.fromRGBO(0, 0, 0, .9), 900: Color.fromRGBO(0, 0, 0, 1), }, ); const MaterialColor primaryWhite = MaterialColor( 0xFFFFFFFF, { 50: Color.fromRGBO(255, 255, 255, .1), 100: Color.fromRGBO(255, 255, 255, .2), 200: Color.fromRGBO(255, 255, 255, .3), 300: Color.fromRGBO(255, 255, 255, .4), 400: Color.fromRGBO(255, 255, 255, .5), 500: Color.fromRGBO(255, 255, 255, .6), 600: Color.fromRGBO(255, 255, 255, .7), 700: Color.fromRGBO(255, 255, 255, .8), 800: Color.fromRGBO(255, 255, 255, .9), 900: Color.fromRGBO(255, 255, 255, 1), }, ); ================================================ FILE: lib/themes/dark.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; // ColorScheme.fromSeed( // seedColor: accentColor, // brightness: darkScheme.brightness, // primary: accentColor, // primaryContainer: accentColor, // onPrimaryContainer: Colors.black, // surface: accentColor.withAlpha(10), // ) final defaultFontStyle = GoogleFonts.poppins(); ColorScheme darkScheme = const ColorScheme.dark(); ThemeData darkTheme({required ColorScheme colorScheme}) { return ThemeData.dark().copyWith( colorScheme: colorScheme, scaffoldBackgroundColor: Platform.isWindows ? Colors.transparent : colorScheme.surface, primaryColor: colorScheme.primary, appBarTheme: AppBarTheme( centerTitle: true, backgroundColor: Colors.transparent, surfaceTintColor: Platform.isWindows ? Colors.transparent : null, systemOverlayStyle: const SystemUiOverlayStyle( statusBarBrightness: Brightness.dark, statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, ), ), textTheme: TextTheme( headlineLarge: defaultFontStyle.copyWith(color: Colors.white), headlineMedium: defaultFontStyle.copyWith(color: Colors.white), headlineSmall: defaultFontStyle.copyWith(color: Colors.white), bodyLarge: defaultFontStyle.copyWith(color: Colors.white), bodyMedium: defaultFontStyle.copyWith(color: Colors.white), bodySmall: defaultFontStyle.copyWith(color: Colors.white), displayLarge: defaultFontStyle.copyWith(color: Colors.white), displayMedium: defaultFontStyle.copyWith(color: Colors.white), displaySmall: defaultFontStyle.copyWith(color: Colors.white), titleLarge: defaultFontStyle.copyWith(color: Colors.white), titleMedium: defaultFontStyle.copyWith(color: Colors.white), titleSmall: defaultFontStyle.copyWith(color: Colors.white), labelLarge: defaultFontStyle.copyWith(color: Colors.white), labelMedium: defaultFontStyle.copyWith(color: Colors.white), labelSmall: defaultFontStyle.copyWith(color: Colors.white), ), ); } ================================================ FILE: lib/themes/light.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; final defaultFontStyle = GoogleFonts.poppins(); ThemeData lightTheme({required ColorScheme colorScheme}) { return ThemeData.light().copyWith( colorScheme: colorScheme, primaryColor: colorScheme.primary, scaffoldBackgroundColor: colorScheme.surface, appBarTheme: AppBarTheme( backgroundColor: Colors.transparent, centerTitle: true, systemOverlayStyle: const SystemUiOverlayStyle( statusBarBrightness: Brightness.light, statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Colors.transparent, ), ), textTheme: TextTheme( headlineLarge: defaultFontStyle.copyWith(color: Colors.black), headlineMedium: defaultFontStyle.copyWith(color: Colors.black), headlineSmall: defaultFontStyle.copyWith(color: Colors.black), bodyLarge: defaultFontStyle.copyWith(color: Colors.black), bodyMedium: defaultFontStyle.copyWith(color: Colors.black), bodySmall: defaultFontStyle.copyWith(color: Colors.black), displayLarge: defaultFontStyle.copyWith(color: Colors.black), displayMedium: defaultFontStyle.copyWith(color: Colors.black), displaySmall: defaultFontStyle.copyWith(color: Colors.black), titleLarge: defaultFontStyle.copyWith(color: Colors.black), titleMedium: defaultFontStyle.copyWith(color: Colors.black), titleSmall: defaultFontStyle.copyWith(color: Colors.black), labelLarge: defaultFontStyle.copyWith(color: Colors.black), labelMedium: defaultFontStyle.copyWith(color: Colors.black), labelSmall: defaultFontStyle.copyWith(color: Colors.black), ), ); } ================================================ FILE: lib/themes/text_styles.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; final defaultFontStyle = GoogleFonts.poppins(); TextStyle bigTextStyle(BuildContext context, {double opacity = 1, bool bold = true}) { return defaultFontStyle.copyWith( fontSize: 30, fontWeight: bold ? FontWeight.w900 : FontWeight.normal, color: Theme.of(context).textTheme.bodyMedium?.color, ); } TextStyle mediumTextStyle(BuildContext context, {double opacity = 1, bool bold = true}) { return defaultFontStyle.copyWith( fontSize: 24, fontWeight: bold ? FontWeight.w900 : FontWeight.normal, color: Theme.of(context).textTheme.bodyMedium?.color, ); } TextStyle textStyle(BuildContext context, {double opacity = 1, bool bold = true}) { return defaultFontStyle.copyWith( fontSize: 19, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: Theme.of(context).textTheme.bodyMedium?.color, ); } TextStyle subtitleTextStyle(BuildContext context, {double opacity = 1, bool bold = false}) { return defaultFontStyle.copyWith( fontSize: 15, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: Colors.grey.withAlpha(200), ); } TextStyle smallTextStyle(BuildContext context, {double opacity = 1, bool bold = false}) { return defaultFontStyle.copyWith( fontSize: 13, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: Theme.of(context).textTheme.bodyMedium?.color, ); } TextStyle tinyTextStyle(BuildContext context, {double opacity = 1, bool bold = false}) { return defaultFontStyle.copyWith( fontSize: 11, fontWeight: bold ? FontWeight.w600 : FontWeight.normal, color: Theme.of(context).textTheme.bodyMedium?.color, ); } TextStyle customTextStyle(BuildContext context, {double opacity = 1, bool bold = false, double? fontSize}) { return defaultFontStyle.copyWith( fontSize: fontSize, fontWeight: bold ? FontWeight.w600 : FontWeight.normal); } ================================================ FILE: lib/themes/theme.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'typography.dart'; class AppTheme { static ThemeData light({Color? primary}) { final colorScheme = ColorScheme.fromSeed( seedColor: primary??Colors.red, brightness: Brightness.light, ); return ThemeData.light(useMaterial3: true).copyWith( scaffoldBackgroundColor: colorScheme.surface, textTheme: appTextTheme(ThemeData.light().textTheme), colorScheme: colorScheme, visualDensity: VisualDensity.adaptivePlatformDensity, appBarTheme: AppBarTheme( systemOverlayStyle: const SystemUiOverlayStyle( statusBarBrightness: Brightness.light, statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Colors.transparent, ), ), pageTransitionsTheme: PageTransitionsTheme( builders: Map.fromIterable( TargetPlatform.values, value: (_) => const FadeForwardsPageTransitionsBuilder(), ), ), ); } static ThemeData dark({Color? primary, bool isPureBlack = false}) { final colorScheme = ColorScheme.fromSeed( seedColor: primary??Colors.deepPurpleAccent, brightness: Brightness.dark, ); return ThemeData.dark(useMaterial3: true).copyWith( scaffoldBackgroundColor: isPureBlack ? Colors.black : colorScheme.surface, textTheme: appTextTheme(ThemeData.dark().textTheme), colorScheme: colorScheme, visualDensity: VisualDensity.adaptivePlatformDensity, appBarTheme: AppBarTheme( backgroundColor: isPureBlack ? Colors.black : null, surfaceTintColor: isPureBlack ? Colors.black : null, systemOverlayStyle: const SystemUiOverlayStyle( statusBarBrightness: Brightness.dark, statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, ), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: isPureBlack ? Colors.black : null, ), pageTransitionsTheme: PageTransitionsTheme( builders: Map.fromIterable( TargetPlatform.values, value: (_) => const FadeForwardsPageTransitionsBuilder(), ), ), ); } } ================================================ FILE: lib/themes/typography.dart ================================================ import 'package:flutter/material.dart'; TextTheme appTextTheme(TextTheme? textTheme) { textTheme ??= ThemeData.light().textTheme; return TextTheme( displayLarge: TextStyle( color: textTheme.displayLarge?.color, fontWeight: FontWeight.normal, fontSize: 57, height: 64 / 57, letterSpacing: -0.25, ), displayMedium: TextStyle( color: textTheme.displayMedium?.color, fontWeight: FontWeight.normal, fontSize: 45, height: 52 / 45, letterSpacing: 0, ), displaySmall: TextStyle( color: textTheme.displaySmall?.color, fontWeight: FontWeight.normal, fontSize: 36, height: 44 / 36, letterSpacing: 0, ), headlineLarge: TextStyle( color: textTheme.headlineLarge?.color, fontWeight: FontWeight.normal, fontSize: 32, height: 40 / 32, letterSpacing: 0, ), headlineMedium: TextStyle( color: textTheme.headlineMedium?.color, fontWeight: FontWeight.normal, fontSize: 28, height: 36 / 28, letterSpacing: 0, ), headlineSmall: TextStyle( color: textTheme.headlineSmall?.color, fontWeight: FontWeight.normal, fontSize: 24, height: 32 / 24, letterSpacing: 0, ), titleLarge: TextStyle( color: textTheme.titleLarge?.color, fontWeight: FontWeight.normal, // M3 uses normal, M2 used w500 fontSize: 22, height: 28 / 22, letterSpacing: 0, ), titleMedium: TextStyle( color: textTheme.titleMedium?.color, fontWeight: FontWeight.w500, fontSize: 16, height: 24 / 16, letterSpacing: 0.15, ), titleSmall: TextStyle( color: textTheme.titleSmall?.color, fontWeight: FontWeight.w500, fontSize: 14, height: 20 / 14, letterSpacing: 0.1, ), bodyLarge: TextStyle( color: textTheme.bodyLarge?.color, fontWeight: FontWeight.normal, fontSize: 16, height: 24 / 16, letterSpacing: 0.5, // M3 uses 0.5, M2 used 0.15 ), bodyMedium: TextStyle( color: textTheme.bodyMedium?.color, fontWeight: FontWeight.normal, fontSize: 14, height: 20 / 14, letterSpacing: 0.25, ), bodySmall: TextStyle( color: textTheme.bodySmall?.color, fontWeight: FontWeight.normal, fontSize: 12, height: 16 / 12, letterSpacing: 0.4, ), labelLarge: TextStyle( color: textTheme.labelLarge?.color?.withAlpha(200), fontWeight: FontWeight.w600, fontSize: 14, height: 20 / 14, letterSpacing: 0.1, ), labelMedium: TextStyle( color: textTheme.labelMedium?.color?.withAlpha(200), fontWeight: FontWeight.w500, fontSize: 12, height: 16 / 12, letterSpacing: 0.5, ), labelSmall: TextStyle( color: textTheme.labelSmall?.color?.withAlpha(200), fontWeight: FontWeight.w500, fontSize: 11, height: 16 / 11, letterSpacing: 0.5, ), ); } ================================================ FILE: lib/utils/adaptive_widgets/adaptive_widgets.dart ================================================ export 'appbar.dart'; export 'buttons.dart'; export 'card.dart'; export 'dropdown_button.dart'; export 'icons.dart'; export 'inkwell.dart'; export 'listtile.dart'; export 'progress_ring.dart'; export 'scaffold.dart'; export 'slider.dart'; export 'switch.dart'; export 'text_field.dart'; export 'theme.dart'; ================================================ FILE: lib/utils/adaptive_widgets/appbar.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; class AdaptiveAppBar extends StatelessWidget implements PreferredSizeWidget { const AdaptiveAppBar({ super.key, this.leading, this.title, this.centerTitle, this.automaticallyImplyLeading = true, this.bottom, this.actions, }); ///The widget displayed before the [title] /// ///Usually an [Icon] widget. final Widget? leading; /// The title of this [AdaptiveAppBar]. final Widget? title; /// Whether the title should be centered. /// /// Works only on android. final bool? centerTitle; /// Controls whether we should try to imply the leading widget if null. final bool automaticallyImplyLeading; /// This widget appears across the bottom of the [AdaptiveAppBar]. final PreferredSizeWidget? bottom; /// A list of Widgets to display in a row after the [title] widget. final List? actions; @override Widget build(BuildContext context) { return AppBar( leading: leading, title: title, centerTitle: centerTitle, automaticallyImplyLeading: automaticallyImplyLeading, bottom: bottom, actions: actions, ); } @override Size get preferredSize { if (Platform.isWindows) { return Size.fromHeight(50.0 + (bottom == null ? 0 : kTextTabBarHeight)); } else { return Size.fromHeight( kToolbarHeight + (bottom == null ? 0 : kTextTabBarHeight)); } } } ================================================ FILE: lib/utils/adaptive_widgets/buttons.dart ================================================ import 'package:flutter/material.dart'; import 'icons.dart'; class AdaptiveButton extends StatelessWidget { final Widget child; final void Function()? onPressed; final Color? color; const AdaptiveButton( {super.key, required this.child, required this.onPressed, this.color}); @override Widget build(BuildContext context) { return TextButton( key: key, onPressed: onPressed, child: child, ); } } class AdaptiveFilledButton extends StatelessWidget { final Widget child; final void Function()? onPressed; final Color? color; final OutlinedBorder? shape; final EdgeInsetsGeometry? padding; const AdaptiveFilledButton({ super.key, required this.child, required this.onPressed, this.color, this.shape, this.padding, }); @override Widget build(BuildContext context) { return FilledButton( key: key, onPressed: onPressed, style: ButtonStyle( backgroundColor: WidgetStateProperty.all(color), shape: WidgetStateProperty.all(shape), padding: WidgetStateProperty.all(padding), ), child: child, ); } } class AdaptiveOutlinedButton extends StatelessWidget { final Widget child; final void Function()? onPressed; final Color? color; const AdaptiveOutlinedButton( {super.key, required this.child, required this.onPressed, this.color}); @override Widget build(BuildContext context) { return OutlinedButton( key: key, onPressed: onPressed, style: ButtonStyle( backgroundColor: color != null ? WidgetStateProperty.all(color) : null, foregroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.primary)), child: child, ); } } class AdaptiveIconButton extends StatelessWidget { const AdaptiveIconButton({ super.key, required this.icon, required this.onPressed, this.isSelected, this.color, }); final Widget icon; final void Function()? onPressed; final bool? isSelected; final Color? color; @override Widget build(BuildContext context) { return IconButton( key: key, icon: icon, onPressed: onPressed, isSelected: isSelected, color: color, ); } } class AdaptiveBackButton extends StatelessWidget { const AdaptiveBackButton({super.key}); @override Widget build(BuildContext context) { return AdaptiveIconButton( icon: Icon(AdaptiveIcons.back), onPressed: () => Navigator.of(context).maybePop(), ); } } ================================================ FILE: lib/utils/adaptive_widgets/card.dart ================================================ import 'package:flutter/material.dart'; class Adaptivecard extends StatelessWidget { const Adaptivecard({ super.key, required this.child, this.backgroundColor, this.borderRadius, this.margin, this.padding, this.elevation, }); final Widget child; final Color? backgroundColor; final BorderRadius? borderRadius; final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? padding; final double? elevation; @override Widget build(BuildContext context) { return Card( margin: margin ?? const EdgeInsets.all(1), color: backgroundColor, elevation: elevation, shape: RoundedRectangleBorder( borderRadius: borderRadius ?? const BorderRadius.all(Radius.circular(8.0))), child: Padding( padding: padding ?? const EdgeInsets.all(12.0), child: child, ), ); } } ================================================ FILE: lib/utils/adaptive_widgets/dropdown_button.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveDropdownButton extends StatelessWidget { final T? value; final List>? items; final TextStyle? style; final void Function(T?)? onChanged; const AdaptiveDropdownButton({ super.key, this.value, this.items, this.style, this.onChanged, }); @override Widget build(BuildContext context) { return DropdownButton( style: style, underline: const SizedBox(), value: value, isDense: true, borderRadius: BorderRadius.circular(8), alignment: AlignmentDirectional.centerEnd, items: items ?.map( (item) => DropdownMenuItem( value: item.value, enabled: item.enabled, onTap: item.onTap, child: item.child, ), ) .toList(), onChanged: onChanged, ); } } class AdaptiveDropdownMenuItem { final Widget child; final T? value; final bool enabled; final void Function()? onTap; AdaptiveDropdownMenuItem( {required this.child, this.value, this.enabled = true, this.onTap}); } ================================================ FILE: lib/utils/adaptive_widgets/icons.dart ================================================ // ignore_for_file: non_constant_identifier_names import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; abstract final class AdaptiveIcons { static IconData home = Icons.home; static IconData home_filled = Icons.home_filled; static IconData back = Icons.arrow_back; static IconData chevron_left = Icons.chevron_left; static IconData chevron_right = Icons.chevron_right; static IconData chevron_down = CupertinoIcons.chevron_down; static IconData search = Icons.search; static IconData download = Icons.download; static IconData downloading = Icons.downloading; static IconData sync = Icons.sync; static IconData create = Icons.edit; static IconData add = Icons.add; static IconData import = Icons.import_export_outlined; static IconData heart = CupertinoIcons.heart; static IconData heart_fill = CupertinoIcons.heart_fill; static IconData lyrics = Icons.lyrics; static IconData queue = Icons.queue_music; static IconData play = Icons.play_arrow; static IconData pause = Icons.pause; static IconData skip_previous = Icons.skip_previous; static IconData skip_next = Icons.skip_next; static IconData repeat = Icons.repeat; static IconData repeat_one = Icons.repeat_one; static IconData repeat_all = Icons.repeat; static IconData more_vertical = Icons.more_vert; static IconData delete = Icons.delete; static IconData volume(double range) { if (range == 0) { return Icons.volume_off; } else if (range < .2) { return Icons.volume_down; } else if (range < .6) { return Icons.volume_down; } return Icons.volume_up; } static IconData library_add = Icons.library_add; static IconData library_add_check = Icons.library_add_check; static IconData playlist_play = Icons.playlist_play; static IconData queue_add = Icons.queue; static IconData radio = Icons.radar; static IconData person = Icons.person; static IconData people = Icons.people; static IconData album = Icons.album; static IconData equalizer = Icons.equalizer; static IconData timer = Icons.timer; static IconData share = Icons.share; static IconData wifi_off_rounded = Icons.wifi_off_rounded; } ================================================ FILE: lib/utils/adaptive_widgets/inkwell.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'no_splash_factory.dart'; class AdaptiveInkWell extends StatelessWidget { final Widget? child; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final bool enabled; final GestureTapCallback? onTap; final GestureLongPressCallback? onLongPress; final GestureLongPressCallback? onDoubleTap; final GestureLongPressCallback? onSecondaryTap; final BorderRadius? borderRadius; final bool selected; const AdaptiveInkWell({ super.key, this.child, this.padding, this.margin, this.enabled = true, this.onTap, this.onLongPress, this.onDoubleTap, this.onSecondaryTap, this.borderRadius, this.selected = false, }); @override Widget build(BuildContext context) { return Container( margin: margin, child: Material( color: Colors.transparent, child: InkWell( onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, onDoubleTap: enabled ? onDoubleTap : null, onSecondaryTap: enabled ? onSecondaryTap : null, borderRadius: borderRadius ?? BorderRadius.circular(4), splashFactory: (Platform.isWindows) ? const NoSplashFactory() : null, child: Padding( padding: padding ?? const EdgeInsets.all(0), child: child, ), ), ), ); } } ================================================ FILE: lib/utils/adaptive_widgets/listtile.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import '../../themes/text_styles.dart'; import 'no_splash_factory.dart'; class AdaptiveListTile extends StatelessWidget { final Widget? leading; final Widget? title; final Widget? subtitle; final Widget? trailing; final Widget? description; final bool isThreeLine; final bool dense; final EdgeInsetsGeometry? contentPadding; final EdgeInsetsGeometry? margin; final bool enabled; final GestureTapCallback? onTap; final GestureLongPressCallback? onLongPress; final GestureLongPressCallback? onDoubleTap; final GestureLongPressCallback? onSecondaryTap; final bool selected; final Color? backgroundColor; const AdaptiveListTile({ super.key, this.leading, this.title, this.subtitle, this.trailing, this.description, this.isThreeLine = false, this.dense = false, this.contentPadding, this.margin, this.enabled = true, this.onTap, this.onLongPress, this.onDoubleTap, this.onSecondaryTap, this.selected = false, this.backgroundColor, }); @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final TextStyle titleStyle = mediumTextStyle(context, bold: false).copyWith( fontSize: dense ? 14.0 : 16.0, ); final TextStyle subtitleStyle = subtitleTextStyle(context).copyWith( fontSize: dense ? 12.0 : 14.0, ); final TextStyle descriptionStyle = smallTextStyle(context).copyWith( fontSize: dense ? 12.0 : 14.0, ); return Container( margin: margin, child: Material( color: Colors.transparent, child: InkWell( onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, onDoubleTap: enabled ? onDoubleTap : null, onSecondaryTap: enabled ? onSecondaryTap : null, borderRadius: BorderRadius.circular(4), splashFactory: (Platform.isWindows) ? const NoSplashFactory() : null, child: ClipRRect( borderRadius: BorderRadius.circular(4), child: Container( padding: contentPadding ?? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), decoration: BoxDecoration( color: selected ? theme.colorScheme.primary.withValues(alpha:0.1) : backgroundColor, ), child: Column( children: [ Row( children: [ if (leading != null) ...[ IconTheme( data: Theme.of(context) .iconTheme .copyWith(size: dense ? 24 : 28), child: leading!), const SizedBox(width: 16.0), ], Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) ...[ DefaultTextStyle( style: titleStyle, child: title!, ), if (subtitle != null || isThreeLine) SizedBox(height: dense ? 2.0 : 4.0), ], if (subtitle != null || isThreeLine) ...[ DefaultTextStyle( style: subtitleStyle, child: subtitle ?? Container(), ), ], ], ), ), if (trailing != null) ...[ const SizedBox(width: 16.0), trailing!, ], ], ), if (subtitle != null || isThreeLine) SizedBox(height: dense ? 2.0 : 4.0), if (description != null) ...[ DefaultTextStyle( style: descriptionStyle, child: description!, ), ], if (description != null) const Divider(), ], ), ), ), ), ), ); } } ================================================ FILE: lib/utils/adaptive_widgets/no_splash_factory.dart ================================================ import 'package:flutter/material.dart'; class NoSplashFactory extends InteractiveInkFeatureFactory { const NoSplashFactory(); @override InteractiveInkFeature create({ required MaterialInkController controller, required RenderBox referenceBox, required Offset position, required Color color, required TextDirection textDirection, bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, ShapeBorder? customBorder, double? radius, onRemoved, }) { return NoSplash( controller: controller, referenceBox: referenceBox, color: color, ); } } class NoSplash extends InteractiveInkFeature { @override final Color color; // Color for onTap effect NoSplash({ required super.controller, required super.referenceBox, required this.color, }) : super(color: color); @override void paintFeature(Canvas canvas, Matrix4 transform) { final Paint paint = Paint() ..color = color.withAlpha(50); // Adjust opacity as needed canvas.drawRect(Offset.zero & referenceBox.size, paint); } } ================================================ FILE: lib/utils/adaptive_widgets/progress_ring.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveProgressRing extends StatelessWidget { final double? value; final double strokeWidth; final Color? backgroundColor; final Color? color; /// Creates progress ring. /// /// [value], if non-null, must be in the range of 0 to 100 /// /// [strokeWidth] must be equal or greater than 0 const AdaptiveProgressRing({ super.key, this.value, this.strokeWidth = 4.5, this.backgroundColor, this.color, }); @override Widget build(BuildContext context) { return CircularProgressIndicator( value: value, strokeWidth: strokeWidth, backgroundColor: backgroundColor, color: color, ); } } ================================================ FILE: lib/utils/adaptive_widgets/scaffold.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveScaffold extends StatelessWidget { /// Creates a new [AdaptiveScaffold]. const AdaptiveScaffold({this.body, this.appBar, super.key}); /// The primary content of the [AdaptiveScaffold]. final Widget? body; /// An app bar to display at the top of the [AdaptiveScaffold]. final PreferredSizeWidget? appBar; @override Widget build(BuildContext context) { return Scaffold( key: key, appBar: appBar, body: body, ); } } ================================================ FILE: lib/utils/adaptive_widgets/slider.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveSlider extends StatelessWidget { final double value; final bool disabled; final double min; final double max; final int? divisions; final String? label; final bool vertical; final void Function(double)? onChanged; const AdaptiveSlider( {required this.value, this.disabled = false, this.onChanged, this.min = 0, this.max = 1, this.divisions, this.label, this.vertical = false, super.key}); @override Widget build(BuildContext context) { return RotatedBox( quarterTurns: vertical ? 3 : 0, child: Slider( value: value, min: min, max: max, label: label, divisions: divisions, onChanged: disabled ? null : onChanged, ), ); } } ================================================ FILE: lib/utils/adaptive_widgets/switch.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveSwitch extends StatelessWidget { final bool value; final void Function(bool)? onChanged; const AdaptiveSwitch({super.key, required this.value, this.onChanged}); @override Widget build(BuildContext context) { return Switch(value: value, onChanged: onChanged); } } ================================================ FILE: lib/utils/adaptive_widgets/text_field.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveTextField extends StatelessWidget { final TextEditingController? controller; final void Function(String)? onChanged; final void Function(String)? onSubmitted; final void Function()? onTap; final FocusNode? focusNode; final bool readOnly; final Color? fillColor; final EdgeInsetsGeometry? contentPadding; final TextInputType? keyboardType; final String? hintText; final Widget? prefix; final Widget? suffix; final bool autofocus; final int? maxLines; final TextInputAction? textInputAction; final BorderRadius borderRadius; final double borderWidth; const AdaptiveTextField({ super.key, this.controller, this.onTap, this.contentPadding, this.fillColor, this.focusNode, this.hintText, this.keyboardType, this.onChanged, this.onSubmitted, this.prefix, this.suffix, this.textInputAction, this.readOnly = false, this.autofocus = false, this.maxLines = 1, this.borderRadius = const BorderRadius.all(Radius.circular(4.0)), this.borderWidth = 0, }); @override Widget build(BuildContext context) { return TextField( key: key, controller: controller, onChanged: onChanged, onSubmitted: onSubmitted, onTap: onTap, focusNode: focusNode, readOnly: readOnly, keyboardType: keyboardType, autofocus: autofocus, maxLines: maxLines, textInputAction: textInputAction, decoration: InputDecoration( fillColor: fillColor, filled: fillColor != null, contentPadding: contentPadding, hintText: hintText, prefixIcon: prefix, suffixIcon: suffix, border: OutlineInputBorder( borderSide: borderWidth > 0 ? BorderSide(width: borderWidth) : BorderSide.none, borderRadius: borderRadius, ), ), ); } } ================================================ FILE: lib/utils/adaptive_widgets/theme.dart ================================================ import 'package:flutter/material.dart'; class AdaptiveTheme { static ThemeData getMaterialTheme(BuildContext context) { return Theme.of(context); } static AdaptiveThemeData of(BuildContext context) { AdaptiveThemeData adaptiveThemeData; final materialTheme = Theme.of(context); adaptiveThemeData = AdaptiveThemeData( primaryColor: materialTheme.colorScheme.primary, inactiveBackgroundColor: materialTheme.scaffoldBackgroundColor, ); return adaptiveThemeData; } } class AdaptiveThemeData { Color primaryColor; Color inactiveBackgroundColor; AdaptiveThemeData( {required this.primaryColor, required this.inactiveBackgroundColor}); } ================================================ FILE: lib/utils/add_history.dart ================================================ import 'package:get_it/get_it.dart'; import 'package:yt_music/ytmusic.dart'; import '../services/download_manager.dart'; import '../services/history_manager.dart'; import '../services/settings_manager.dart'; Future addHistory(Map song) async { await GetIt.I().songs.add(song); final downloadSong = GetIt.I().getDownload(song['videoId']); if (GetIt.I().personalisedContent && (downloadSong == null || downloadSong['status'] != 'DOWNLOADED')) { GetIt.I().addYoutubeHistory(song['videoId']); } } ================================================ FILE: lib/utils/bottom_modals.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:duration_picker/duration_picker.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/screens/settings/player/equalizer/equalizer_page.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'playlist_icon.dart'; import 'playlist_icons.dart'; import 'playlist_icon_widget.dart'; import '../generated/l10n.dart'; import '../screens/settings/widgets/color_icon.dart'; import '../services/bottom_message.dart'; import '../services/download_manager.dart'; import '../services/favourites_manager.dart'; import '../services/library.dart'; import '../services/media_player.dart'; import '../services/settings_manager.dart'; import '../utils/text_controller_builder.dart'; import '../utils/playlist_thumbnail.dart'; import '../themes/colors.dart'; import '../themes/text_styles.dart'; import 'adaptive_widgets/adaptive_widgets.dart'; import 'format_duration.dart'; import '../utils/extensions.dart'; class Modals { static Future showCenterLoadingModal(BuildContext context, {String? title}) { return showDialog( context: context, useRootNavigator: false, builder: (context) { return AlertDialog( title: Text(title ?? S.of(context).Progress), content: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()], ), ); }, ); } // static Future showUpdateDialog( // BuildContext context, UpdateInfo? updateInfo) => // showDialog( // context: context, // useRootNavigator: false, // builder: (context) { // return _updateDialog(context, updateInfo); // }, // ); static Future showTextField( BuildContext context, { String? title, String? hintText, String? doneText, }) { return showModalBottomSheet( context: context, useRootNavigator: false, backgroundColor: Colors.transparent, isScrollControlled: true, useSafeArea: true, builder: (context) => _textFieldBottomModal( context, title: title, hintText: hintText, doneText: doneText, ), ); } static Future showSelection( BuildContext context, List items, ) { return showModalBottomSheet( context: context, useRootNavigator: false, backgroundColor: Colors.transparent, isScrollControlled: true, useSafeArea: true, builder: (context) => _showSelection(context, items), ); } static void showSongBottomModal(BuildContext context, Map song) { showModalBottomSheet( context: context, useRootNavigator: false, backgroundColor: Colors.transparent, isScrollControlled: true, useSafeArea: true, builder: (context) => _songBottomModal(context, song), ); } static void showPlayerOptionsModal(BuildContext context, Map song) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, context: context, useSafeArea: true, isScrollControlled: true, builder: (context) => _playerOptionsModal(context, song), ); } static void showPlaylistBottomModal(BuildContext context, Map playlist) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _playlistBottomModal(context, playlist), ); } static void showFavouritesBottomModal(BuildContext context, Map playlist) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _favouritesBottomModal(context, playlist), ); } static void showDownloadBottomModal(BuildContext context) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _downloadBottomModal(context), ); } static void showDownloadDetailsBottomModal( BuildContext context, Map playlist, ) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _downloadDetailsBottomModal(context, playlist), ); } static Future showArtistsBottomModal( BuildContext context, List artists, { String? leading, bool shouldPop = false, }) { return showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _artistsBottomModal(context, artists, shouldPop: shouldPop), ); } static void showCreateplaylistModal(BuildContext context, {Map? item}) { PlaylistIcon selectedIcon = PlaylistIcons.musicNoteList; showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return _createPlaylistModal( context, item, selectedIcon, (icon) => setState(() => selectedIcon = icon), ); }, ); }, ); } static Future showSelectPlaylistIconModal( BuildContext context, { Map? item, }) async { return await showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _selectPlaylistIconModal(context), ); } static void showImportplaylistModal(BuildContext context, {Map? item}) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _importPlaylistModal(context), ); } static void showEditPlaylistBottomModal( BuildContext context, { required String playlistId, required String iconId, String? name, }) { PlaylistIcon selectedIcon = PlaylistIcons.byId(iconId); showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return _editPlaylistBottomModal( context, playlistId: playlistId, name: name, selectedIcon: selectedIcon, onIconChanged: (icon) => setState(() => selectedIcon = icon), ); }, ); }, ); } static void addToPlaylist(BuildContext context, Map item) { showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _addToPlaylist(context, item), ); } static Future showConfirmBottomModal( BuildContext context, { required String message, bool isDanger = false, String? doneText, String? cancelText, }) async { return await showModalBottomSheet( useRootNavigator: false, backgroundColor: Colors.transparent, useSafeArea: true, isScrollControlled: true, context: context, builder: (context) => _confirmBottomModal( context, message: message, isDanger: isDanger, doneText: doneText, cancelText: cancelText, ), ) ?? false; } static void showAccentSelector(BuildContext context) { showModalBottomSheet( context: context, useRootNavigator: false, backgroundColor: Colors.transparent, isScrollControlled: true, useSafeArea: true, builder: (context) => _accentSelector(context), ); } } BottomModalLayout _confirmBottomModal( BuildContext context, { required String message, bool isDanger = false, String? doneText, String? cancelText, }) { return BottomModalLayout( title: Center( child: Text(S.of(context).Confirm, style: bigTextStyle(context)), ), actions: [ AdaptiveButton( color: Platform.isAndroid ? Theme.of(context).colorScheme.primary.withAlpha(30) : null, onPressed: () { Navigator.pop(context, false); }, child: Text(cancelText ?? S.of(context).No), ), const SizedBox(width: 16), AdaptiveFilledButton( onPressed: () { Navigator.pop(context, true); }, color: isDanger ? Colors.red : Theme.of(context).colorScheme.primary, child: Text( doneText ?? S.of(context).Yes, style: TextStyle(color: isDanger ? Colors.white : null), ), ), ], child: SingleChildScrollView( child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [Text(message, textAlign: TextAlign.center)], ), ), ), ); } Widget _editPlaylistBottomModal( BuildContext context, { String? name, required String playlistId, required PlaylistIcon selectedIcon, required Function(PlaylistIcon) onIconChanged, }) { return TextControllerBuilder( initialText: name, builder: (context, controller) { final bool blockInput = context.isKeyboardSpaceLimited; return BottomModalLayout( title: Center( child: Text( S.of(context).Edit_Playlist, style: mediumTextStyle(context), ), ), actions: [ AdaptiveFilledButton( onPressed: () async { String text = controller.text; context .read() .editPlaylist( playlistId: playlistId, iconId: selectedIcon.toId(), title: text.trim().isNotEmpty ? text : null, ) .then((String message) { if (context.mounted) { Navigator.pop(context); BottomMessage.showText(context, message); } }); }, child: Text(S.of(context).Edit), ), ], child: Row( spacing: 10, children: [ GestureDetector( onTap: () async { final icon = await Modals.showSelectPlaylistIconModal(context); if (icon != null) { onIconChanged(icon); } }, child: Stack( clipBehavior: Clip.none, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Theme.of(context).colorScheme.primaryContainer, ), child: PlaylistIconWidget(data: selectedIcon, size: 36), ), Positioned( right: -4, bottom: -4, child: CircleAvatar( radius: 10, backgroundColor: Theme.of(context).colorScheme.primary, child: Icon( Icons.edit, size: 15, color: Theme.of(context).colorScheme.onPrimary, ), ), ), ], ), ), Expanded( child: AdaptiveTextField( controller: controller, fillColor: Platform.isAndroid ? greyColor : null, hintText: S.of(context).Playlist_Name, readOnly: blockInput, onTap: () { if (blockInput) { FocusScope.of(context).unfocus(); BottomMessage.showText( context, S.of(context).Rotate_Device, ); } }, ), ), ], ), ); }, ); } BottomModalLayout _artistsBottomModal( BuildContext context, List artists, { bool shouldPop = false, }) { return BottomModalLayout( title: Center( child: Text(S.of(context).Artists, style: mediumTextStyle(context)), ), child: SingleChildScrollView( child: Column( children: [ ...artists.map( (artist) => AdaptiveListTile( dense: true, title: Text( artist['name'], maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Icon(AdaptiveIcons.person), trailing: Icon(AdaptiveIcons.chevron_right), onTap: () { if (shouldPop) { context.go( '/browse', extra: { 'endpoint': artist['endpoint'].cast(), }, ); } else { Navigator.pop(context); context.push( '/browse', extra: { 'endpoint': artist['endpoint'].cast(), }, ); } }, ), ), ], ), ), ); } Widget _createPlaylistModal( BuildContext context, Map? item, PlaylistIcon selectedIcon, Function(PlaylistIcon) onIconChanged, ) { return TextControllerBuilder( builder: (context, controller) { final bool blockInput = context.isKeyboardSpaceLimited; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: BottomModalLayout( title: Text( S.of(context).Create_Playlist, style: mediumTextStyle(context), ), actions: [ AdaptiveButton( onPressed: () => Navigator.pop(context), child: Text(S.of(context).Cancel), ), AdaptiveFilledButton( color: Theme.of(context).colorScheme.primary, onPressed: () async { final message = await context .read() .createPlaylist( controller.text, selectedIcon.toId(), item: item, ); if (!context.mounted) return; Navigator.pop(context); BottomMessage.showText(context, message); }, child: Text( S.of(context).Create, style: TextStyle( color: context.isDarkMode ? Colors.black : Colors.white, ), ), ), ], child: Row( spacing: 10, children: [ GestureDetector( onTap: () async { final icon = await Modals.showSelectPlaylistIconModal( context, ); if (icon != null) { onIconChanged(icon); } }, child: Stack( clipBehavior: Clip.none, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Theme.of(context).colorScheme.primaryContainer, ), child: PlaylistIconWidget(data: selectedIcon, size: 36), ), Positioned( right: -4, bottom: -4, child: CircleAvatar( radius: 10, backgroundColor: Theme.of(context).colorScheme.primary, child: Icon( Icons.edit, size: 15, color: Theme.of(context).colorScheme.onPrimary, ), ), ), ], ), ), Expanded( child: AdaptiveTextField( controller: controller, fillColor: Platform.isAndroid ? greyColor : null, hintText: S.of(context).Playlist_Name, readOnly: blockInput, onTap: () { if (blockInput) { FocusScope.of(context).unfocus(); BottomMessage.showText( context, S.of(context).Rotate_Device, ); } }, ), ), ], ), ), ); }, ); } Widget _selectPlaylistIconModal(BuildContext context) { return BottomModalLayout( title: Text( S.of(context).Select_Playlist_Icon, style: mediumTextStyle(context), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: GridView.builder( shrinkWrap: true, itemCount: PlaylistIcons.values.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, ), itemBuilder: (context, index) { final icon = PlaylistIcons.values[index]; return GestureDetector( onTap: () => Navigator.pop(context, icon), child: Center(child: PlaylistIconWidget(data: icon, size: 36)), ); }, ), ), ); } Widget _importPlaylistModal(BuildContext context) { final bool blockInput = context.isKeyboardSpaceLimited; return TextControllerBuilder( builder: (context, controller) { return BottomModalLayout( title: Center( child: Text( S.of(context).Import_Playlist, style: mediumTextStyle(context), ), ), actions: [ AdaptiveButton( onPressed: () async { Navigator.pop(context); }, child: Text(S.of(context).Cancel), ), AdaptiveFilledButton( color: Theme.of(context).colorScheme.primary, onPressed: () async { Modals.showCenterLoadingModal(context); String message = await GetIt.I().importPlaylist( controller.text, ); if (context.mounted) { Navigator.pop(context); Navigator.pop(context); BottomMessage.showText(context, message); } }, child: Text( S.of(context).Import, style: TextStyle( color: context.isDarkMode ? Colors.black : Colors.white, ), ), ), ], child: SingleChildScrollView( child: Column( children: [ Column( children: [ AdaptiveTextField( controller: controller, keyboardType: TextInputType.url, hintText: 'https://music.youtube.com/playlist?list=', prefix: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), child: Icon(Icons.title), ), fillColor: Platform.isWindows ? null : greyColor, contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 16, ), readOnly: blockInput, onTap: () { if (blockInput) { FocusScope.of(context).unfocus(); BottomMessage.showText( context, S.of(context).Rotate_Device, ); } }, ), ], ), ], ), ), ); }, ); } BottomModalLayout _addToPlaylist(BuildContext context, Map item) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text( S.of(context).Add_To_Playlist, style: mediumTextStyle(context), ), trailing: AdaptiveIconButton( onPressed: () { Navigator.pop(context); Modals.showCreateplaylistModal(context, item: item); }, icon: const Icon(Icons.playlist_add, size: 20), ), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...context.read().userPlaylists.map((key, playlist) { return MapEntry( key, playlist['songs'] .map((song) => song["videoId"]) .contains(item["videoId"]) ? const SizedBox.shrink() : AdaptiveListTile( dense: true, title: Text(playlist['title']), leading: playlist['isPredefined'] == true ? ClipRRect( borderRadius: BorderRadius.circular( playlist['type'] == 'ARTIST' ? 50 : 3, ), child: CachedNetworkImage( imageUrl: playlist['thumbnails'].first['url'] .replaceAll('w540-h225', 'w60-h60'), height: 50, width: 50, ), ) : Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: PlaylistIconWidget( data: PlaylistIcons.byId(playlist['iconId']), size: 30, ), ), onTap: () async { await context .read() .addToPlaylist(item: item, key: key) .then((String message) { if (context.mounted) { Navigator.pop(context); BottomMessage.showText(context, message); } }); }, ), ); }).values, ], ), ), ); } // SizedBox _updateDialog(BuildContext context, UpdateInfo? updateInfo) { // final f = DateFormat('MMMM dd, yyyy'); // return SizedBox( // height: MediaQuery.of(context).size.height, // width: MediaQuery.of(context).size.width, // child: LayoutBuilder(builder: (context, constraints) { // return AlertDialog( // icon: Center( // child: Container( // padding: const EdgeInsets.all(16), // decoration: BoxDecoration( // color: Colors.green.withAlpha(100), // borderRadius: BorderRadius.circular(16)), // child: const Icon( // Icons.update_outlined, // size: 70, // ), // ), // ), // scrollable: true, // title: Column( // children: [ // Text(updateInfo != null ? 'Update Available' : 'Update Info'), // if (updateInfo != null) // Text( // '${updateInfo.name}\n${f.format(DateTime.parse(updateInfo.publishedAt))}', // style: TextStyle(fontSize: 16, color: context.subtitleColor), // ) // ], // ), // content: updateInfo != null // ? SizedBox( // width: constraints.maxWidth, // height: constraints.maxHeight - 400, // child: Markdown( // data: updateInfo.body, // shrinkWrap: true, // softLineBreak: true, // onTapLink: (text, href, title) { // if (href != null) { // launchUrl(Uri.parse(href), // mode: LaunchMode.platformDefault); // } // }, // ), // ) // : const Center( // child: Text("You are already up to date."), // ), // actions: [ // if (updateInfo != null) // AdaptiveButton( // onPressed: () { // Navigator.pop(context); // }, // child: const Text('Cancel'), // ), // AdaptiveFilledButton( // onPressed: () { // Navigator.pop(context); // if (updateInfo != null) { // launchUrl(Uri.parse(updateInfo.downloadUrl), // mode: LaunchMode.externalApplication); // } // }, // child: Text(updateInfo != null ? 'Update' : 'Done'), // ), // ], // ); // }), // ); // } Widget _textFieldBottomModal( BuildContext context, { String? title, String? hintText, String? doneText, }) { final bool blockInput = context.isKeyboardSpaceLimited; return TextControllerBuilder( builder: (context, controller) { return BottomModalLayout( title: (title != null) ? Center(child: Text(title, style: mediumTextStyle(context))) : null, actions: [ AdaptiveFilledButton( onPressed: () async { Navigator.pop(context, controller.text); }, child: Text(doneText ?? S.of(context).Done), ), ], child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), child: Column( children: [ AdaptiveTextField( controller: controller, fillColor: greyColor, contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 16, ), hintText: hintText, prefix: const Icon(Icons.title), readOnly: blockInput, onTap: () { if (blockInput) { FocusScope.of(context).unfocus(); BottomMessage.showText( context, S.of(context).Rotate_Device, ); } }, ), ], ), ), ], ), ), ); }, ); } BottomModalLayout _playerOptionsModal(BuildContext context, Map song) { return BottomModalLayout( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Column( children: [ StreamBuilder( stream: GetIt.I().player.volumeStream, builder: (context, progress) { return AdaptiveListTile( dense: true, leading: Icon( AdaptiveIcons.volume( (progress.hasData && progress.data != null) ? progress.data! : GetIt.I().player.volume, ), ), title: AdaptiveSlider( label: (((progress.hasData && progress.data != null) ? progress.data! : GetIt.I().player.volume) * 100) .toStringAsFixed(1), value: (progress.hasData && progress.data != null) ? progress.data! : GetIt.I().player.volume, onChanged: (volume) { GetIt.I().player.setVolume(volume); }, ), ); }, ), StreamBuilder( stream: GetIt.I().player.speedStream, builder: (context, progress) { return AdaptiveListTile( dense: true, leading: const Icon(Icons.speed), title: AdaptiveSlider( max: 2, min: 0.25, divisions: 7, label: ((progress.hasData && progress.data != null) ? progress.data! : GetIt.I().player.speed) .toString(), value: (progress.hasData && progress.data != null) ? progress.data! : GetIt.I().player.speed, onChanged: (speed) { GetIt.I().player.setSpeed(speed); }, ), ); }, ), ], ), if (Platform.isAndroid) AdaptiveListTile( dense: true, title: Text(S.of(context).Equalizer), leading: Icon(AdaptiveIcons.equalizer), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const EqualizerPage(), ), ); }, trailing: Icon(Icons.chevron_right), ), if (song['artists'] != null) AdaptiveListTile( dense: true, title: Text(S.of(context).Artists), leading: Icon(AdaptiveIcons.people), trailing: Icon(Icons.chevron_right), onTap: () { Navigator.pop(context); Modals.showArtistsBottomModal( context, song['artists'], leading: song['thumbnails'].first['url'], shouldPop: true, ); }, ), if (song['album'] != null) AdaptiveListTile( dense: true, title: Text( S.of(context).Album, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Icon(AdaptiveIcons.album), trailing: Icon(Icons.chevron_right), onTap: () { context.go( '/browse', extra: { 'endpoint': song['album']['endpoint'] .cast(), }, ); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Playlist), leading: Icon(AdaptiveIcons.library_add), onTap: () { Navigator.pop(context); Modals.addToPlaylist(context, song); }, ), AdaptiveListTile( dense: true, leading: Icon(AdaptiveIcons.timer), title: Text(S.of(context).Sleep_Timer), onTap: () { showDurationPicker( context: context, initialTime: const Duration(minutes: 30), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: AdaptiveTheme.of(context).inactiveBackgroundColor, ), ).then((duration) { if (duration != null) { if (context.mounted) { context.read().setTimer(duration); } } }); }, trailing: ValueListenableBuilder( valueListenable: GetIt.I().timerDuration, builder: (context, value, child) { return value == null ? const SizedBox.shrink() : TextButton.icon( onPressed: () { GetIt.I().cancelTimer(); }, label: Text(formatDuration(value)), icon: const Icon(CupertinoIcons.clear), iconAlignment: IconAlignment.end, ); }, ), ), AdaptiveListTile( dense: true, title: const Text('Share'), leading: Icon(AdaptiveIcons.share), onTap: () { Navigator.pop(context); Share.shareUri( Uri.parse( 'https://music.youtube.com/watch?v=${song['videoId']}', ), ); }, ), ], ), ), ); } BottomModalLayout _showSelection( BuildContext context, List items, ) { return BottomModalLayout( title: Center(child: Text("Select", style: mediumTextStyle(context))), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ...items.map( (item) => AdaptiveListTile( dense: true, title: Text(item.title), onTap: () { Navigator.pop(context, item.data); }, ), ), ], ), ), ); } BottomModalLayout _songBottomModal(BuildContext context, Map song) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text(song['title'], maxLines: 1, overflow: TextOverflow.ellipsis), leading: ClipRRect( borderRadius: BorderRadius.circular(10), child: CachedNetworkImage( imageUrl: song['thumbnails'].first['url'], height: 50, width: song['type'] == 'VIDEO' ? 80 : 50, ), ), subtitle: song['subtitle'] != null ? Text(song['subtitle'], maxLines: 1, overflow: TextOverflow.ellipsis) : null, trailing: IconButton( onPressed: () => Share.shareUri( Uri.parse('https://music.youtube.com/watch?v=${song['videoId']}'), ), icon: const Icon(CupertinoIcons.share), ), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( dense: true, title: Text(S.of(context).Play_Next), leading: Icon(AdaptiveIcons.playlist_play), onTap: () async { Navigator.pop(context); await GetIt.I().playNext(Map.from(song)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Queue), leading: Icon(AdaptiveIcons.queue_add), onTap: () async { Navigator.pop(context); await GetIt.I().addToQueue(Map.from(song)); }, ), ListenableBuilder( listenable: GetIt.I().listenable, builder: (context, child) { bool isFavorite = GetIt.I().isFavourite(song); return AdaptiveListTile( dense: true, title: Text( !isFavorite ? S.of(context).Add_To_Favourites : S.of(context).Remove_From_Favourites, ), leading: Icon( !isFavorite ? AdaptiveIcons.heart : AdaptiveIcons.heart_fill, ), onTap: () async { Navigator.pop(context); GetIt.I().addOrRemove(song); }, ); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Download), leading: Icon(AdaptiveIcons.download), onTap: () { Navigator.pop(context); BottomMessage.showText(context, S.of(context).Download_Started); GetIt.I().downloadSong(song); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Playlist), leading: Icon(AdaptiveIcons.library_add), onTap: () { Navigator.pop(context); Modals.addToPlaylist(context, song); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Start_Radio), leading: Icon(AdaptiveIcons.radio), onTap: () { Navigator.pop(context); GetIt.I().startRelated(Map.from(song), radio: true); }, ), if (song['artists'] != null) AdaptiveListTile( dense: true, title: Text(S.of(context).Artists), leading: Icon(AdaptiveIcons.people), trailing: Icon(AdaptiveIcons.chevron_right), onTap: () { Navigator.pop(context); Modals.showArtistsBottomModal( context, song['artists'], leading: song['thumbnails'].first['url'], ); }, ), if (song['album'] != null) AdaptiveListTile( dense: true, title: Text( S.of(context).Album, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Icon(AdaptiveIcons.album), trailing: Icon(AdaptiveIcons.chevron_right), onTap: () { Navigator.pop(context); context.push( '/browse', extra: { 'endpoint': song['album']['endpoint'] .cast(), }, ); }, ), ], ), ), ); } BottomModalLayout _playlistBottomModal(BuildContext context, Map playlist) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text( playlist['title'], maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: (playlist['isPredefined'] != false) ? ClipRRect( borderRadius: BorderRadius.circular( playlist['type'] == 'ARTIST' ? 30 : 8, ), child: CachedNetworkImage( imageUrl: playlist['thumbnails'].first['url'].replaceAll( 'w540-h225', 'w60-h60', ), height: 40, width: 40, ), ) : Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: PlaylistIconWidget( data: PlaylistIcons.byId(playlist['iconId']), size: 30, ), ), subtitle: playlist['subtitle'] != null ? Text( playlist['subtitle'], maxLines: 1, overflow: TextOverflow.ellipsis, ) : null, trailing: playlist['isPredefined'] != false ? IconButton( onPressed: () => Share.shareUri( Uri.parse( playlist['type'] == 'ARTIST' ? 'https://music.youtube.com/channel/${playlist['endpoint']['browseId']}' : 'https://music.youtube.com/playlist?list=${playlist['playlistId']}', ), ), icon: const Icon(CupertinoIcons.share), ) : null, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( dense: true, title: Text(S.of(context).Play_Next), leading: Icon(AdaptiveIcons.playlist_play), onTap: () async { Navigator.pop(context); await GetIt.I().playNext(Map.from(playlist)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Queue), leading: Icon(AdaptiveIcons.queue_add), onTap: () async { Navigator.pop(context); await GetIt.I().addToQueue(Map.from(playlist)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Download), leading: Icon(AdaptiveIcons.download), onTap: () async { Navigator.pop(context); BottomMessage.showText(context, S.of(context).Download_Started); GetIt.I().downloadPlaylist(playlist); }, ), if (playlist['isPredefined'] == false) AdaptiveListTile( dense: true, leading: const Icon(Icons.edit), title: Text(S.of(context).Edit), onTap: () { Navigator.pop(context); Modals.showEditPlaylistBottomModal( context, playlistId: playlist['playlistId'], name: playlist['title'], iconId: playlist['iconId'], ); }, ), AdaptiveListTile( dense: true, title: Text( context.watch().getPlaylist( playlist['playlistId'] ?? playlist['endpoint']['browseId'], ) == null ? S.of(context).Add_To_Library : S.of(context).Remove_From_Library, ), leading: Icon( context.watch().getPlaylist( playlist['playlistId'] ?? playlist['endpoint']['browseId'], ) == null ? AdaptiveIcons.library_add : AdaptiveIcons.library_add_check, ), onTap: () async { if (context.read().getPlaylist( playlist['playlistId'], ) == null) { final String message = await GetIt.I() .addToOrRemoveFromLibrary(playlist); if (!context.mounted) return; BottomMessage.showText(context, message); } else { final bool confirm = await Modals.showConfirmBottomModal( context, message: S.of(context).Delete_Item_Message, isDanger: true, ); if (confirm != true) return; final String message = await GetIt.I() .addToOrRemoveFromLibrary(playlist); if (!context.mounted) return; BottomMessage.showText(context, message); } Navigator.pop(context); }, ), if (playlist['playlistId'] != null && playlist['type'] == 'ARTIST') AdaptiveListTile( dense: true, title: Text(S.of(context).Start_Radio), leading: Icon(AdaptiveIcons.radio), onTap: () async { Navigator.pop(context); BottomMessage.showText( context, S.of(context).Songs_Will_Start_Playing_Soon, ); await GetIt.I().startRelated( Map.from(playlist), radio: true, isArtist: playlist['type'] == 'ARTIST', ); }, ), if (playlist['artists'] != null && playlist['artists'].isNotEmpty) AdaptiveListTile( dense: true, title: Text(S.of(context).Artists), leading: Icon(AdaptiveIcons.people), trailing: Icon(AdaptiveIcons.chevron_right), onTap: () { Navigator.pop(context); Modals.showArtistsBottomModal( context, playlist['artists'], leading: playlist['thumbnails'].first['url'], ); }, ), if (playlist['album'] != null) AdaptiveListTile( dense: true, title: Text( S.of(context).Album, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Icon(AdaptiveIcons.album), trailing: Icon(AdaptiveIcons.chevron_right), onTap: () => context.push( '/browse', extra: {'endpoint': playlist['album']['endpoint']}, ), ), ], ), ), ); } BottomModalLayout _favouritesBottomModal(BuildContext context, Map playlist) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text( S.of(context).Favourites, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: ColorIcon( icon: FluentIcons.heart_24_filled, boxColor: Theme.of(context).colorScheme.primaryContainer, iconColor: Theme.of(context).colorScheme.onPrimaryContainer, size: 30, ), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( dense: true, title: Text(S.of(context).Play_Next), leading: Icon(AdaptiveIcons.playlist_play), onTap: () async { Navigator.pop(context); await GetIt.I().playNext(Map.from(playlist)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Queue), leading: Icon(AdaptiveIcons.queue_add), onTap: () async { Navigator.pop(context); await GetIt.I().addToQueue(Map.from(playlist)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Download), leading: Icon(AdaptiveIcons.download), onTap: () async { Navigator.pop(context); BottomMessage.showText(context, S.of(context).Download_Started); GetIt.I().downloadPlaylist(playlist); }, ), ], ), ), ); } BottomModalLayout _downloadBottomModal(BuildContext context) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text( S.of(context).Downloads, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: ColorIcon( icon: FluentIcons.cloud_arrow_down_24_filled, boxColor: Theme.of(context).colorScheme.primaryContainer, iconColor: Theme.of(context).colorScheme.onPrimaryContainer, size: 30, ), ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( dense: true, title: Text(S.of(context).Downloading), leading: Icon(AdaptiveIcons.downloading), onTap: () async { context.push('/library/downloads/downloading'); Navigator.pop(context); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Restore_Missing_Songs), leading: Icon(AdaptiveIcons.sync), onTap: () async { Navigator.pop(context); BottomMessage.showText( context, S.of(context).Restoring_Missing_Songs, ); GetIt.I().restoreDownloads(); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Delete_All_Songs), leading: Icon(AdaptiveIcons.delete), onTap: () async { bool shouldDelete = await Modals.showConfirmBottomModal( context, message: S.of(context).Confirm_Delete_All_Message, isDanger: true, doneText: S.of(context).Yes, cancelText: S.of(context).No, ); if (shouldDelete) { if (context.mounted) { Navigator.pop(context); BottomMessage.showText(context, S.of(context).Deleting_Songs); } await GetIt.I().deleteAllSongs(); } }, ), ], ), ), ); } BottomModalLayout _downloadDetailsBottomModal( BuildContext context, Map playlist, ) { return BottomModalLayout( title: AdaptiveListTile( contentPadding: EdgeInsets.zero, title: Text( playlist['title'], maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: (playlist['songs']?.length > 0 && playlist['id'] != DownloadManager.songsPlaylistId && playlist['id'] != FavouritesManager.playlistId) ? (playlist['type'] == "ALBUM") ? PlaylistThumbnail( playlist: [playlist['songs'][0]], size: 50, radius: 8, ) : PlaylistThumbnail( playlist: playlist['songs'], size: 50, radius: 8, ) : Container( height: 40, width: 40, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Icon( playlist['id'] == FavouritesManager.playlistId ? AdaptiveIcons.heart_fill : CupertinoIcons.music_note_list, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), subtitle: playlist['subtitle'] != null ? Text( playlist['subtitle'], maxLines: 1, overflow: TextOverflow.ellipsis, ) : null, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AdaptiveListTile( dense: true, title: Text(S.of(context).Play_Next), leading: Icon(AdaptiveIcons.playlist_play), onTap: () async { Navigator.pop(context); final plst = { ...playlist, 'songs': GetIt.I().getDownloadedSongs( playlist['id'], ), }; await GetIt.I().playNext(Map.from(plst)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Add_To_Queue), leading: Icon(AdaptiveIcons.queue_add), onTap: () async { Navigator.pop(context); final plst = { ...playlist, 'songs': GetIt.I().getDownloadedSongs( playlist['id'], ), }; await GetIt.I().addToQueue(Map.from(plst)); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Restore_Missing_Songs), leading: Icon(Icons.restore), onTap: () async { Navigator.pop(context); BottomMessage.showText( context, S.of(context).Restoring_Missing_Songs, ); GetIt.I().restoreDownloads( songs: playlist['songs'], ); }, ), AdaptiveListTile( dense: true, title: Text(S.of(context).Delete_All_Songs), leading: Icon(AdaptiveIcons.delete), onTap: () async { Modals.showConfirmBottomModal( context, message: S.of(context).Confirm_Delete_All_Message, isDanger: true, ).then((bool confirm) async { if (confirm) { if (context.mounted) { Navigator.pop(context); BottomMessage.showText( context, S.of(context).Deleting_Songs, ); } for (var song in playlist['songs']) { await GetIt.I().deleteSong( key: song['videoId'], playlistId: playlist['id'], ); } } }); }, ), ], ), ), ); } BottomModalLayout _accentSelector(BuildContext context) { Color? accentColor = GetIt.I().accentColor; return BottomModalLayout( title: Center(child: Text('Select Color', style: mediumTextStyle(context))), actions: [ AdaptiveButton( onPressed: () { Navigator.pop(context); GetIt.I().accentColor = null; }, child: const Text('Reset'), ), AdaptiveFilledButton( child: Text(S.of(context).Done), onPressed: () => Navigator.pop(context), ), ], child: Column( mainAxisSize: MainAxisSize.min, children: [ ColorPicker( pickerColor: accentColor ?? Colors.white, onColorChanged: (color) { GetIt.I().accentColor = color; }, labelTypes: const [], portraitOnly: true, colorPickerWidth: min(300, MediaQuery.of(context).size.width - 32), pickerAreaHeightPercent: 0.7, enableAlpha: false, displayThumbColor: false, paletteType: PaletteType.hueWheel, ), ], ), ); } class BottomModalLayout extends StatelessWidget { const BottomModalLayout({ required this.child, this.title, this.actions, super.key, }); final Widget child; final Widget? title; final List? actions; @override Widget build(BuildContext context) { return Container( width: double.maxFinite, constraints: const BoxConstraints(maxWidth: 600), child: Material( color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), bottomLeft: Radius.circular(0), bottomRight: Radius.circular(0), ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.onSurfaceVariant.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(2), ), ), ), if (title != null) Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 0, ), child: title!, ), Flexible( child: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: child, ), ), if (actions != null) Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: actions!, ), ), ], ), ), ), ), ); } } class SelectionItem { final String title; final IconData? icon; final T data; SelectionItem({required this.title, this.icon, required this.data}); } ================================================ FILE: lib/utils/check_update.dart ================================================ // import 'dart:convert'; // import 'dart:io'; // import 'package:device_info_plus/device_info_plus.dart'; // import 'package:flutter/material.dart'; // import 'package:http/http.dart' as http; // import 'package:pub_semver/pub_semver.dart'; // import '../app_config.dart'; // Future checkUpdate({BaseDeviceInfo? deviceInfo}) async { // try { // final response = await http.get(appConfig.updateUri, // headers: {'Accept': 'application/vnd.github+json'}); // Map update = jsonDecode(response.body); // Version currentVersion = Version.parse(appConfig.codeName); // Version remoteVersion = // Version.parse(update['tag_name'].toString().replaceAll('v', '')); // int comparison = remoteVersion.compareTo(currentVersion); // if (comparison > 0) { // if (deviceInfo == null) { // final deviceInfoPlugin = DeviceInfoPlugin(); // deviceInfo = await deviceInfoPlugin.deviceInfo; // } // Map? supportedAsset; // List assets = update['assets']; // if (Platform.isAndroid) { // List supportedAbis = // deviceInfo.data['supportedAbis'].cast(); // for (var supportedAbi in supportedAbis) { // List supportedAssets = assets // .where((asset) => asset['name'].contains(supportedAbi)) // .toList(); // if (supportedAssets.isNotEmpty) { // supportedAsset = supportedAssets.first; // break; // } // } // } else if (Platform.isWindows) { // List supportedAssets = assets // .where( // (asset) => // asset["content_type"] == "application/x-msdownload" || // asset['name'].toString().endsWith('.exe'), // ) // .toList(); // supportedAsset = // supportedAssets.isNotEmpty ? supportedAssets.first : null; // } // if (supportedAsset == null) return null; // int downloadCount = 0; // for (var asset in assets) { // downloadCount += (asset['download_count'] as int); // } // return UpdateInfo( // name: update['name'], // publishedAt: update['published_at'], // body: update['body'], // downloadUrl: supportedAsset['browser_download_url'], // downloadCount: downloadCount, // ); // } else { // return null; // } // } catch (e) { // debugPrint("Exception in checkUpdate: $e"); // return null; // } // } // class UpdateInfo { // String name; // String publishedAt; // String body; // String downloadUrl; // int downloadCount; // UpdateInfo({ // required this.name, // required this.publishedAt, // required this.body, // required this.downloadCount, // required this.downloadUrl, // }); // Map toMap() { // return { // 'name': name, // 'publishedAt': publishedAt, // 'body': body, // 'downloadUrl': downloadUrl, // 'downloadCount': downloadCount, // }; // } // } ================================================ FILE: lib/utils/enhanced_image.dart ================================================ String getEnhancedImage(String imageUrl, {String quality = 'high', double? width, double? dp}) { if (width != null) { int newWidth = ((dp ?? 1) * width).ceil(); return imageUrl .trim() .replaceAll('w60-h60', 'w${newWidth.toInt()}-h${newWidth.toInt()}') .replaceAll('w226-h226', 'w${newWidth.toInt()}-h${newWidth.toInt()}') .replaceAll('w540-h225', 'w${newWidth.toInt()}-h${newWidth.toInt()}') .replaceAll('w544-h544', 'w${newWidth.toInt()}-h${newWidth.toInt()}') .replaceAll('=s192', '=s$newWidth') .replaceAll('=s1200', '=s$newWidth') .replaceAll('sddefault', 'mqdefault'); } switch (quality) { case 'high': return imageUrl .trim() .replaceAll('w60-h60', 'w500-h500') .replaceAll('w226-h226', 'w500-h500') .replaceAll('w540-h225', 'w500-h500') .replaceAll('w544-h544', 'w500-h500') .replaceAll('sddefault', 'mqdefault'); case 'medium': return imageUrl .trim() .replaceAll('w60-h60', 'w300-h300') .replaceAll('w226-h226', 'w300-h300') .replaceAll('w540-h225', 'w300-h300') .replaceAll('w544-h544', 'w300-h300') .replaceAll('sddefault', 'mqdefault'); case 'low': return imageUrl .trim() .replaceAll('w60-h60', 'w100-h100') .replaceAll('w226-h226', 'w100-h100') .replaceAll('w540-h225', 'w100-h100') .replaceAll('w544-h544', 'w100-h100') .replaceAll('sddefault', 'mqdefault'); default: return imageUrl.trim().replaceAll('sddefault', 'maxresdefault'); } } ================================================ FILE: lib/utils/extensions.dart ================================================ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/settings_manager.dart'; extension DarkMode on BuildContext { /// is dark mode currently enabled? bool get isDarkMode { final themeMode = select((s) => s.themeMode); final brightness = themeMode == ThemeMode.system ? MediaQuery.of(this).platformBrightness : themeMode == ThemeMode.dark ? Brightness.dark : Brightness.light; return brightness == Brightness.dark; } bool get isDarkModeOnce { final brightness = read().themeMode == ThemeMode.system ? MediaQuery.of(this).platformBrightness : read().themeMode == ThemeMode.dark ? Brightness.dark : Brightness.light; return brightness == Brightness.dark; } Color get subtitleColor => isDarkMode ? Colors.white.withAlpha(150) : Colors.black.withAlpha(150); Color get bottomModalBackgroundColor => isDarkMode ? Color.alphaBlend(Colors.black.withAlpha(220), Colors.white) : Colors.white; } extension LayoutExtensions on BuildContext { bool get isKeyboardSpaceLimited { final mediaQuery = MediaQuery.of(this); final isLandscape = mediaQuery.orientation == Orientation.landscape; return isLandscape && mediaQuery.size.height < 450; } } extension StringMani on String { String get breakWord { String breakWord = ''; for (var element in runes) { breakWord += String.fromCharCode(element); breakWord += '\u200B'; } return breakWord; } } ================================================ FILE: lib/utils/format_duration.dart ================================================ String formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, ""); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); String newHours = duration.inHours > 0 ? '${duration.inHours}:' : ''; String newMinutes = int.parse(twoDigitMinutes) > 0 ? '$twoDigitMinutes:' : ''; return "$newHours$newMinutes$twoDigitSeconds"; } ================================================ FILE: lib/utils/internet_guard.dart ================================================ // import 'dart:async'; // import 'package:flutter/material.dart'; // import 'package:get_it/get_it.dart'; // import 'package:go_router/go_router.dart'; // import 'package:gyawun/generated/l10n.dart'; // import 'package:gyawun/themes/colors.dart'; // import 'package:gyawun/utils/adaptive_widgets/buttons.dart'; // import 'package:gyawun/utils/adaptive_widgets/icons.dart'; // import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; // import 'package:yt_music/ytmusic.dart'; // class InternetGuard extends StatefulWidget { // final Widget child; // final VoidCallback? onInternetLost; // final VoidCallback? onInternetRestored; // const InternetGuard({ // super.key, // required this.child, // this.onInternetLost, // this.onInternetRestored, // }); // @override // State createState() => _InternetGuardState(); // } // class _InternetGuardState extends State { // int _lastHandledError = 0; // bool _isOfflineMode = false; // ValueNotifier get lastConnectionError => // GetIt.I().lastConnectionError; // StreamSubscription? _networkSubscription; // final InternetConnection _internetConnection = InternetConnection(); // @override // void initState() { // super.initState(); // lastConnectionError.addListener(_onConnectionErrorDetected); // _lastHandledError = lastConnectionError.value; // } // @override // void dispose() { // _networkSubscription?.cancel(); // lastConnectionError.removeListener(_onConnectionErrorDetected); // super.dispose(); // } // void _onConnectionErrorDetected() { // if (!mounted) return; // final currentErrorTime = lastConnectionError.value; // if (currentErrorTime > _lastHandledError) { // _lastHandledError = currentErrorTime; // _enterOfflineMode(); // } // } // void _enterOfflineMode() { // if (_isOfflineMode) return; // setState(() { // _isOfflineMode = true; // }); // widget.onInternetLost?.call(); // _startMonitoringNetwork(); // } // void _startMonitoringNetwork() async { // _networkSubscription?.cancel(); // // Prevent flickering loops: only auto-restore if we transition from Offline to Online. // // If we are already connected (API error), we wait for manual retry. // InternetStatus lastKnownStatus = await _internetConnection.internetStatus; // _networkSubscription = _internetConnection.onStatusChange.listen((status) { // if (lastKnownStatus == InternetStatus.disconnected && // status == InternetStatus.connected) { // _tryRestoreConnection(); // } // }); // } // void _tryRestoreConnection() { // if (!mounted || !_isOfflineMode) return; // setState(() { // _isOfflineMode = false; // }); // _networkSubscription?.cancel(); // _networkSubscription = null; // widget.onInternetRestored?.call(); // } // @override // Widget build(BuildContext context) { // return Stack( // children: [ // widget.child, // if (_isOfflineMode) // Scaffold( // backgroundColor: Theme.of(context).scaffoldBackgroundColor, // body: Center( // child: Column( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // Icon(AdaptiveIcons.wifi_off_rounded, // size: 80, color: greyColor), // const SizedBox(height: 20), // Text( // S.of(context).No_Internet_Connection, // textAlign: TextAlign.center, // style: const TextStyle( // fontSize: 18, fontWeight: FontWeight.bold), // ), // const SizedBox(height: 20), // AdaptiveFilledButton( // onPressed: () => context.go('/saved/downloads'), // child: Text(S.of(context).Go_To_Downloads), // ), // const SizedBox(height: 20), // OutlinedButton.icon( // icon: const Icon(Icons.refresh), // label: Text(S.of(context).Retry), // onPressed: () { // _tryRestoreConnection(); // }, // ), // ], // ), // ), // ), // ], // ); // } // } ================================================ FILE: lib/utils/playlist_icon.dart ================================================ import 'package:flutter/material.dart'; import 'package:m3e_collection/m3e_collection.dart'; abstract class PlaylistIcon { final String id; const PlaylistIcon(this.id); String toId() => id; } class MaterialPlaylistIcon extends PlaylistIcon { final IconData iconData; const MaterialPlaylistIcon(super.id, this.iconData); } class PolygonPlaylistIcon extends PlaylistIcon { final RoundedPolygon polygon; const PolygonPlaylistIcon(super.id, this.polygon); } ================================================ FILE: lib/utils/playlist_icon_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gyawun/core/widgets/rounded_polygon_icon.dart'; import 'playlist_icon.dart'; class MaterialIconWidget extends StatelessWidget { final MaterialPlaylistIcon data; final double size; const MaterialIconWidget({super.key, required this.data, required this.size}); @override Widget build(BuildContext context) { return Icon( data.iconData, size: size, color: Theme.of(context).colorScheme.onPrimaryContainer, ); } } class PolygonIconWidget extends StatelessWidget { final PolygonPlaylistIcon data; final double size; const PolygonIconWidget({super.key, required this.data, required this.size}); @override Widget build(BuildContext context) { return RoundedPolygonIcon( polygon: data.polygon, size: size, color: Theme.of(context).colorScheme.onPrimaryContainer, ); } } class PlaylistIconWidget extends StatelessWidget { final PlaylistIcon data; final double size; const PlaylistIconWidget({super.key, required this.data, required this.size}); @override Widget build(BuildContext context) { return switch (data) { MaterialPlaylistIcon m => MaterialIconWidget(data: m, size: size), PolygonPlaylistIcon p => PolygonIconWidget(data: p, size: size), _ => SizedBox(width: size, height: size), }; } } ================================================ FILE: lib/utils/playlist_icons.dart ================================================ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:gyawun/utils/playlist_icon.dart'; import 'package:m3e_collection/m3e_collection.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class PlaylistIcons { PlaylistIcons._(); static final musicNoteList = MaterialPlaylistIcon( 'musicNoteList', CupertinoIcons.music_note_list, ); static final compactDisc = MaterialPlaylistIcon( 'compactDisc', FontAwesomeIcons.compactDisc, ); static final radio = MaterialPlaylistIcon('radio', FontAwesomeIcons.radio); static final podcast = MaterialPlaylistIcon( 'podcast', FontAwesomeIcons.podcast, ); static final guitar = MaterialPlaylistIcon('guitar', FontAwesomeIcons.guitar); static final drum = MaterialPlaylistIcon('drum', FontAwesomeIcons.drum); static final dumbbell = MaterialPlaylistIcon( 'dumbbell', FontAwesomeIcons.dumbbell, ); static final personRunning = MaterialPlaylistIcon( 'person_running', FontAwesomeIcons.personRunning, ); static final atom = MaterialPlaylistIcon('atom', FontAwesomeIcons.atom); static final circleRadiation = MaterialPlaylistIcon( 'circleRadiation', FontAwesomeIcons.circleRadiation, ); static final spa = MaterialPlaylistIcon('spa', FontAwesomeIcons.spa); static final bed = MaterialPlaylistIcon('bed', FontAwesomeIcons.bed); static final sun = MaterialPlaylistIcon('sun', FontAwesomeIcons.sun); static final water = MaterialPlaylistIcon('water', FontAwesomeIcons.water); static final fire = MaterialPlaylistIcon('fire', FontAwesomeIcons.fire); static final wind = MaterialPlaylistIcon('wind', FontAwesomeIcons.wind); static final car = MaterialPlaylistIcon('car', FontAwesomeIcons.car); static final motorcycle = MaterialPlaylistIcon( 'motorcycle', FontAwesomeIcons.motorcycle, ); static final fly = MaterialPlaylistIcon('fly', FontAwesomeIcons.fly); static final dna = MaterialPlaylistIcon('dna', FontAwesomeIcons.dna); static final skull = MaterialPlaylistIcon('skull', FontAwesomeIcons.skull); static final virus = MaterialPlaylistIcon('virus', FontAwesomeIcons.virus); static final flask = MaterialPlaylistIcon('flask', FontAwesomeIcons.flask); static final faceLaugh = MaterialPlaylistIcon( 'faceLaugh', FontAwesomeIcons.faceLaugh, ); static final faceSadCry = MaterialPlaylistIcon( 'faceSadCry', FontAwesomeIcons.faceSadCry, ); static final brain = MaterialPlaylistIcon('brain', FontAwesomeIcons.brain); static final earthAmericas = MaterialPlaylistIcon( 'earthAmericas', FontAwesomeIcons.earthAmericas, ); static final heartCrack = MaterialPlaylistIcon( 'heartCrack', FontAwesomeIcons.heartCrack, ); static final spotify = MaterialPlaylistIcon( 'spotify', FontAwesomeIcons.spotify, ); static final pizzaSlice = MaterialPlaylistIcon( 'pizzaSlice', FontAwesomeIcons.pizzaSlice, ); static final pill = PolygonPlaylistIcon('pill', MaterialShapes.pill); static final arrow = PolygonPlaylistIcon('arrow', MaterialShapes.arrow); static final boom = PolygonPlaylistIcon('boom', MaterialShapes.boom); static final circle = PolygonPlaylistIcon('circle', MaterialShapes.circle); static final clover4Leaf = PolygonPlaylistIcon( 'clover4Leaf', MaterialShapes.clover4Leaf, ); static List values = [ musicNoteList, compactDisc, radio, podcast, guitar, drum, dumbbell, personRunning, atom, circleRadiation, spa, bed, sun, water, fire, wind, car, motorcycle, fly, dna, skull, virus, flask, faceLaugh, faceSadCry, brain, earthAmericas, heartCrack, spotify, pizzaSlice, pill, arrow, boom, circle, clover4Leaf, ]; static PlaylistIcon byId(String id) => values.firstWhereOrNull((icon) => icon.toId() == id) ?? values.first; } ================================================ FILE: lib/utils/playlist_thumbnail.dart ================================================ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:gyawun/utils/song_thumbnail.dart'; class PlaylistThumbnail extends StatefulWidget { final List playlist; final double size; final double radius; const PlaylistThumbnail({ super.key, required this.playlist, required this.size, this.radius = 0, }); @override State createState() => _PlaylistThumbnailState(); } class _PlaylistThumbnailState extends State { List _itemsToDisplay = []; @override void initState() { super.initState(); _calculateItems(forceUpdate: true); } @override void didUpdateWidget(covariant PlaylistThumbnail oldWidget) { super.didUpdateWidget(oldWidget); _calculateItems(); } void _calculateItems({bool forceUpdate = false}) { final int count = min(widget.playlist.length, 4); final List sublist = widget.playlist.sublist(0, count); final List currentIds = sublist.map((e) => e['videoId'].toString()).toList(); final List cachedIds = _itemsToDisplay.map((e) => e['videoId'].toString()).toList(); if (!forceUpdate && listEquals(cachedIds, currentIds)) { return; } if (forceUpdate) { _itemsToDisplay = sublist; } else { setState(() { _itemsToDisplay = sublist; }); } } @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(widget.radius), child: SizedBox( height: widget.size, width: widget.size, child: ClipRRect( borderRadius: .circular(8), child: StaggeredGrid.count( crossAxisCount: _itemsToDisplay.length > 1 ? 2 : 1, children: _itemsToDisplay.indexed.map((ind) { int index = ind.$1; Map song = ind.$2; return SongThumbnail( key: ValueKey(song['videoId']), song: song, height: (_itemsToDisplay.length <= 2 || (_itemsToDisplay.length == 3 && index == 0)) ? widget.size : widget.size / 2, width: _itemsToDisplay.length > 1 ? widget.size / 2 : widget.size, fit: BoxFit.cover, ); }).toList(), ), ), ), ); } } ================================================ FILE: lib/utils/pprint.dart ================================================ import 'dart:convert'; import 'dart:developer'; void pprint(Object? data) { const JsonEncoder encoder = JsonEncoder.withIndent(' '); final jsonString = encoder.convert(data); log(jsonString); } ================================================ FILE: lib/utils/router.dart ================================================ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:gyawun/screens/browse/browse_page.dart'; import 'package:gyawun/screens/chip/chip_page.dart'; import 'package:gyawun/screens/home/home_page.dart'; import 'package:gyawun/screens/library/downloads/downloading/downloading_page.dart'; import 'package:gyawun/screens/library/downloads/downloads_page.dart'; import 'package:gyawun/screens/library/downloads/playlist/download_playlist_page.dart'; import 'package:gyawun/screens/library/favourites/favourites_page.dart'; import 'package:gyawun/screens/library/history/history_page.dart'; import 'package:gyawun/screens/library/library_page.dart'; import 'package:gyawun/screens/library/playlist/playlist_details_page.dart'; import 'package:gyawun/screens/player/player_page.dart'; import 'package:gyawun/screens/search/search_page.dart'; import 'package:gyawun/screens/settings/about/about_page.dart'; import 'package:gyawun/screens/settings/appearance/appearance_page.dart'; import 'package:gyawun/screens/settings/backup_storage/backup_storage_page.dart'; import 'package:gyawun/screens/settings/player/equalizer/equalizer_page.dart'; import 'package:gyawun/screens/settings/player/player_settings_page.dart'; import 'package:gyawun/screens/settings/privacy/privacy_page.dart'; import 'package:gyawun/screens/settings/services/yt_music/yt_music_page.dart'; import 'package:gyawun/screens/settings/settings_page.dart'; import 'package:gyawun/screens/shell/app_shell.dart'; GoRouter router = GoRouter( initialLocation: '/', routes: [ ShellRoute( builder: (context, state, child) => child, routes: [ StatefulShellRoute( branches: branches, builder: (context, state, navigationShell) => AppShell( navigationShell: navigationShell, ), navigatorContainerBuilder: (context, navigationShell, children) => MyPageView( currentIndex: navigationShell.currentIndex, children: children, ), ), GoRoute( path: '/player', pageBuilder: (context, state) { final videoId = state.extra as String?; return CustomTransitionPage( key: state.pageKey, child: PlayerPage(videoId: videoId), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(0.0, 1.0); const end = Offset.zero; final curve = Curves.ease; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: curve)); return SlideTransition( position: animation.drive(tween), child: child, ); }, ); }, ), ], ), ], ); List branches = [ StatefulShellBranch( routes: [ GoRoute( path: '/', builder: (context, state) => const HomePage(), routes: [ GoRoute( path: 'chip', builder: (context, state) { Map args = state.extra as Map; return ChipPage( title: args['title'] ?? '', endpoint: args['endpoint'] ?? {}); }, ), GoRoute( path: 'browse', builder: (context, state) { final args = state.extra as Map? ?? {}; return BrowsePage( endpoint: args['endpoint'] as Map, isMore: args['isMore'] as bool? ?? false, ); }, ), GoRoute( path: 'search', builder: (context, state) { final args = state.extra as Map?; return SearchPage( endpoint: args?['endpoint'] as Map?, isMore: args?['isMore'] as bool? ?? false, ); }, ), ]), ], ), StatefulShellBranch(routes: [ GoRoute( path: '/library', builder: (context, state) => const LibraryPage(), routes: [ GoRoute( path: 'favourites', builder: (context, state) => const FavouritesPage(), ), GoRoute( path: 'downloads', builder: (context, state) => const DownloadsPage(), routes: [ GoRoute( path: 'download_playlist', builder: (context, state) { final args = state.extra as Map; return DownloadPlaylistPage( playlistId: args['playlistId'] as String, ); }, ), GoRoute( path: 'downloading', builder: (context, state) => const DownloadingPage(), ), ], ), GoRoute( path: 'history', builder: (context, state) => const HistoryPage(), ), GoRoute( path: 'playlist_details', builder: (context, state) { final args = state.extra as Map; return PlaylistDetailsPage( playlistkey: args['playlistkey'] as String, ); }, ), ], ), ]), StatefulShellBranch(routes: [ GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), routes: [ GoRoute( path: 'appearance', builder: (context, state) => const AppearancePage(), ), GoRoute( path: 'player', builder: (context, state) => const PlayerSettingsPage(), routes: [ GoRoute( path: 'equalizer', builder: (context, state) => const EqualizerPage(), ) ]), GoRoute( path: 'services/ytmusic', builder: (context, state) => const YTMusicPage(), ), GoRoute( path: 'backup_storage', builder: (context, state) => const BackupStoragePage(), ), GoRoute( path: 'privacy', builder: (context, state) => const PrivacyPage(), ), GoRoute( path: 'about', builder: (context, state) => const AboutPage(), ), ]), ]) ]; class MyPageView extends StatefulWidget { final int currentIndex; final List children; const MyPageView( {super.key, required this.currentIndex, required this.children}); @override MyPageViewState createState() => MyPageViewState(); } class MyPageViewState extends State { final PageController controller = PageController(initialPage: 0); @override void initState() { super.initState(); } @override void didUpdateWidget(covariant MyPageView oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.currentIndex != widget.currentIndex) { controller.animateToPage(widget.currentIndex, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); } } @override Widget build(BuildContext context) { return PageView( scrollDirection: (Platform.isWindows || MediaQuery.of(context).size.width >= 450) ? Axis.vertical : Axis.horizontal, physics: const NeverScrollableScrollPhysics(), controller: controller, children: widget.children, ); } } ================================================ FILE: lib/utils/song_thumbnail.dart ================================================ import 'package:audiotags/audiotags.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:gyawun/utils/enhanced_image.dart'; import '../services/download_manager.dart'; class SongThumbnail extends StatefulWidget { final Map song; final double? dp; final double? width; final double? height; final FilterQuality filterQuality; final BoxFit? fit; final Widget Function(BuildContext, String, Object)? errorWidget; final void Function(ImageProvider)? onImageReady; const SongThumbnail({ super.key, required this.song, this.dp, this.height, this.width, this.filterQuality = FilterQuality.high, this.fit, this.errorWidget, this.onImageReady, }); @override State createState() => _SongThumbnailState(); } class _SongThumbnailState extends State { MemoryImage? _localImageProvider; bool _isCheckingLocal = true; ImageProvider? _lastNotifiedProvider; @override void initState() { super.initState(); _checkLocalThumbnail(); } @override void didUpdateWidget(covariant SongThumbnail oldWidget) { super.didUpdateWidget(oldWidget); final oldId = oldWidget.song['videoId'] ?? ''; final newId = widget.song['videoId'] ?? ''; if (oldId != newId) { _lastNotifiedProvider = null; _checkLocalThumbnail(); } } Future _checkLocalThumbnail() async { if (!_isCheckingLocal) setState(() => _isCheckingLocal = true); MemoryImage? foundImage; final downloadSong = GetIt.I().getDownload( widget.song['videoId'], ); if (downloadSong != null && downloadSong['status'] == "DOWNLOADED" && downloadSong['path'] != null) { try { final Tag? tag = await AudioTags.read(downloadSong['path']); if (tag?.pictures.isNotEmpty == true) { foundImage = MemoryImage(tag!.pictures.first.bytes); } } catch (e) { debugPrint("Errore lettura tag: $e"); _localImageProvider = null; _isCheckingLocal = false; } } if (!mounted) return; setState(() { _localImageProvider = foundImage; _isCheckingLocal = false; }); } Widget _buildDisplayImage(ImageProvider provider) { if (widget.onImageReady != null && provider != _lastNotifiedProvider) { _lastNotifiedProvider = provider; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) widget.onImageReady!(provider); }); } return RepaintBoundary( child: Image( image: provider, height: widget.height, width: widget.width, fit: widget.fit, filterQuality: widget.filterQuality, gaplessPlayback: true, ), ); } Widget _buildCachedNetworkImage(List urls, int index) { return RepaintBoundary( child: CachedNetworkImage( imageUrl: urls[index], height: widget.height, width: widget.width, fit: widget.fit, filterQuality: widget.filterQuality, imageBuilder: (context, provider) => _buildDisplayImage(provider), placeholder: (context, url) => SizedBox(height: widget.height, width: widget.width), errorWidget: (index + 1 < urls.length) ? (context, url, error) => _buildCachedNetworkImage(urls, index + 1) : widget.errorWidget, ), ); } @override Widget build(BuildContext context) { if (_isCheckingLocal) { return SizedBox(height: widget.height, width: widget.width); } if (_localImageProvider != null) { return _buildDisplayImage(_localImageProvider!); } final String baseUrl = widget.song['thumbnails'].first['url']; final List urls = [ getEnhancedImage(baseUrl, dp: widget.dp, width: widget.width), getEnhancedImage(baseUrl, quality: 'medium'), getEnhancedImage(baseUrl, quality: 'low'), ]; return _buildCachedNetworkImage(urls, 0); } } ================================================ FILE: lib/utils/text_controller_builder.dart ================================================ import 'package:flutter/material.dart'; class TextControllerBuilder extends StatefulWidget { final String? initialText; final Widget Function(BuildContext context, TextEditingController controller) builder; const TextControllerBuilder({ super.key, this.initialText, required this.builder, }); @override State createState() => _TextControllerBuilderState(); } class _TextControllerBuilderState extends State { late final TextEditingController _controller; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialText); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return widget.builder(context, _controller); } } ================================================ FILE: pubspec.yaml ================================================ name: gyawun description: "A new Flutter project." publish_to: 'none' version: 2.0.18-beta.1+218 environment: sdk: '>=3.10.0 <4.0.0' dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter cupertino_icons: ^1.0.6 go_router: ^16.2.4 google_fonts: ^6.2.1 intl: any http: ^1.5.0 hive: ^2.2.3 hive_flutter: ^1.1.0 youtube_explode_dart: git: url: https://github.com/sheikhhaziq/youtube_explode_dart.git expandable_page_view: ^1.0.17 get_it: ^8.0.3 cached_network_image: ^3.3.1 flutter_typeahead: ^5.2.0 expandable_text: ^2.3.0 salomon_bottom_bar: ^3.3.2 share_plus: ^12.0.0 provider: ^6.1.2 flutter_staggered_grid_view: ^0.7.0 flutter_swipe_action_cell: ^3.1.3 just_audio: ^0.10.5 just_audio_background: ^0.0.1-beta.14 audio_video_progress_bar: ^2.0.3 sliding_up_panel: ^2.0.0+1 url_launcher: ^6.3.0 country_picker: ^2.0.26 text_scroll: ^0.2.0 duration_picker: ^1.2.0 path_provider: ^2.1.3 path: ^1.9.0 file_picker: ^10.1.9 dynamic_color: ^1.7.0 audiotags: ^1.4.3 pub_semver: ^2.1.4 device_info_plus: ^12.1.0 flutter_expandable_fab: ^2.1.0 flutter_lyric: ^2.0.4+6 media_kit_libs_windows_audio: any media_kit_libs_linux: any just_audio_media_kit: ^2.0.6 media_kit_native_event_loop: any receive_sharing_intent: git: url: https://github.com/sheikhhaziq/receive_sharing_intent.git fl_toast: ^3.2.0 # Discontinued flutter_colorpicker: ^1.1.0 easy_folder_picker: ^1.3.3 permission_handler: ^11.4.0 http_server: ^1.0.0 rxdart: ^0.28.0 translator: ^1.0.4+1 language_detector: ^1.0.1 wakelock_plus: ^1.4.0 internet_connection_checker_plus: ^2.9.1 collection: ^1.19.1 flutter_bloc: ^9.1.1 bloc: ^9.2.0 meta: ^1.17.0 loading_indicator_m3e: ^0.1.1 # m3e_collection: ^0.3.7 package_info_plus: ^9.0.0 flutter_markdown_plus: ^1.0.7 yt_music: git: url: https://github.com/sheikhhaziq/yt_music.git connectivity_plus: ^7.0.0 audio_service_mpris: ^0.2.0 fluentui_system_icons: ^1.1.273 navigation_rail_m3e: ^0.3.5 m3e_collection: ^0.3.7 font_awesome_flutter: ^10.12.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 change_app_package_name: ^1.3.0 flutter_native_splash: ^2.4.7 intl_utils: ^2.5.0 flutter: uses-material-design: true generate: true assets: - assets/images/ flutter_intl: enabled: true ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:gyawun/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const Gyawun()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); }