diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index a55310ce4..000000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,177 +0,0 @@
-version: 2.1
-
-orbs:
- gh: circleci/github-cli@2.2.0
-
-jobs:
- flutter_linux_arm:
- machine:
- image: ubuntu-2204:current
- resource_class: arm.medium
- parameters:
- version:
- type: string
- default: 3.1.1
- channel:
- type: enum
- enum:
- - release
- - nightly
- default: release
- github_run_number:
- type: string
- default: "0"
- dry_run:
- type: boolean
- default: true
- steps:
- - checkout
- - gh/setup
-
- - run:
- name: Get current date
- command: |
- echo "export CURRENT_DATE=$(date +%Y-%m-%d)" >> $BASH_ENV
-
- - run:
- name: Install dependencies
- command: |
- sudo apt-get update -y
- sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev zip rpm
-
- - run:
- name: Install Flutter
- command: |
- git clone https://github.com/flutter/flutter.git
- cd flutter && git checkout stable && cd ..
- export PATH="$PATH:`pwd`/flutter/bin"
- flutter precache
- flutter doctor -v
-
- - run:
- name: Install AppImageTool
- command: |
- wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage"
- chmod +x appimagetool
- mv appimagetool flutter/bin
-
- - persist_to_workspace:
- root: flutter
- paths:
- - .
-
- - when:
- condition:
- equal: [<< parameters.channel >>, nightly]
- steps:
- - run:
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
- command: |
- curl -sS https://webi.sh/yq | sh
- yq -i '.version |= sub("\+\d+", "+<< parameters.channel >>.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo 'export BUILD_VERSION="<< parameters.version >>+<< parameters.channel >>.<< parameters.github_run_number >>"' >> $BASH_ENV
-
- - when:
- condition:
- equal: [<< parameters.channel >>, release]
- steps:
- - run: echo 'export BUILD_VERSION="<< parameters.version >>"' >> $BASH_ENV
-
- - run:
- name: Generate .env file
- command: |
- echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env
-
- - run:
- name: Replace Version in files
- command: |
- sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml
- echo "build_arch: aarch64" >> linux/packaging/rpm/make_config.yaml
-
- - run:
- name: Build secrets
- command: |
- export PATH="$PATH:`pwd`/flutter/bin"
- flutter config --enable-linux-desktop
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - run:
- name: Build Flutter app
- command: |
- export PATH="$PATH:`pwd`/flutter/bin"
- export PATH="$PATH":"$HOME/.pub-cache/bin"
- dart pub global activate flutter_distributor
- alias dpkg-deb="dpkg-deb --Zxz"
- flutter_distributor package --platform=linux --targets=deb
- flutter_distributor package --platform=linux --targets=appimage
- flutter_distributor package --platform=linux --targets=rpm
-
- - when:
- condition:
- equal: [<< parameters.channel >>, nightly]
- steps:
- - run: make tar VERSION=nightly ARCH=arm64 PKG_ARCH=aarch64
-
- - when:
- condition:
- equal: [<< parameters.channel >>, release]
- steps:
- - run: make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64
-
- - run:
- name: Move artifacts
- command: |
- mkdir bundle
- mv build/spotube-linux-*-aarch64.tar.xz bundle/
- mv dist/**/spotube-*-linux.deb bundle/Spotube-linux-aarch64.deb
- mv dist/**/spotube-*-linux.rpm bundle/Spotube-linux-aarch64.rpm
- mv dist/**/spotube-*-linux.AppImage bundle/Spotube-linux-aarch64.AppImage
- zip -r Spotube-linux-aarch64.zip bundle
-
- - store_artifacts:
- path: Spotube-linux-aarch64.zip
-
- - when:
- condition:
- and:
- - equal: [<< parameters.dry_run >>, false]
- - equal: [<< parameters.channel >>, release]
- steps:
- - run:
- name: Upload to release (release)
- command: gh release upload v<< parameters.version >> bundle/* --clobber
-
- - when:
- condition:
- and:
- - equal: [<< parameters.dry_run >>, false]
- - equal: [<< parameters.channel >>, nightly]
- steps:
- - run:
- name: Upload to release (nightly)
- command: gh release upload nightly bundle/* --clobber
-
-parameters:
- GHA_Actor:
- type: string
- default: ""
- GHA_Action:
- type: string
- default: ""
- GHA_Event:
- type: string
- default: ""
- GHA_Meta:
- type: string
- default: ""
-
-workflows:
- build_flutter_for_arm_workflow:
- when: << pipeline.parameters.GHA_Action >>
- jobs:
- - flutter_linux_arm:
- context:
- - org-global
- - GITHUB_CREDS
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..ddfd15179
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+build
+dist
+.dart_tool
+.idea
+.github
+.git
\ No newline at end of file
diff --git a/.env.example b/.env.example
index 22abd24bd..566656632 100644
--- a/.env.example
+++ b/.env.example
@@ -9,3 +9,6 @@ ENABLE_UPDATE_CHECK=
LASTFM_API_KEY=
LASTFM_API_SECRET=
+
+# Release channel. Can be: nightly, stable
+RELEASE_CHANNEL=
diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
index 7ca74200a..160b5b293 100644
--- a/.fvm/fvm_config.json
+++ b/.fvm/fvm_config.json
@@ -1,4 +1,4 @@
{
- "flutterSdkVersion": "3.19.1",
+ "flutterSdkVersion": "3.22.3",
"flavors": {}
}
\ No newline at end of file
diff --git a/.github/Dockerfile b/.github/Dockerfile
new file mode 100644
index 000000000..f6a9f538a
--- /dev/null
+++ b/.github/Dockerfile
@@ -0,0 +1,25 @@
+ARG FLUTTER_VERSION
+
+FROM --platform=linux/arm64 krtirtho/flutter_distributor:${FLUTTER_VERSION}
+
+ARG BUILD_VERSION
+
+WORKDIR /app
+
+COPY . .
+
+RUN chown -R $(whoami) /app
+
+RUN rustup target add aarch64-unknown-linux-gnu
+
+RUN flutter pub get
+
+RUN alias dpkg-deb="dpkg-deb --Zxz" &&\
+ flutter_distributor package --platform=linux --targets=deb --skip-clean
+
+RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64
+
+RUN mv build/spotube-linux-*-aarch64.tar.xz dist/ &&\
+ mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb
+
+CMD [ "sleep", "5000000" ]
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 64ee89d2c..fed668501 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -53,7 +53,7 @@ body:
description: Where did you install Spotube from?
multiple: true
options:
- - "Website (spotube.netlify.app) or (spotube.krtirtho.dev)"
+ - "Website (spotube.krtirtho.dev)"
- "GitHub Releases (Binary)"
- "GitHub Actions (Nightly Binary)"
- "Play Store (Android)"
@@ -77,4 +77,4 @@ body:
description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions!
options:
- label: I'm ready to work on this issue!
- required: false
\ No newline at end of file
+ required: false
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index 156d1a076..db158029a 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -4,7 +4,7 @@ on:
pull_request:
env:
- FLUTTER_VERSION: '3.19.5'
+ FLUTTER_VERSION: 3.22.2
jobs:
lint:
diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml
index 805a89ac3..812849acc 100644
--- a/.github/workflows/spotube-publish-binary.yml
+++ b/.github/workflows/spotube-publish-binary.yml
@@ -4,7 +4,7 @@ on:
inputs:
version:
description: Version to publish (x.x.x)
- default: 3.1.0
+ default: 3.8.0
required: true
dry_run:
description: Dry run
@@ -12,10 +12,10 @@ on:
type: boolean
default: true
jobs:
- description: Jobs to run (flathub,aur,winget,chocolatey)
+ description: Jobs to run (flathub,aur,winget,chocolatey,playstore)
required: true
type: string
- default: "flathub,aur,winget,chocolatey"
+ default: "flathub,aur,winget,chocolatey,playstore"
jobs:
flathub:
@@ -66,7 +66,7 @@ jobs:
- name: Release to AUR
if: ${{ !inputs.dry_run }}
- uses: KSXGitHub/github-actions-deploy-aur@v2.7.1
+ uses: KSXGitHub/github-actions-deploy-aur@v2.7.2
with:
pkgname: spotube-bin
pkgbuild: aur-struct/PKGBUILD
@@ -104,3 +104,34 @@ jobs:
- name: Publish to Chocolatey Repository
if: ${{ !inputs.dry_run }}
run: choco push Spotube-windows-x86_64.nupkg --source https://push.chocolatey.org/
+
+ playstore:
+ runs-on: ubuntu-latest
+ if: contains(inputs.jobs, 'playstore')
+ steps:
+ - name: Tagname (workflow dispatch)
+ run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
+
+ - uses: robinraju/release-downloader@main
+ with:
+ repository: KRTirtho/spotube
+ tag: v${{ env.TAG_NAME }}
+ tarBall: false
+ zipBall: false
+ out-file-path: dist
+ fileName: "Spotube-playstore-all-arch.aab"
+
+ - name: Create service-account.json
+ run: |
+ echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
+
+ - name: Upload Android Release to Play Store
+ if: ${{!inputs.dry_run}}
+ uses: r0adkll/upload-google-play@v1
+ with:
+ serviceAccountJson: ./service-account.json
+ releaseFiles: ./dist/Spotube-playstore-all-arch.aab
+ packageName: oss.krtirtho.spotube
+ track: production
+ status: draft
+ releaseName: ${{ env.TAG_NAME }}
\ No newline at end of file
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index d9fbd0c7d..b103ea2e0 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -2,345 +2,119 @@ name: Spotube Release Binary
on:
workflow_dispatch:
inputs:
- version:
- description: Version to release (x.x.x)
- default: 3.6.0
- required: true
channel:
type: choice
- description: Release Channel
- required: true
options:
- stable
- nightly
default: nightly
+ description: The release channel
debug:
- description: Debug on failed when channel is nightly
- required: true
type: boolean
default: false
+ description: Debug with SSH toggle
+ required: false
dry_run:
- description: Dry run
- required: true
type: boolean
- default: true
+ default: false
+ description: Dry run without uploading to release
env:
- FLUTTER_VERSION: '3.19.1'
-
-jobs:
- windows:
- runs-on: windows-latest
- steps:
- - uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.12.0
- with:
- cache: true
- flutter-version: ${{ env.FLUTTER_VERSION }}
-
- - name: Replace pubspec version and BUILD_VERSION Env (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: |
- choco install sed make yq -y
- yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $env:GITHUB_ENV
-
- - name: BUILD_VERSION Env (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: |
- "BUILD_VERSION=${{ inputs.version }}" >> $env:GITHUB_ENV
-
- - name: Replace version in files
- run: |
- choco install sed make -y
- sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" windows/runner/Runner.rc
- sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/tools/VERIFICATION.txt
- sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/spotube.nuspec
-
- - name: Create Stable .env
- if: ${{ inputs.channel == 'stable' }}
- run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
-
- - name: Create Nightly .env
- if: ${{ inputs.channel == 'nightly' }}
- run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
-
- - name: Generating Secrets
- run: |
- flutter config --enable-windows-desktop
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - name: Build Windows Executable
- run: |
- dart pub global activate flutter_distributor
- make innoinstall
- flutter_distributor package --platform=windows --targets=exe --skip-clean
- mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
-
- - name: Create Chocolatey Package and set hash
- if: ${{ inputs.channel == 'stable' }}
- run: |
- Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash
- sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt
- make choco
- mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg
-
-
- - name: Upload Artifact
- uses: actions/upload-artifact@v3
- with:
- if-no-files-found: error
- name: Spotube-Release-Binaries
- path: |
- dist/Spotube-windows-x86_64.nupkg
- dist/Spotube-windows-x86_64-setup.exe
-
- - name: Debug With SSH When fails
- if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
- uses: mxschmitt/action-tmate@v3
- with:
- limit-access-to-actor: true
-
- linux:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.12.0
- with:
- cache: true
- flutter-version: ${{ env.FLUTTER_VERSION }}
-
- - name: Get current date
- id: date
- run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
-
- - name: Install Dependencies
- run: |
- sudo apt-get update -y
- sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev
-
- - name: Install AppImage Tool
- run: |
- wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
- chmod +x appimagetool
- mv appimagetool /usr/local/bin/
-
- - name: Replace pubspec version and BUILD_VERSION Env (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: |
- curl -sS https://webi.sh/yq | sh
- yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: BUILD_VERSION Env (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: |
- echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
-
- - name: Create Stable .env
- if: ${{ inputs.channel == 'stable' }}
- run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
+ FLUTTER_VERSION: 3.22.3
- - name: Create Nightly .env
- if: ${{ inputs.channel == 'nightly' }}
- run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
-
- - name: Replace Version in files
- run: |
- sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml
-
- - name: Generate Secrets
- run: |
- flutter config --enable-linux-desktop
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - name: Build Linux Packages
- run: |
- dart pub global activate flutter_distributor
- alias dpkg-deb="dpkg-deb --Zxz"
- flutter_distributor package --platform=linux --targets=deb
- flutter_distributor package --platform=linux --targets=rpm
-
- - name: Create tar.xz (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: make tar VERSION=${{ env.BUILD_VERSION }} ARCH=x64 PKG_ARCH=x86_64
-
- - name: Create tar.xz (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: make tar VERSION=nightly ARCH=x64 PKG_ARCH=x86_64
-
- - name: Move Files to dist
- run: |
- mv build/spotube-linux-*-x86_64.tar.xz dist/
- mv dist/**/spotube-*-linux.deb dist/Spotube-linux-x86_64.deb
- mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm
+permissions:
+ contents: write
-
- - uses: actions/upload-artifact@v3
- if: ${{ inputs.channel == 'stable' }}
- with:
- if-no-files-found: error
- name: Spotube-Release-Binaries
- path: |
- dist/Spotube-linux-x86_64.deb
- dist/Spotube-linux-x86_64.rpm
- dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
-
- - uses: actions/upload-artifact@v3
- if: ${{ inputs.channel == 'nightly' }}
- with:
- if-no-files-found: error
- name: Spotube-Release-Binaries
- path: |
- dist/Spotube-linux-x86_64.deb
- dist/Spotube-linux-x86_64.rpm
- dist/spotube-linux-nightly-x86_64.tar.xz
-
- - name: Debug With SSH When fails
- if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
- uses: mxschmitt/action-tmate@v3
- with:
- limit-access-to-actor: true
-
-
- android:
- runs-on: ubuntu-latest
+jobs:
+ build_platform:
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-latest
+ platform: linux
+ files: |
+ dist/Spotube-linux-x86_64.deb
+ dist/Spotube-linux-x86_64.rpm
+ dist/spotube-linux-*-x86_64.tar.xz
+ - os: ubuntu-latest
+ platform: linux_arm
+ files: |
+ dist/Spotube-linux-aarch64.deb
+ dist/spotube-linux-*-aarch64.tar.xz
+ - os: ubuntu-latest
+ platform: android
+ files: |
+ build/Spotube-android-all-arch.apk
+ build/Spotube-playstore-all-arch.aab
+ - os: windows-latest
+ platform: windows
+ files: |
+ dist/Spotube-windows-x86_64.nupkg
+ dist/Spotube-windows-x86_64-setup.exe
+ - os: macos-latest
+ platform: ios
+ files: |
+ Spotube-iOS.ipa
+ - os: macos-14
+ platform: macos
+ files: |
+ build/Spotube-macos-universal.dmg
+ build/Spotube-macos-universal.pkg
+ runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2.12.0
with:
cache: true
+ cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
flutter-version: ${{ env.FLUTTER_VERSION }}
-
- - name: Install Dependencies
- run: |
- sudo apt-get update -y
- sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse xmlstarlet
-
- - name: Replace pubspec version and BUILD_VERSION Env (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: |
- curl -sS https://webi.sh/yq | sh
- yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: BUILD_VERSION Env (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: |
- echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
-
- - name: Create Stable .env
- if: ${{ inputs.channel == 'stable' }}
- run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
-
- - name: Create Nightly .env
- if: ${{ inputs.channel == 'nightly' }}
- run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
-
- - name: Generate Secrets
+ - name: Setup Java
+ if: ${{matrix.platform == 'android'}}
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '17'
+ cache: 'gradle'
+ check-latest: true
+ - name: Set up QEMU
+ if: ${{matrix.platform == 'linux_arm'}}
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ if: ${{matrix.platform == 'linux_arm'}}
+ uses: docker/setup-buildx-action@v3
+ - name: Setup Rust toolchain
+ if: ${{matrix.platform != 'linux_arm'}}
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Install ${{matrix.platform}} dependencies
run: |
flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
+ dart cli/cli.dart install-dependencies --platform=${{matrix.platform}}
- name: Sign Apk
+ if: ${{matrix.platform == 'android'}}
run: |
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
-
- - name: Build Apk
- run: |
- flutter build apk --flavor ${{ inputs.channel }}
- mv build/app/outputs/flutter-apk/app-${{ inputs.channel }}-release.apk build/Spotube-android-all-arch.apk
-
- - name: Build Playstore AppBundle
- run: |
- echo 'ENABLE_UPDATE_CHECK=0' >> .env
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
- export MANIFEST=android/app/src/main/AndroidManifest.xml
- xmlstarlet ed -d '//meta-data[@android:name="com.google.android.gms.car.application"]' $MANIFEST > $MANIFEST.tmp
- mv $MANIFEST.tmp $MANIFEST
- flutter build appbundle --flavor ${{ inputs.channel }}
- mv build/app/outputs/bundle/${{ inputs.channel }}Release/app-${{ inputs.channel }}-release.aab build/Spotube-playstore-all-arch.aab
-
-
- - uses: actions/upload-artifact@v3
- with:
- if-no-files-found: error
- name: Spotube-Release-Binaries
- path: |
- build/Spotube-android-all-arch.apk
- build/Spotube-playstore-all-arch.aab
-
- - name: Debug With SSH When fails
- if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
- uses: mxschmitt/action-tmate@v3
- with:
- limit-access-to-actor: true
-
- macos:
-
- runs-on: macos-14
- steps:
- - uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.12.0
- with:
- cache: true
- flutter-version: ${{ env.FLUTTER_VERSION }}
-
- - name: Replace pubspec version and BUILD_VERSION Env (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: |
- brew install yq
- yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: BUILD_VERSION Env (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: |
- echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
-
- - name: Create Stable .env
- if: ${{ inputs.channel == 'stable' }}
- run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
-
- - name: Create Nightly .env
- if: ${{ inputs.channel == 'nightly' }}
- run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
-
- - name: Generate Secrets
- run: |
- dart pub global activate flutter_distributor
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - name: Build Macos App
- run: |
- flutter config --enable-macos-desktop
- flutter build macos
- du -sh build/macos/Build/Products/Release/spotube.app
-
- - name: Package Macos App
+
+ - name: Unessary hosted tools
+ if: ${{matrix.platform == 'linux_arm'}}
run: |
- brew install python-setuptools
- npm install -g appdmg
- mkdir -p build/${{ env.BUILD_VERSION }}
- appdmg appdmg.json build/Spotube-macos-universal.dmg
- flutter_distributor package --platform=macos --targets pkg --skip-clean
- mv dist/**/spotube-*-macos.pkg build/Spotube-macos-universal.pkg
-
+ sudo rm -rf /usr/share/dotnet
+
+ - name: Build ${{matrix.platform}} binaries
+ run: dart cli/cli.dart build ${{matrix.platform}}
+ env:
+ CHANNEL: ${{inputs.channel}}
+ DOTENV: ${{secrets.DOTENV_RELEASE}}
+
- uses: actions/upload-artifact@v3
with:
if-no-files-found: error
name: Spotube-Release-Binaries
- path: |
- build/Spotube-macos-universal.dmg
- build/Spotube-macos-universal.pkg
+ path: ${{matrix.files}}
- name: Debug With SSH When fails
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
@@ -348,70 +122,12 @@ jobs:
with:
limit-access-to-actor: true
- iOS:
- runs-on: macos-14
- steps:
- - uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.10.0
- with:
- cache: true
- flutter-version: ${{ env.FLUTTER_VERSION }}
-
- - name: Replace pubspec version and BUILD_VERSION Env (nightly)
- if: ${{ inputs.channel == 'nightly' }}
- run: |
- brew install yq
- yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
-
- - name: BUILD_VERSION Env (stable)
- if: ${{ inputs.channel == 'stable' }}
- run: |
- echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
-
- - name: Create Stable .env
- if: ${{ inputs.channel == 'stable' }}
- run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
-
- - name: Create Nightly .env
- if: ${{ inputs.channel == 'nightly' }}
- run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
-
- - name: Generate Secrets
- run: |
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - name: Build iOS iPA
- run: |
- flutter build ios --release --no-codesign --flavor ${{ inputs.channel }}
- ln -sf ./build/ios/iphoneos Payload
- zip -r9 Spotube-iOS.ipa Payload/${{ inputs.channel }}.app
-
- - uses: actions/upload-artifact@v3
- with:
- if-no-files-found: error
- name: Spotube-Release-Binaries
- path: |
- Spotube-iOS.ipa
-
- - name: Debug With SSH When fails
- if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
- uses: mxschmitt/action-tmate@v3
- with:
- limit-access-to-actor: true
-
upload:
runs-on: ubuntu-latest
-
needs:
- - windows
- - linux
- - android
- - macos
- - iOS
+ - build_platform
steps:
+ - uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: Spotube-Release-Binaries
@@ -426,6 +142,10 @@ jobs:
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
+
+ - name: Extract pubspec version
+ run: |
+ echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
- uses: actions/upload-artifact@v3
with:
@@ -440,7 +160,7 @@ jobs:
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- tag: v${{ inputs.version }} # mind the "v" prefix
+ tag: v${{ env.PUBSPEC_VERSION }} # mind the "v" prefix
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
omitPrereleaseDuringUpdate: true
@@ -458,3 +178,8 @@ jobs:
omitPrereleaseDuringUpdate: true
allowUpdates: true
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
+ body: |
+ Build Number: ${{github.run_number}}
+
+ Nightly release includes newest features but may contain bugs
+ It is preferred to use the stable version unless you know what you're doing
diff --git a/.gitignore b/.gitignore
index 96d810871..4f9ebc281 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,3 +76,5 @@ android/key.properties
.fvm/flutter_sdk
**/pb_data
+
+tm.json
diff --git a/.metadata b/.metadata
index 082985ad4..828f2c0ad 100644
--- a/.metadata
+++ b/.metadata
@@ -1,11 +1,11 @@
# 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.
+# This file should be version controlled and should not be manually edited.
version:
- revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- channel: stable
+ revision: "300451adae589accbece3490f4396f10bdf15e6e"
+ channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- - platform: macos
- create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
- base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
+ create_revision: 300451adae589accbece3490f4396f10bdf15e6e
+ base_revision: 300451adae589accbece3490f4396f10bdf15e6e
+ - platform: windows
+ create_revision: 300451adae589accbece3490f4396f10bdf15e6e
+ base_revision: 300451adae589accbece3490f4396f10bdf15e6e
# User provided section
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 29c5ba4e0..0ec6ca766 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -17,6 +17,7 @@
"songlink",
"speechiness",
"Spotube",
+ "titlebar",
"winget"
],
"editor.formatOnSave": true,
@@ -24,5 +25,6 @@
"explorer.fileNesting.patterns": {
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
+ "*.dart": "${capture}.g.dart,${capture}.freezed.dart",
}
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21ca4b693..7e4345740 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,91 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
-## [3.6.0-0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0-0) (2024-04-15)
+## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06)
+
+### Features
+
+- translations: make state page's hard coded strings translatable (#1719)
+- discord: add listening activity type
+- discord: album art, playing time and play pause support (#1765)
+- linux: Use XDG_STATE_HOME to storage logs (#1675)
+- discord rpc for macOS, windows-arm64 and linux-arm64 (#1713)
+- desktop: implement webview based login
+- stats: add lazy loading support
+
+### Bug Fixes
+
+- translations: fix Russian translations (#1696)
+- ios: permission exception
+- linux: tray icon wrong name for flatpak
+- windows: app crashes when no internet
+- windows: local tracks plays but disabled playback controls
+- go to track album shows up for local tracks
+- local track metadata timeout
+- windows: window stretching #1553
+- android: app getting killed from background
+- linux: OS Media control not working for Flatpak #1627
+- incorrect datatype used for MPRIS position property #1521
+- Too many artists for a track causing overflows
+- playlist share button does not work #1639
+- unescape html escape values #1300
+- lyrics page doesn't scroll to top after song ends #885
+- changed source doesn't get saved and uses the wrong once again
+- null exception in album page navigated from /home
+- popup menu item opacity
+- linux: change app id in flatpak environment
+
+
+## [3.7.1](https://github.com/krtirtho/spotube/compare/v3.7.0...v3.7.1) (2024-06-06)
+
+
+### Bug Fixes
+
+* alternative sources not showing up for SongLink matched results ([37d002d](https://github.com/krtirtho/spotube/commit/37d002d133cacb3a34884713ac8f6637694af57c))
+* **android:** Media Controls not working above Android 14 [#1561](https://github.com/krtirtho/spotube/issues/1561) ([3394c1b](https://github.com/krtirtho/spotube/commit/3394c1b0574e44dc624b2b2f0bf32f343ae9f049))
+* browse anonymously button takes to wrong route ([73c5b30](https://github.com/krtirtho/spotube/commit/73c5b30b63a4c82bb7ad5b52bc10c5594566a800))
+* **desktop:** titlebar drag to move not working ([5f280a1](https://github.com/krtirtho/spotube/commit/5f280a19f4d5f8882ae2ff60c6cc595ded7a5a1d))
+* **desktop:** window is not centered ([47f98b9](https://github.com/krtirtho/spotube/commit/47f98b98aafab9b426733ed44cab2be8a646a98e))
+* **ios:** download not working [#1575](https://github.com/krtirtho/spotube/issues/1575) ([6591ec0](https://github.com/krtirtho/spotube/commit/6591ec0e1b441dd8fd535eace19a58c7749389ca))
+* **linux:** application window not visible after launch ([8fc44ed](https://github.com/krtirtho/spotube/commit/8fc44ed6550e8b2b804991ff82df08afb1c94ca8))
+* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5))
+* use weak match for Jiosaavn fallback to improve matching ([6cb2986](https://github.com/krtirtho/spotube/commit/6cb29868d2030b5e9312863c17e8f24889942e24))
+* **windows:** media controls not showing up [#1542](https://github.com/krtirtho/spotube/issues/1542) ([d7d864f](https://github.com/krtirtho/spotube/commit/d7d864ff2bc937675a544a7edf645c5148ec836a))
+* **windows:** revert Flutter version to 3.19.6 to avoid distortion [#1553](https://github.com/krtirtho/spotube/issues/1553) ([982cf0b](https://github.com/krtirtho/spotube/commit/982cf0bd435638fa20f17ef527fe21d031b5ffaf))
+
+## [3.7.0](https://github.com/krtirtho/spotube/compare/v3.6.0...v3.7.0) (2024-06-03)
+
+
+### Features
+
+* local library folder cards ([fc5bfa0](https://github.com/krtirtho/spotube/commit/fc5bfa089ce2f46ab786565d6750564d704ee7e0))
+* Local music library ([#1479](https://github.com/krtirtho/spotube/issues/1479)) ([22caa81](https://github.com/krtirtho/spotube/commit/22caa818f4ac31626aaff6952e43512b42237d00))
+* personalized stats based on local music history ([#1522](https://github.com/krtirtho/spotube/issues/1522)) ([82307bc](https://github.com/krtirtho/spotube/commit/82307bc030035b03ab1b8d8ec7b24da19a866b12))
+* play initially available tracks of playlist/album immediately and fetch rest in background [#670](https://github.com/krtirtho/spotube/issues/670) ([02acbd9](https://github.com/krtirtho/spotube/commit/02acbd93271145dde365f6c547e0d9d902be65f1))
+* **player:** add volume slider floating label showing percentage ([#1445](https://github.com/krtirtho/spotube/issues/1445)) ([8fad225](https://github.com/krtirtho/spotube/commit/8fad2251b3536e9468e0fb193939ead98bad3bc6)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
+* **translations:** add Basque translation ([#1493](https://github.com/krtirtho/spotube/issues/1493)) ([dbc1c45](https://github.com/krtirtho/spotube/commit/dbc1c452dd53153c61589f956ea9836cea7bf2bb))
+* **translations:** add Finnish translations ([#1449](https://github.com/krtirtho/spotube/issues/1449)) ([edc997e](https://github.com/krtirtho/spotube/commit/edc997e7470ce17f60c96b8198dc8851cbf21f18)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
+* **translations:** add georgian language ([#1450](https://github.com/krtirtho/spotube/issues/1450)) ([1e7f0e1](https://github.com/krtirtho/spotube/commit/1e7f0e1fe71e0a8d86614fc884861f8791469112))
+* **translations:** add Indonesian translation ([#1426](https://github.com/krtirtho/spotube/issues/1426)) ([0280654](https://github.com/krtirtho/spotube/commit/0280654bb6bad373aee521f5a866228d2d38f038)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
+* **translations:** Improve tr locales ([#1419](https://github.com/krtirtho/spotube/issues/1419)) ([bf45681](https://github.com/krtirtho/spotube/commit/bf45681deb951c772bf6ca05e213c949c04bded1)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
+* upgrade to Flutter 3.22.0 ([71341ec](https://github.com/krtirtho/spotube/commit/71341ec0bda6ed985b43836712075b97a2cf8bac))
+
+
+### Bug Fixes
+
+* fallback to LRCLIB when lyrics line less than 6 lines [#1461](https://github.com/krtirtho/spotube/issues/1461) ([9aea354](https://github.com/krtirtho/spotube/commit/9aea35468fa7cd176ddc8810b37b90c2d8246931))
+* **linux:** tray icon not showing [#541](https://github.com/krtirtho/spotube/issues/541) ([7ac7917](https://github.com/krtirtho/spotube/commit/7ac791757abb30f40374c169c4211916287bb3f3))
+* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5))
+* **macos:** Logs directory not created by default [#1353](https://github.com/krtirtho/spotube/issues/1353) ([4ca8939](https://github.com/krtirtho/spotube/commit/4ca893950b07f678acf7db690112c47d21e54782))
+* **playback:** skipping tracks with unplayable sources instead of falling back [#1492](https://github.com/krtirtho/spotube/issues/1492) ([c607a33](https://github.com/krtirtho/spotube/commit/c607a330ed279dfbebe8d4bd325745ac6301a58f))
+* **search:** load more button not working [#1417](https://github.com/krtirtho/spotube/issues/1417) ([7e07c2e](https://github.com/krtirtho/spotube/commit/7e07c2e1985da7ccb96b1fac2ecd703720068d26))
+* some text are garbled in different parts of the app [#1463](https://github.com/krtirtho/spotube/issues/1463) [#1505](https://github.com/krtirtho/spotube/issues/1505) ([d2683c5](https://github.com/krtirtho/spotube/commit/d2683c52d81d807be6ff72f15b8e9eb18181e211))
+* spotify friends and user profile icon (mobile) showing when not authenticated [#1410](https://github.com/krtirtho/spotube/issues/1410) ([9bccbc9](https://github.com/krtirtho/spotube/commit/9bccbc93c63dd34f6e15ff68c276976ecd1d9a33))
+* **updater:** dead link ([#1408](https://github.com/krtirtho/spotube/issues/1408)) ([6907f9c](https://github.com/krtirtho/spotube/commit/6907f9c756d8f49aadb1b23a2a1dc8bf7d658dc0)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
+* windows SSL Certificate error breaking login [#905](https://github.com/krtirtho/spotube/issues/905) ([#1474](https://github.com/krtirtho/spotube/issues/1474)) ([937a706](https://github.com/krtirtho/spotube/commit/937a706ac9c0e59943b2609e5cc398dcdbed2344)), closes [#1468](https://github.com/krtirtho/spotube/issues/1468)
+* **windows:** installer tries to install in current directory ([c3c9fc5](https://github.com/krtirtho/spotube/commit/c3c9fc544c68b3d897dd7241a61cab7a199b4539))
+
+## [3.6.0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0) (2024-04-15)
### Features
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index e859f9e6f..d4746a1a9 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -32,7 +32,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
This project and everyone participating in it is governed by the
[Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
-to <>.
+to krtirtho@gmail.com.
## I Have a Question
@@ -123,16 +123,16 @@ Do the following:
- Install Development dependencies in linux
- Debian (>=12/Bookworm)/Ubuntu
```bash
- $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan
+ $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev
```
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
- Arch/Manjaro
```bash
- yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan
+ yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan webkit2gtk-4.1 libsoup3
```
- Fedora
```bash
- dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns
+ dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns webkit2gtk4.1 webkit2gtk4.1-devel libsoup3 libsoup3-devel
```
- Clone the Repo
- Create a `.env` in root of the project following the `.env.example` template
diff --git a/README.md b/README.md
index f2666fbc2..71c879bac 100644
--- a/README.md
+++ b/README.md
@@ -210,116 +210,116 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
### Dependencies
+1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
+1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
+1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
+1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
-1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons
+1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration.
+1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
+1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop.
1. [device_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
-1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device.
1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
+1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
+1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
+1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
+1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
+1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
+1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
+1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
+1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
+1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
+1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
+1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
-1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
+1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
+1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
+1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
1. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
+1. [io](https://pub.dev/packages/io) - Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values.
+1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
+1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
+1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
-1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
+1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
+1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
-1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
+1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
+1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
+1. [process_run](https://github.com/tekartik/process_run.dart/blob/master/packages/process_run) - Process run helpers for Linux/Win/Mac and which like feature for finding executables.
+1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
+1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
+1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
+1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
-1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
+1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
+1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
+1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
+1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
+1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
+1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
+1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
+1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
+1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime.
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
+1. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray.
1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart
1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/
-1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
-1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
-1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
-1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
-1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
-1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
-1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more.
-1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
-1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
-1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
+1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
+1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel.
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
-1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
-1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
-1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
-1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
-1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
-1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
-1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
-1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
-1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
-1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
-1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel.
-1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
-1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
-1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
-1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime.
-1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
-1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
-1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
-1. [flutter_distributor](https://distributor.leanflutter.dev) - A complete tool for packaging and publishing your Flutter apps.
-1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
-1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
-1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
-1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
-1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
-1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
-1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
-1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
-1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
-1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
-1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
-1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
-1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
-1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
+1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
+1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
+1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
© Copyright Spotube 2024
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2f85cdebc..8ec1872e0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@@ -34,7 +31,7 @@ if (keystorePropertiesFile.exists()) {
android {
compileSdkVersion 34
- ndkVersion "21.4.7075529"
+ ndkVersion "25.1.8937393"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -50,10 +47,9 @@ android {
}
defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "oss.krtirtho.spotube"
minSdkVersion 24
- targetSdkVersion flutter.targetSdkVersion
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -71,6 +67,9 @@ android {
release {
signingConfig signingConfigs.release
}
+ debug {
+ signingConfig signingConfigs.release
+ }
}
flavorDimensions "default"
@@ -81,16 +80,19 @@ android {
resValue "string", "app_name_en", "Spotube Nightly"
applicationIdSuffix ".nightly"
versionNameSuffix "-nightly"
+ signingConfig signingConfigs.release
}
dev {
dimension "default"
resValue "string", "app_name_en", "Spotube Dev"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
+ signingConfig signingConfigs.release
}
stable {
dimension "default"
resValue "string", "app_name_en", "Spotube"
+ signingConfig signingConfigs.release
}
}
@@ -101,15 +103,6 @@ flutter {
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- constraints {
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
- because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
- }
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
- because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
- }
- }
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
// other deps so just ignore
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5ab7a0b51..64c32e28d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
@@ -24,6 +25,11 @@
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
>
+
+
+
properties.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
-def flutterSdkPath = properties.getProperty("flutter.sdk")
-assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
-apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "7.2.1" apply false
+ id "org.jetbrains.kotlin.android" version "1.8.22" apply false
+}
+
+include ":app"
\ No newline at end of file
diff --git a/aur-struct/.SRCINFO b/aur-struct/.SRCINFO
index ae0b6d10c..29eedf742 100644
--- a/aur-struct/.SRCINFO
+++ b/aur-struct/.SRCINFO
@@ -1,17 +1,17 @@
pkgbase = spotube-bin
- pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
- pkgver = 2.3.0
- pkgrel = 1
- url = https://github.com/KRTirtho/spotube/
- arch = x86_64
- license = BSD-4-Clause
- depends = mpv
- depends = libappindicator-gtk3
- depends = libsecret
- depends = jsoncpp
- depends = libnotify
- depends = xdg-user-dirs
- source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz
- md5sums = 8cd6a7385c5c75d203dccd762f1d63ec
+pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
+pkgver = 3.7.1
+pkgrel = 2
+url = https://github.com/KRTirtho/spotube/
+arch = x86_64
+license = BSD-4-Clause
+depends = mpv
+depends = libappindicator-gtk3
+depends = libsecret
+depends = jsoncpp
+depends = libnotify
+depends = xdg-user-dirs
+source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
+md5sums = 475b1ae9b08f27743a4d4749391ae3db
pkgname = spotube-bin
diff --git a/bin/gen-credits.dart b/bin/gen-credits.dart
deleted file mode 100644
index f8975335f..000000000
--- a/bin/gen-credits.dart
+++ /dev/null
@@ -1,103 +0,0 @@
-import 'dart:developer';
-import 'dart:io';
-
-import 'package:collection/collection.dart';
-import 'package:http/http.dart';
-import 'package:html/parser.dart';
-import 'package:pub_api_client/pub_api_client.dart';
-import 'package:pubspec_parse/pubspec_parse.dart';
-
-void main() async {
- final client = PubClient();
-
- final pubspec = Pubspec.parse(File('pubspec.yaml').readAsStringSync());
-
- final allDeps = [
- ...pubspec.dependencies.entries,
- ...pubspec.devDependencies.entries,
- ];
-
- final dependencies = allDeps
- .where((d) => d.value is HostedDependency)
- .map((d) => d.key)
- .toSet();
- final packageInfo = await Future.wait(dependencies.map(client.packageInfo));
-
- final gitDepsList = List.castFrom,
- MapEntry>(
- allDeps
- .where((d) => d.value is GitDependency)
- .map((d) => MapEntry(d.key, d.value as GitDependency))
- .toList(),
- );
-
- final gitDeps = gitDepsList.map(
- (d) {
- final uri = Uri.parse(
- d.value.url.toString().replaceAll('.git', ''),
- );
- return MapEntry(
- d.key,
- uri.replace(
- pathSegments: [
- ...uri.pathSegments,
- 'raw',
- d.value.ref ?? 'main',
- d.value.path ?? '',
- 'pubspec.yaml',
- ],
- ).toString(),
- );
- },
- ).toList();
-
- final gitPubspecs = await Future.wait(
- gitDeps.map(
- (d) {
- Pubspec parser(res) {
- try {
- return Pubspec.parse(res.body);
- } catch (e) {
- final document = parse(res.body);
- final pre = document.querySelector('pre');
- if (pre == null) {
- log(d.toString());
- rethrow;
- }
- return Pubspec.parse(pre.text);
- }
- }
-
- return get(Uri.parse(d.value)).then(parser).catchError(
- (_) => get(Uri.parse(d.value.replaceFirst('/main', '/master')))
- .then(parser),
- );
- },
- ),
- );
-
- // ignore: avoid_print
- print(
- packageInfo
- .map(
- (package) =>
- '1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}',
- )
- .join('\n'),
- );
- // ignore: avoid_print
- print(
- gitPubspecs.map(
- (package) {
- final packageUrl = package.homepage ??
- gitDepsList
- .firstWhereOrNull((dep) => dep.key == package.name)
- ?.value
- .url
- .toString();
- return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}';
- },
- ).join('\n'),
- );
- exit(0);
-}
diff --git a/bin/translated_messages.dart b/bin/translated_messages.dart
deleted file mode 100644
index 1ac8f148f..000000000
--- a/bin/translated_messages.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-// ignore_for_file: avoid_print
-
-import 'dart:convert';
-import 'dart:io';
-
-void main(List args) async {
- final translatedFile =
- jsonDecode(await File('tm.json').readAsString()) as Map;
-
- for (final MapEntry(:key, :value) in translatedFile.entries) {
- print('Updating locale: $key');
- final file = File('lib/l10n/app_$key.arb');
-
- final fileContent =
- jsonDecode(await file.readAsString()) as Map;
-
- final newContent = {
- ...fileContent,
- ...value,
- };
-
- await file.writeAsString(
- const JsonEncoder.withIndent(' ').convert(newContent),
- );
-
- print('✅ Updated locale: $key');
- }
-}
diff --git a/bin/untranslated_messages.dart b/bin/untranslated_messages.dart
deleted file mode 100644
index 0b3485a7b..000000000
--- a/bin/untranslated_messages.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-// ignore_for_file: avoid_print
-
-import 'dart:convert';
-import 'dart:io';
-
-/// Generate JSON output for untranslated messages with English values
-/// for quick translation in ChatGPT
-///
-/// Usage: dart bin/untranslated_messages.dart [locale?]
-///
-/// Example: dart bin/untranslated_messages.dart
-///
-/// or with specific locale (e.g. bn (Bengali))
-///
-/// Example: dart bin/untranslated_messages.dart bn
-
-void main(List args) {
- final file = jsonDecode(
- File('untranslated_messages.json').readAsStringSync(),
- ) as Map;
-
- final englishMessages =
- jsonDecode(File('lib/l10n/app_en.arb').readAsStringSync())
- as Map;
-
- final messagesWithValues = {};
-
- for (final MapEntry(key: locale, value: messages) in file.entries) {
- messagesWithValues[locale] = Map.fromEntries(
- messages
- .map(
- (message) =>
- MapEntry(message, englishMessages[message]),
- )
- .toList()
- .cast>(),
- );
- }
-
- print(
- "Prompt:\n"
- "Translate following to their appropriate locale for flutter arb translations files."
- " Put the respective new translations in a map of their corresponding locale.",
- );
- print(
- const JsonEncoder.withIndent(' ').convert(
- args.isNotEmpty ? messagesWithValues[args.first] : messagesWithValues,
- ),
- );
-}
diff --git a/bin/verify-pkgbuild.dart b/bin/verify-pkgbuild.dart
deleted file mode 100644
index 587e63d07..000000000
--- a/bin/verify-pkgbuild.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'dart:convert';
-import 'dart:io';
-
-void main() {
- Process.run("sh", ["-c", '"./scripts/pkgbuild2json.sh aur-struct/PKGBUILD"'])
- .then((result) {
- try {
- final pkgbuild = jsonDecode(result.stdout);
- if (pkgbuild["version"] !=
- Platform.environment["RELEASE_VERSION"]?.substring(1)) {
- throw Exception(
- "PKGBUILD version doesn't match current RELEASE_VERSION");
- }
- if (pkgbuild["release"] != "1") {
- throw Exception("In new releases pkgrel should be 1");
- }
- } catch (e) {
- // ignore: avoid_print
- print("[Failed to parse PKGBUILD] $e");
- }
- });
-}
diff --git a/build.yaml b/build.yaml
index f074d6e15..8dbfe45d6 100644
--- a/build.yaml
+++ b/build.yaml
@@ -2,4 +2,16 @@ targets:
$default:
sources:
exclude:
- - bin/*.dart
\ No newline at end of file
+ - bin/*.dart
+ builders:
+ json_serializable:
+ options:
+ any_map: true
+ explicit_to_json: true
+ drift_dev:
+ options:
+ sql:
+ dialect: sqlite
+ options:
+ modules:
+ - json1
diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 000000000..b2ba8ebda
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,4 @@
+## Spotube Configuration CLI
+
+This is used for building the project for multiple platforms and having utilities specific for the project.
+Written in Dart
diff --git a/cli/cli.dart b/cli/cli.dart
new file mode 100644
index 000000000..26190d4c4
--- /dev/null
+++ b/cli/cli.dart
@@ -0,0 +1,22 @@
+import 'package:args/command_runner.dart';
+
+import 'commands/build.dart';
+import 'commands/credits.dart';
+import 'commands/install-dependencies.dart';
+import 'commands/translated.dart';
+import 'commands/untranslated.dart';
+
+void main(List args) {
+ final commandRunner = CommandRunner(
+ "cli",
+ "Configuration CLI for Spotube",
+ );
+
+ commandRunner.addCommand(InstallDependenciesCommand());
+ commandRunner.addCommand(BuildCommand());
+ commandRunner.addCommand(CreditsCommand());
+ commandRunner.addCommand(TranslatedCommand());
+ commandRunner.addCommand(UntranslatedCommand());
+
+ commandRunner.run(args);
+}
diff --git a/cli/commands/build.dart b/cli/commands/build.dart
new file mode 100644
index 000000000..fdf35a952
--- /dev/null
+++ b/cli/commands/build.dart
@@ -0,0 +1,25 @@
+import 'package:args/command_runner.dart';
+
+import 'build/android.dart';
+import 'build/ios.dart';
+import 'build/linux.dart';
+import 'build/linux_arm.dart';
+import 'build/macos.dart';
+import 'build/windows.dart';
+
+class BuildCommand extends Command {
+ @override
+ String get description => "Build for different platforms";
+
+ @override
+ String get name => "build";
+
+ BuildCommand() {
+ addSubcommand(AndroidBuildCommand());
+ addSubcommand(IosBuildCommand());
+ addSubcommand(LinuxBuildCommand());
+ addSubcommand(LinuxArmBuildCommand());
+ addSubcommand(MacosBuildCommand());
+ addSubcommand(WindowsBuildCommand());
+ }
+}
diff --git a/cli/commands/build/android.dart b/cli/commands/build/android.dart
new file mode 100644
index 000000000..800522b8a
--- /dev/null
+++ b/cli/commands/build/android.dart
@@ -0,0 +1,90 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:collection/collection.dart';
+import 'package:path/path.dart';
+import 'package:xml/xml.dart';
+
+import '../../core/env.dart';
+import 'common.dart';
+
+class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "Build for android";
+
+ @override
+ String get name => "android";
+
+ @override
+ FutureOr? run() async {
+ await bootstrap();
+
+ await shell.run(
+ "flutter build apk --flavor ${CliEnv.channel.name}",
+ );
+
+ await dotEnvFile.writeAsString(
+ "\nENABLE_UPDATE_CHECK=0",
+ mode: FileMode.append,
+ );
+
+ final androidManifestFile = File(
+ join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml"));
+
+ final androidManifestXml =
+ XmlDocument.parse(await androidManifestFile.readAsString());
+
+ final deletingElement =
+ androidManifestXml.findAllElements("meta-data").firstWhereOrNull(
+ (el) =>
+ el.getAttribute("android:name") ==
+ "com.google.android.gms.car.application",
+ );
+
+ deletingElement?.parent?.children.remove(deletingElement);
+
+ await androidManifestFile.writeAsString(
+ androidManifestXml.toXmlString(pretty: true),
+ );
+
+ await shell.run(
+ """
+ dart run build_runner build --delete-conflicting-outputs
+ flutter build appbundle --flavor ${CliEnv.channel.name}
+ """,
+ );
+
+ final ogApkFile = File(
+ join(
+ "build",
+ "app",
+ "outputs",
+ "flutter-apk",
+ "app-${CliEnv.channel.name}-release.apk",
+ ),
+ );
+
+ await ogApkFile.copy(
+ join(cwd.path, "build", "Spotube-android-all-arch.apk"),
+ );
+
+ final ogAppbundleFile = File(
+ join(
+ cwd.path,
+ "build",
+ "app",
+ "outputs",
+ "bundle",
+ "${CliEnv.channel.name}Release",
+ "app-${CliEnv.channel.name}-release.aab",
+ ),
+ );
+
+ await ogAppbundleFile.copy(
+ join(cwd.path, "build", "Spotube-playstore-all-arch.aab"),
+ );
+
+ stdout.writeln("✅ Built Android Apk and Appbundle");
+ }
+}
diff --git a/cli/commands/build/common.dart b/cli/commands/build/common.dart
new file mode 100644
index 000000000..4c7e3e510
--- /dev/null
+++ b/cli/commands/build/common.dart
@@ -0,0 +1,66 @@
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+import 'package:process_run/shell_run.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+
+import '../../core/env.dart';
+
+mixin BuildCommandCommonSteps on Command {
+ final shell = Shell();
+ Directory get cwd => Directory.current;
+
+ Pubspec? _pubspec;
+
+ Pubspec get pubspec {
+ if (_pubspec != null) {
+ return _pubspec!;
+ }
+
+ final pubspecFile = File(join(cwd.path, "pubspec.yaml"));
+ _pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
+
+ return _pubspec!;
+ }
+
+ String get versionWithoutBuildNumber {
+ return "${pubspec.version!.major}.${pubspec.version!.minor}.${pubspec.version!.patch}";
+ }
+
+ RegExp get versionVarRegExp =>
+ RegExp(r"\%\{\{SPOTUBE_VERSION\}\}\%", multiLine: true);
+
+ File get dotEnvFile => File(join(cwd.path, ".env"));
+
+ Future bootstrap() async {
+ await dotEnvFile.create(recursive: true);
+
+ await dotEnvFile.writeAsString(
+ "${CliEnv.dotenv}\n"
+ "RELEASE_CHANNEL=${CliEnv.channel.name}\n",
+ );
+
+ if (CliEnv.channel == BuildChannel.nightly) {
+ final pubspecFile = File(join(cwd.path, "pubspec.yaml"));
+
+ pubspecFile.writeAsStringSync(
+ pubspecFile.readAsStringSync().replaceAll(
+ "version: ${pubspec.version!.canonicalizedVersion}",
+ "version: $versionWithoutBuildNumber+${CliEnv.ghRunNumber}",
+ ),
+ );
+
+ _pubspec = null;
+ pubspec;
+ }
+
+ await shell.run(
+ """
+ flutter pub get
+ dart run build_runner build --delete-conflicting-outputs
+ dart pub global activate flutter_distributor
+ """,
+ );
+ }
+}
diff --git a/cli/commands/build/ios.dart b/cli/commands/build/ios.dart
new file mode 100644
index 000000000..6460f9edb
--- /dev/null
+++ b/cli/commands/build/ios.dart
@@ -0,0 +1,29 @@
+import 'dart:async';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+
+import '../../core/env.dart';
+import 'common.dart';
+
+class IosBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "iOS build command";
+
+ @override
+ String get name => "ios";
+
+ @override
+ FutureOr? run() async {
+ await bootstrap();
+
+ final buildDirPath = join(cwd.path, "build", "ios", "iphoneos");
+ await shell.run(
+ """
+ flutter build ios --release --no-codesign --flavor ${CliEnv.channel.name}
+ ln -sf $buildDirPath Payload
+ zip -r9 Spotube-iOS.ipa ${join("Payload", "${CliEnv.channel.name}.app")}
+ """,
+ );
+ }
+}
diff --git a/cli/commands/build/linux.dart b/cli/commands/build/linux.dart
new file mode 100644
index 000000000..a218720ce
--- /dev/null
+++ b/cli/commands/build/linux.dart
@@ -0,0 +1,106 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:io/io.dart';
+import 'package:args/command_runner.dart';
+import 'package:intl/intl.dart';
+import 'package:path/path.dart';
+
+import '../../core/env.dart';
+import 'common.dart';
+
+class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "Linux build command";
+
+ @override
+ String get name => "linux";
+
+ @override
+ FutureOr? run() async {
+ stdout.writeln("Replacing versions");
+
+ final appDataFile = File(
+ join(cwd.path, "linux", "com.github.KRTirtho.Spotube.appdata.xml"),
+ );
+
+ appDataFile.writeAsStringSync(
+ appDataFile.readAsStringSync().replaceAll(
+ versionVarRegExp,
+ '',
+ ),
+ );
+
+ await bootstrap();
+
+ await shell.run(
+ """
+ flutter_distributor package --platform=linux --targets=deb
+ flutter_distributor package --platform=linux --targets=rpm
+ """,
+ );
+
+ final tempDir = join(Directory.systemTemp.path, "spotube-tar");
+
+ final bundleDirPath =
+ join(cwd.path, "build", "linux", "x64", "release", "bundle");
+
+ final tarFile = File(join(
+ cwd.path,
+ "dist",
+ "spotube-linux-"
+ "${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}"
+ "-x86_64.tar.xz",
+ ));
+
+ await copyPath(bundleDirPath, tempDir);
+ await File(join(cwd.path, "linux", "spotube.desktop")).copy(
+ join(tempDir, "spotube.desktop"),
+ );
+ await File(
+ join(cwd.path, "linux", "com.github.KRTirtho.Spotube.appdata.xml"),
+ ).copy(
+ join(tempDir, "com.github.KRTirtho.Spotube.appdata.xml"),
+ );
+ await File(join(cwd.path, "assets", "spotube-logo.png")).copy(
+ join(tempDir, "spotube-logo.png"),
+ );
+
+ await shell.run(
+ "tar -cJf ${tarFile.path} -C $tempDir .",
+ );
+
+ final ogDeb = File(
+ join(
+ cwd.path,
+ "dist",
+ pubspec.version.toString(),
+ "spotube-${pubspec.version}-linux.deb",
+ ),
+ );
+
+ final ogRpm = File(
+ join(
+ cwd.path,
+ "dist",
+ pubspec.version.toString(),
+ "spotube-${pubspec.version}-linux.rpm",
+ ),
+ );
+
+ await ogDeb.copy(
+ join(cwd.path, "dist", "Spotube-linux-x86_64.deb"),
+ );
+ await ogRpm.copy(
+ join(cwd.path, "dist", "Spotube-linux-x86_64.rpm"),
+ );
+
+ await ogDeb.delete();
+ await ogRpm.delete();
+
+ stdout.writeln("✅ Linux building done");
+ }
+}
diff --git a/cli/commands/build/linux_arm.dart b/cli/commands/build/linux_arm.dart
new file mode 100644
index 000000000..a09f09808
--- /dev/null
+++ b/cli/commands/build/linux_arm.dart
@@ -0,0 +1,37 @@
+import 'dart:async';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+
+import '../../core/env.dart';
+import 'common.dart';
+
+class LinuxArmBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "Build Linux Arm";
+
+ @override
+ String get name => "linux_arm";
+
+ @override
+ FutureOr? run() async {
+ await bootstrap();
+
+ await shell.run(
+ "docker buildx build --platform=linux/arm64 "
+ "-f ${join(cwd.path, ".github", "Dockerfile")} ${cwd.path} "
+ "--build-arg FLUTTER_VERSION=${CliEnv.flutterVersion} "
+ "--build-arg BUILD_VERSION=${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber} "
+ "-t krtirtho/spotube_linux_arm:latest "
+ "--load",
+ );
+
+ await shell.run(
+ """
+ docker images ls
+ docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest
+ docker cp spotube_linux_arm:/app/dist/ dist/
+ """,
+ );
+ }
+}
diff --git a/cli/commands/build/macos.dart b/cli/commands/build/macos.dart
new file mode 100644
index 000000000..e8f34b775
--- /dev/null
+++ b/cli/commands/build/macos.dart
@@ -0,0 +1,42 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+
+import 'common.dart';
+
+class MacosBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "Macos Build command";
+
+ @override
+ String get name => "macos";
+
+ @override
+ FutureOr? run() async {
+ await bootstrap();
+
+ await shell.run(
+ """
+ flutter build macos
+ appdmg appdmg.json ${join(cwd.path, "build", "Spotube-macos-universal.dmg")}
+ flutter_distributor package --platform=macos --targets pkg --skip-clean
+ """,
+ );
+
+ final ogPkg = File(
+ join(
+ cwd.path,
+ "dist",
+ pubspec.version.toString(),
+ "spotube-${pubspec.version}-macos.pkg",
+ ),
+ );
+
+ await ogPkg.copy(
+ join(cwd.path, "build", "Spotube-macos-universal.pkg"),
+ );
+ await ogPkg.delete();
+ }
+}
diff --git a/cli/commands/build/windows.dart b/cli/commands/build/windows.dart
new file mode 100644
index 000000000..c44ed52f5
--- /dev/null
+++ b/cli/commands/build/windows.dart
@@ -0,0 +1,119 @@
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+import 'package:crypto/crypto.dart';
+import 'common.dart';
+
+class WindowsBuildCommand extends Command with BuildCommandCommonSteps {
+ @override
+ String get description => "Build Windows exe";
+
+ @override
+ String get name => "windows";
+
+ Future innoDependInstall() async {
+ final innoDependencyPath = join(cwd.path, "build", "inno-depend");
+
+ await shell.run(
+ "git clone https://github.com/DomGries/InnoDependencyInstaller.git $innoDependencyPath",
+ );
+ }
+
+ @override
+ void run() async {
+ stdout.writeln("Replace versions");
+
+ final chocoFiles = [
+ join(cwd.path, "choco-struct", "tools", "VERIFICATION.txt"),
+ join(cwd.path, "choco-struct", "spotube.nuspec"),
+ ];
+
+ for (final filePath in chocoFiles) {
+ final file = File(filePath);
+ final content = file.readAsStringSync();
+ final newContent =
+ content.replaceAll(versionVarRegExp, versionWithoutBuildNumber);
+
+ file.writeAsStringSync(newContent);
+ }
+
+ await bootstrap();
+ await innoDependInstall();
+
+ final runnerRCFile = File(
+ join(cwd.path, "windows", "runner", "Runner.rc"),
+ );
+
+ runnerRCFile.writeAsStringSync(
+ runnerRCFile
+ .readAsStringSync()
+ .replaceAll("%{{SPOTUBE_VERSION}}%", versionWithoutBuildNumber)
+ .replaceAll(
+ "%{{SPOTUBE_VERSION_AS_NUMBER}}%",
+ [
+ pubspec.version!.major,
+ pubspec.version!.minor,
+ pubspec.version!.patch,
+ 0
+ ].join(","),
+ ),
+ );
+
+ await shell.run(
+ "flutter_distributor package --platform=windows --targets=exe --skip-clean",
+ );
+
+ final ogExe = File(
+ join(
+ cwd.path,
+ "dist",
+ pubspec.version.toString(),
+ "spotube-${pubspec.version}-windows-setup.exe",
+ ),
+ );
+
+ final exePath = join(cwd.path, "dist", "Spotube-windows-x86_64-setup.exe");
+
+ await ogExe.copy(exePath);
+ await ogExe.delete();
+
+ stdout.writeln("✅ Windows exe built at $exePath");
+
+ final exeFile = File(exePath);
+
+ final hash = sha256.convert(await exeFile.readAsBytes()).toString();
+
+ final chocoVerificationFile = File(chocoFiles.first);
+
+ chocoVerificationFile.writeAsStringSync(
+ chocoVerificationFile.readAsStringSync().replaceAll(
+ RegExp(r"\%\{\{WIN_SHA256\}\}\%"),
+ hash,
+ ),
+ );
+
+ await exeFile.copy(
+ join(cwd.path, "choco-struct", "tools", basename(exeFile.path)),
+ );
+
+ await shell.run(
+ "choco pack ${chocoFiles[1]} --outputdirectory ${join(cwd.path, "dist")}",
+ );
+
+ final chocoNupkg = File(
+ join(cwd.path, "dist", "spotube.$versionWithoutBuildNumber.nupkg"),
+ );
+
+ final distNupkgPath = join(
+ cwd.path,
+ "dist",
+ "Spotube-windows-x86_64.nupkg",
+ );
+
+ await chocoNupkg.copy(distNupkgPath);
+ await chocoNupkg.delete();
+
+ stdout.writeln("✅ Windows nupkg built at $distNupkgPath");
+ }
+}
diff --git a/cli/commands/credits.dart b/cli/commands/credits.dart
new file mode 100644
index 000000000..6bad7a444
--- /dev/null
+++ b/cli/commands/credits.dart
@@ -0,0 +1,121 @@
+import 'dart:io';
+
+import 'package:args/command_runner.dart';
+import 'package:collection/collection.dart';
+import 'package:dio/dio.dart';
+import 'package:html/parser.dart';
+import 'package:path/path.dart';
+import 'package:pub_api_client/pub_api_client.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+
+class CreditsCommand extends Command {
+ final dio = Dio(
+ BaseOptions(
+ responseType: ResponseType.plain,
+ ),
+ );
+
+ @override
+ String get description => "Generate credits for used Library's authors";
+
+ @override
+ String get name => "credits";
+
+ @override
+ run() async {
+ final client = PubClient();
+ final cwd = Directory.current;
+
+ final pubspec = Pubspec.parse(
+ File(join(cwd.path, 'pubspec.yaml')).readAsStringSync(),
+ );
+
+ final allDeps = [
+ ...pubspec.dependencies.entries,
+ ...pubspec.devDependencies.entries,
+ ];
+
+ final dependencies = allDeps
+ .where((d) => d.value is HostedDependency)
+ .map((d) => d.key)
+ .toSet();
+ final packageInfo = await Future.wait(dependencies.map(client.packageInfo));
+
+ final gitDepsList = List.castFrom,
+ MapEntry>(
+ allDeps
+ .where((d) => d.value is GitDependency)
+ .map((d) => MapEntry(d.key, d.value as GitDependency))
+ .toList(),
+ );
+
+ final gitDeps = gitDepsList.map(
+ (d) {
+ final uri = Uri.parse(
+ d.value.url.toString().replaceAll('.git', ''),
+ );
+ return MapEntry(
+ d.key,
+ uri.replace(
+ pathSegments: [
+ ...uri.pathSegments,
+ 'raw',
+ d.value.ref ?? 'main',
+ d.value.path ?? '',
+ 'pubspec.yaml',
+ ],
+ ).toString(),
+ );
+ },
+ ).toList();
+
+ final gitPubspecs = await Future.wait(
+ gitDeps.map(
+ (d) {
+ Pubspec parser(Response res) {
+ try {
+ return Pubspec.parse(res.data);
+ } catch (e) {
+ final document = parse(res.data);
+ final pre = document.querySelector('pre');
+ if (pre == null) {
+ stdout.writeln(d.toString());
+ rethrow;
+ }
+ return Pubspec.parse(pre.text);
+ }
+ }
+
+ return dio.get(d.value).then(parser).catchError(
+ (_) => dio
+ .get(d.value.replaceFirst('/main', '/master'))
+ .then(parser),
+ );
+ },
+ ),
+ );
+
+ stdout.writeln(
+ packageInfo
+ .map(
+ (package) =>
+ '1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}',
+ )
+ .join('\n'),
+ );
+
+ stdout.writeln(
+ gitPubspecs.map(
+ (package) {
+ final packageUrl = package.homepage ??
+ gitDepsList
+ .firstWhereOrNull((dep) => dep.key == package.name)
+ ?.value
+ .url
+ .toString();
+ return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}';
+ },
+ ).join('\n'),
+ );
+ }
+}
diff --git a/cli/commands/install-dependencies.dart b/cli/commands/install-dependencies.dart
new file mode 100644
index 000000000..dc519cc61
--- /dev/null
+++ b/cli/commands/install-dependencies.dart
@@ -0,0 +1,79 @@
+import 'dart:async';
+
+import 'package:args/command_runner.dart';
+import 'package:process_run/shell_run.dart';
+
+class InstallDependenciesCommand extends Command {
+ @override
+ String get description => "Install platform dependencies";
+
+ @override
+ String get name => "install-dependencies";
+
+ InstallDependenciesCommand() {
+ argParser.addOption(
+ "platform",
+ abbr: "p",
+ allowed: [
+ "windows",
+ "linux",
+ "linux_arm",
+ "macos",
+ "ios",
+ "android",
+ ],
+ mandatory: true,
+ );
+ }
+
+ @override
+ FutureOr? run() async {
+ final shell = Shell();
+
+ switch (argResults!.option("platform")) {
+ case "windows":
+ break;
+ case "linux":
+ await shell.run(
+ """
+ sudo apt-get update -y
+ sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev
+ """,
+ );
+ break;
+ case "linux_arm":
+ await shell.run(
+ """
+ sudo apt-get update -y
+ sudo apt-get install -y pkg-config make python3-pip python3-setuptools
+ """,
+ );
+ break;
+ case "macos":
+ await shell.run(
+ """
+ brew install python-setuptools
+ npm install -g appdmg
+ """,
+ );
+ break;
+ case "ios":
+ await shell.run(
+ """
+ rustup target add aarch64-apple-ios
+ """,
+ );
+ break;
+ case "android":
+ await shell.run(
+ """
+ sudo apt-get update -y
+ sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
+ """,
+ );
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/cli/commands/translated.dart b/cli/commands/translated.dart
new file mode 100644
index 000000000..43c4ea49d
--- /dev/null
+++ b/cli/commands/translated.dart
@@ -0,0 +1,39 @@
+import 'dart:async';
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:args/command_runner.dart';
+import 'package:path/path.dart';
+
+class TranslatedCommand extends Command {
+ @override
+ String get description =>
+ "Update translation based on generated translated messages";
+
+ @override
+ String get name => "translated";
+
+ @override
+ FutureOr? run() async {
+ final cwd = Directory.current;
+ final translatedFile = jsonDecode(
+ await File(join(cwd.path, 'tm.json')).readAsString(),
+ ) as Map;
+
+ for (final MapEntry(:key, :value) in translatedFile.entries) {
+ stdout.writeln('Updating locale: $key');
+ final file = File(join(cwd.path, 'lib', 'l10n', 'app_$key.arb'));
+
+ final fileContent =
+ jsonDecode(await file.readAsString()) as Map;
+
+ final newContent = {...fileContent, ...value};
+
+ await file.writeAsString(
+ const JsonEncoder.withIndent(' ').convert(newContent),
+ );
+
+ stdout.writeln('✅ Updated locale: $key');
+ }
+ }
+}
diff --git a/cli/commands/untranslated.dart b/cli/commands/untranslated.dart
new file mode 100644
index 000000000..dadcd8b5b
--- /dev/null
+++ b/cli/commands/untranslated.dart
@@ -0,0 +1,48 @@
+import 'package:args/command_runner.dart';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart';
+
+class UntranslatedCommand extends Command {
+ @override
+ get name => "untranslated";
+ @override
+ get description =>
+ "Generate Untranslated Messages for ChatGPT based Translation";
+
+ @override
+ run() async {
+ final cwd = Directory.current;
+ final file = jsonDecode(
+ File(join(cwd.path, 'untranslated_messages.json')).readAsStringSync(),
+ ) as Map;
+
+ final englishMessages = jsonDecode(
+ File(join(cwd.path, 'lib', 'l10n', 'app_en.arb')).readAsStringSync(),
+ ) as Map;
+
+ final messagesWithValues = {};
+
+ for (final MapEntry(key: locale, value: messages) in file.entries) {
+ messagesWithValues[locale] = Map.fromEntries(
+ messages
+ .map(
+ (message) =>
+ MapEntry(message, englishMessages[message]),
+ )
+ .toList()
+ .cast>(),
+ );
+ }
+
+ stdout.writeln(
+ "Prompt:\n"
+ "Translate following to their appropriate locale for flutter arb translations files."
+ " Put the respective new translations in a map of their corresponding locale.",
+ );
+ stdout.writeln(
+ const JsonEncoder.withIndent(' ').convert(messagesWithValues),
+ );
+ }
+}
diff --git a/cli/core/env.dart b/cli/core/env.dart
new file mode 100644
index 000000000..33cc5df13
--- /dev/null
+++ b/cli/core/env.dart
@@ -0,0 +1,24 @@
+import 'dart:io';
+
+enum BuildChannel {
+ stable,
+ nightly;
+
+ factory BuildChannel.fromEnvironment(String name) {
+ final channel = Platform.environment[name]!;
+ if (channel == "stable") {
+ return BuildChannel.stable;
+ } else if (channel == "nightly") {
+ return BuildChannel.nightly;
+ } else {
+ throw Exception("Invalid channel: $channel");
+ }
+ }
+}
+
+class CliEnv {
+ static final channel = BuildChannel.fromEnvironment("CHANNEL");
+ static final dotenv = Platform.environment["DOTENV"]!;
+ static final ghRunNumber = Platform.environment["GITHUB_RUN_NUMBER"];
+ static final flutterVersion = Platform.environment["FLUTTER_VERSION"]!;
+}
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 000000000..7e7e7f67d
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 1d048cc9b..7e5f24b53 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -49,6 +49,8 @@ PODS:
- Flutter (1.0.0)
- flutter_broadcasts (0.0.1):
- Flutter
+ - flutter_discord_rpc (0.0.1):
+ - Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -58,20 +60,12 @@ PODS:
- OrderedSet (~> 5.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- - flutter_mailer (0.0.1):
- - Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_sharing_intent (0.0.1):
- Flutter
- - fluttertoast (0.0.2):
- - Flutter
- - Toast
- - FMDB (2.7.5):
- - FMDB/standard (= 2.7.5)
- - FMDB/standard (2.7.5)
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
@@ -80,14 +74,15 @@ PODS:
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- - metadata_god (0.0.1)
+ - metadata_god (0.0.1):
+ - Flutter
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- - permission_handler_apple (9.1.1):
+ - permission_handler_apple (9.3.0):
- Flutter
- SDWebImage (5.18.8):
- SDWebImage/Core (= 5.18.8)
@@ -97,9 +92,23 @@ PODS:
- FlutterMacOS
- sqflite (0.0.3):
- Flutter
- - FMDB (>= 2.7.5)
+ - FlutterMacOS
+ - "sqlite3 (3.46.0+1)":
+ - "sqlite3/common (= 3.46.0+1)"
+ - "sqlite3/common (3.46.0+1)"
+ - "sqlite3/fts5 (3.46.0+1)":
+ - sqlite3/common
+ - "sqlite3/perf-threadsafe (3.46.0+1)":
+ - sqlite3/common
+ - "sqlite3/rtree (3.46.0+1)":
+ - sqlite3/common
+ - sqlite3_flutter_libs (0.0.1):
+ - Flutter
+ - sqlite3 (~> 3.46.0)
+ - sqlite3/fts5
+ - sqlite3/perf-threadsafe
+ - sqlite3/rtree
- SwiftyGif (5.4.4)
- - Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
@@ -113,13 +122,12 @@ DEPENDENCIES:
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
+ - flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
- - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`)
@@ -129,18 +137,18 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- - sqflite (from `.symlinks/plugins/sqflite/ios`)
+ - sqflite (from `.symlinks/plugins/sqflite/darwin`)
+ - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- - FMDB
- OrderedSet
- SDWebImage
+ - sqlite3
- SwiftyGif
- - Toast
EXTERNAL SOURCES:
app_links:
@@ -161,20 +169,18 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_broadcasts:
:path: ".symlinks/plugins/flutter_broadcasts/ios"
+ flutter_discord_rpc:
+ :path: ".symlinks/plugins/flutter_discord_rpc/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
- flutter_mailer:
- :path: ".symlinks/plugins/flutter_mailer/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_sharing_intent:
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
- fluttertoast:
- :path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
@@ -194,45 +200,46 @@ EXTERNAL SOURCES:
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
- :path: ".symlinks/plugins/sqflite/ios"
+ :path: ".symlinks/plugins/sqflite/darwin"
+ sqlite3_flutter_libs:
+ :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
- app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
+ app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
- audio_session: 4f3e461722055d21515cf3261b64c973c062f345
+ audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
- device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
+ device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
- file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
- file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
+ file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
+ file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
+ flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
- flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
- flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
+ flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
- fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
- FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
- image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
- integration_test: 13825b8a9334a850581300559b8839134b124670
+ image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
+ integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
- metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7
+ metadata_god: 4bbd8523cdb5d42c5e59d2fabad01ff8f4bc53f9
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
- package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
- path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
- permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
+ package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
+ path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
- shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
- sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
+ shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
+ sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
+ sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
+ sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
- Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
- url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
+ url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 13f624a46..34793f682 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -324,6 +324,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */,
+ 46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -346,6 +347,7 @@
B536BD992B405DB1009B3CE4 /* Embed Frameworks */,
B536BD9A2B405DB1009B3CE4 /* Thin Binary */,
A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */,
+ 2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -368,6 +370,7 @@
B536BDB62B405FDE009B3CE4 /* Embed Frameworks */,
B536BDB72B405FDE009B3CE4 /* Thin Binary */,
244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */,
+ 4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -390,6 +393,7 @@
B536BDD82B4060B3009B3CE4 /* Embed Frameworks */,
B536BDD92B4060B3009B3CE4 /* Thin Binary */,
D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */,
+ 5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -523,6 +527,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
+ 2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -539,6 +560,57 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
diff --git a/lib/collections/cache_keys.dart b/lib/collections/cache_keys.dart
deleted file mode 100644
index bca133229..000000000
--- a/lib/collections/cache_keys.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-abstract class LocalStorageKeys {
- static String saveTrackLyrics = 'save_track_lyrics';
- static String recommendationMarket = 'recommendation_market';
- static String ytSearchFormate = 'youtube_search_format';
-
- static String clientId = 'clientId';
- static String clientSecret = 'clientSecret';
- static String accessToken = 'accessToken';
- static String refreshToken = 'refreshToken';
- static String expiration = "expiration";
- static String geniusAccessToken = "genius_access_token";
-
- static String themeMode = "theme_mode";
- static String nextTrackHotKey = "next_track_hot_key";
- static String prevTrackHotKey = "prev_track_hot_key";
- static String playPauseHotKey = "play_pause_hot_key";
-
- static String volume = "volume";
-
- static String windowSizeInfo = "window_size_info";
-}
diff --git a/lib/collections/env.dart b/lib/collections/env.dart
index 50fe1e6a7..df45cee91 100644
--- a/lib/collections/env.dart
+++ b/lib/collections/env.dart
@@ -1,8 +1,13 @@
import 'package:envied/envied.dart';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
+import 'package:spotube/utils/platform.dart';
part 'env.g.dart';
+enum ReleaseChannel {
+ nightly,
+ stable,
+}
+
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
abstract class Env {
@EnviedField(varName: 'SPOTIFY_SECRETS')
@@ -25,8 +30,15 @@ abstract class Env {
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
+ @EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
+ static final String _releaseChannel = _Env._releaseChannel;
+
+ static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
+ ? ReleaseChannel.stable
+ : ReleaseChannel.nightly;
+
static bool get enableUpdateChecker =>
- DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1";
+ kIsFlatpak || _enableUpdateChecker == "1";
static String discordAppId = "1176718791388975124";
-}
+}
\ No newline at end of file
diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart
index 4df19dfc9..31f97e0c9 100644
--- a/lib/collections/fake.dart
+++ b/lib/collections/fake.dart
@@ -1,7 +1,8 @@
import 'package:spotify/spotify.dart';
-import 'package:spotube/extensions/track.dart';
+import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/spotify/home_feed.dart';
import 'package:spotube/models/spotify_friends.dart';
+import 'package:spotube/provider/history/summary.dart';
abstract class FakeData {
static final Image image = Image()
@@ -223,4 +224,36 @@ abstract class FakeData {
)
],
);
+
+ static const historySummary = PlaybackHistorySummary(
+ albums: 1,
+ artists: 1,
+ duration: Duration(seconds: 1),
+ playlists: 1,
+ tracks: 1,
+ fees: 1,
+ );
+
+ static final historyRecentlyPlayedPlaylist = HistoryTableData(
+ id: 0,
+ type: HistoryEntryType.track,
+ createdAt: DateTime.now(),
+ itemId: "1",
+ data: playlist.toJson(),
+ );
+
+ static final historyRecentlyPlayedAlbum = HistoryTableData(
+ id: 0,
+ type: HistoryEntryType.track,
+ createdAt: DateTime.now(),
+ itemId: "1",
+ data: album.toJson(),
+ );
+
+ static final historyRecentlyPlayedItems = List.generate(
+ 10,
+ (index) => index % 2 == 0
+ ? historyRecentlyPlayedPlaylist
+ : historyRecentlyPlayedAlbum,
+ );
}
diff --git a/lib/collections/formatters.dart b/lib/collections/formatters.dart
new file mode 100644
index 000000000..0aed9e9f8
--- /dev/null
+++ b/lib/collections/formatters.dart
@@ -0,0 +1,8 @@
+import 'package:intl/intl.dart';
+
+final compactNumberFormatter = NumberFormat.compact();
+final usdFormatter = NumberFormat.compactCurrency(
+ locale: 'en-US',
+ symbol: r"$",
+ decimalDigits: 2,
+);
diff --git a/lib/collections/initializers.dart b/lib/collections/initializers.dart
index 9627de1c3..976661fc2 100644
--- a/lib/collections/initializers.dart
+++ b/lib/collections/initializers.dart
@@ -1,9 +1,10 @@
import 'dart:io';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
+
+import 'package:spotube/utils/platform.dart';
import 'package:win32_registry/win32_registry.dart';
Future registerWindowsScheme(String scheme) async {
- if (!DesktopTools.platform.isWindows) return;
+ if (!kIsWindows) return;
String appPath = Platform.resolvedExecutable;
String protocolRegKey = 'Software\\Classes\\$scheme';
diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart
index 5f60959ed..4f446831a 100644
--- a/lib/collections/intents.dart
+++ b/lib/collections/intents.dart
@@ -5,9 +5,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/collections/routes.dart';
-import 'package:spotube/components/player/player_controls.dart';
-import 'package:spotube/models/logger.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/modules/player/player_controls.dart';
+import 'package:spotube/pages/home/home.dart';
+import 'package:spotube/pages/library/library.dart';
+import 'package:spotube/pages/lyrics/lyrics.dart';
+import 'package:spotube/pages/search/search.dart';
+import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart';
@@ -17,8 +20,6 @@ class PlayPauseIntent extends Intent {
}
class PlayPauseAction extends Action {
- final logger = getLogger(PlayPauseAction);
-
@override
invoke(intent) async {
if (PlayerControls.focusNode.canRequestFocus) {
@@ -67,16 +68,16 @@ class HomeTabAction extends Action {
final router = intent.ref.read(routerProvider);
switch (intent.tab) {
case HomeTabs.browse:
- router.go("/");
+ router.goNamed(HomePage.name);
break;
case HomeTabs.search:
- router.go("/search");
+ router.goNamed(SearchPage.name);
break;
case HomeTabs.library:
- router.go("/library");
+ router.goNamed(LibraryPage.name);
break;
case HomeTabs.lyrics:
- router.go("/lyrics");
+ router.goNamed(LyricsPage.name);
break;
}
return null;
@@ -92,8 +93,8 @@ class SeekIntent extends Intent {
class SeekAction extends Action {
@override
invoke(intent) async {
- final playlist = intent.ref.read(proxyPlaylistProvider);
- if (playlist.isFetching) {
+ final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider);
+ if (isFetchingActiveTrack) {
DirectionalFocusAction().invoke(
DirectionalFocusIntent(
intent.forward ? TraversalDirection.right : TraversalDirection.left,
@@ -101,7 +102,7 @@ class SeekAction extends Action {
);
return null;
}
- final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
+ final position = audioPlayer.position.inSeconds;
await audioPlayer.seek(
Duration(
seconds: intent.forward ? position + 5 : position - 5,
diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart
index 45456d697..44da6ee61 100644
--- a/lib/collections/language_codes.dart
+++ b/lib/collections/language_codes.dart
@@ -81,10 +81,10 @@ abstract class LanguageLocals {
// name: "Bashkir",
// nativeName: "башҡорт теле",
// ),
- // "eu": const ISOLanguageName(
- // name: "Basque",
- // nativeName: "euskara,",
- // ),
+ "eu": const ISOLanguageName(
+ name: "Basque",
+ nativeName: "Euskara",
+ ),
// "be": const ISOLanguageName(
// name: "Belarusian",
// nativeName: "Беларуская",
@@ -197,10 +197,10 @@ abstract class LanguageLocals {
// name: "Fijian",
// nativeName: "vosa Vakaviti",
// ),
- // "fi": const ISOLanguageName(
- // name: "Finnish",
- // nativeName: "suomi",
- // ),
+ "fi": const ISOLanguageName(
+ name: "Finnish",
+ nativeName: "suomi",
+ ),
"fr": const ISOLanguageName(
name: "French",
nativeName: "français",
@@ -213,10 +213,10 @@ abstract class LanguageLocals {
// name: "Galician",
// nativeName: "Galego",
// ),
- // "ka": const ISOLanguageName(
- // name: "Georgian",
- // nativeName: "ქართული",
- // ),
+ "ka": const ISOLanguageName(
+ name: "Georgian",
+ nativeName: "ქართული",
+ ),
"de": const ISOLanguageName(
name: "German",
nativeName: "Deutsch",
@@ -265,10 +265,10 @@ abstract class LanguageLocals {
// name: "Interlingua",
// nativeName: "Interlingua",
// ),
- // "id": const ISOLanguageName(
- // name: "Indonesian",
- // nativeName: "Bahasa Indonesia",
- // ),
+ "id": const ISOLanguageName(
+ name: "Indonesian",
+ nativeName: "Bahasa Indonesia",
+ ),
// "ie": const ISOLanguageName(
// name: "Interlingue",
// nativeName: "Occidental",
@@ -354,8 +354,8 @@ abstract class LanguageLocals {
// nativeName: "KiKongo",
// ),
"ko": const ISOLanguageName(
- name: "Korean",
- nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
+ name: "Korean",
+ nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
),
// "ku": const ISOLanguageName(
// name: "Kurdish",
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index 080cbd8a2..3bf1d883d 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -1,4 +1,3 @@
-import 'package:catcher_2/catcher_2.dart';
import 'package:flutter/foundation.dart' hide Category;
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
@@ -14,6 +13,7 @@ import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/pages/home/genres/genres.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
+import 'package:spotube/pages/library/local_folder.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
@@ -24,21 +24,25 @@ import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
+import 'package:spotube/pages/stats/albums/albums.dart';
+import 'package:spotube/pages/stats/artists/artists.dart';
+import 'package:spotube/pages/stats/fees/fees.dart';
+import 'package:spotube/pages/stats/minutes/minutes.dart';
+import 'package:spotube/pages/stats/playlists/playlists.dart';
+import 'package:spotube/pages/stats/stats.dart';
+import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart';
-import 'package:spotube/provider/authentication_provider.dart';
+import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
-import 'package:spotube/utils/platform.dart';
-import 'package:spotube/components/shared/spotube_page_route.dart';
+import 'package:spotube/components/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
-import 'package:spotube/pages/desktop_login/login_tutorial.dart';
-import 'package:spotube/pages/desktop_login/desktop_login.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
-final rootNavigatorKey = Catcher2.navigatorKey;
+final rootNavigatorKey = GlobalKey();
final shellRouteNavigatorKey = GlobalKey();
final routerProvider = Provider((ref) {
return GoRouter(
@@ -50,12 +54,11 @@ final routerProvider = Provider((ref) {
routes: [
GoRoute(
path: "/",
+ name: HomePage.name,
redirect: (context, state) async {
- final authNotifier = ref.read(authenticationProvider.notifier);
- final json = await authNotifier.box.get(authNotifier.cacheKey);
+ final auth = await ref.read(authenticationProvider.future);
- if (json?["cookie"] == null &&
- !KVStoreService.doneGettingStarted) {
+ if (auth == null && !KVStoreService.doneGettingStarted) {
return "/getting-started";
}
@@ -66,11 +69,13 @@ final routerProvider = Provider((ref) {
routes: [
GoRoute(
path: "genres",
+ name: GenrePage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: GenrePage()),
),
GoRoute(
path: "genre/:categoryId",
+ name: GenrePlaylistsPage.name,
pageBuilder: (context, state) => SpotubePage(
child: GenrePlaylistsPage(
category: state.extra as Category,
@@ -79,6 +84,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "feeds/:feedId",
+ name: HomeFeedSectionPage.name,
pageBuilder: (context, state) => SpotubePage(
child: HomeFeedSectionPage(
sectionUri: state.pathParameters["feedId"] as String,
@@ -89,45 +95,62 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/search",
- name: "Search",
+ name: SearchPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: SearchPage()),
),
GoRoute(
path: "/library",
- name: "Library",
+ name: LibraryPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: LibraryPage()),
routes: [
GoRoute(
- path: "generate",
- pageBuilder: (context, state) =>
- const SpotubePage(child: PlaylistGeneratorPage()),
- routes: [
- GoRoute(
- path: "result",
- pageBuilder: (context, state) => SpotubePage(
- child: PlaylistGenerateResultPage(
- state: state.extra as GeneratePlaylistProviderInput,
- ),
+ path: "generate",
+ name: PlaylistGeneratorPage.name,
+ pageBuilder: (context, state) =>
+ const SpotubePage(child: PlaylistGeneratorPage()),
+ routes: [
+ GoRoute(
+ path: "result",
+ name: PlaylistGenerateResultPage.name,
+ pageBuilder: (context, state) => SpotubePage(
+ child: PlaylistGenerateResultPage(
+ state: state.extra as GeneratePlaylistProviderInput,
),
),
- ]),
+ )
+ ],
+ ),
+ GoRoute(
+ path: "local",
+ name: LocalLibraryPage.name,
+ pageBuilder: (context, state) {
+ assert(state.extra is String);
+ return SpotubePage(
+ child: LocalLibraryPage(state.extra as String,
+ isDownloads:
+ state.uri.queryParameters["downloads"] != null),
+ );
+ },
+ ),
]),
GoRoute(
path: "/lyrics",
- name: "Lyrics",
+ name: LyricsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: LyricsPage()),
),
GoRoute(
path: "/settings",
+ name: SettingsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: SettingsPage(),
),
routes: [
GoRoute(
path: "blacklist",
+ name: BlackListPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const BlackListPage(),
),
@@ -135,12 +158,14 @@ final routerProvider = Provider((ref) {
if (!kIsWeb)
GoRoute(
path: "logs",
+ name: LogsPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const LogsPage(),
),
),
GoRoute(
path: "about",
+ name: AboutSpotube.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const AboutSpotube(),
),
@@ -149,6 +174,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/album/:id",
+ name: AlbumPage.name,
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
return SpotubePage(
@@ -158,6 +184,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/artist/:id",
+ name: ArtistPage.name,
pageBuilder: (context, state) {
assert(state.pathParameters["id"] != null);
return SpotubePage(
@@ -166,6 +193,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/playlist/:id",
+ name: PlaylistPage.name,
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
@@ -177,6 +205,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/track/:id",
+ name: TrackPage.name,
pageBuilder: (context, state) {
final id = state.pathParameters["id"]!;
return SpotubePage(
@@ -186,12 +215,14 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/connect",
+ name: ConnectPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: ConnectPage(),
),
routes: [
GoRoute(
path: "control",
+ name: ConnectControlPage.name,
pageBuilder: (context, state) {
return const SpotubePage(
child: ConnectControlPage(),
@@ -202,13 +233,66 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/profile",
+ name: ProfilePage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: ProfilePage()),
+ ),
+ GoRoute(
+ path: "/stats",
+ name: StatsPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsPage(),
+ ),
+ routes: [
+ GoRoute(
+ path: "minutes",
+ name: StatsMinutesPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsMinutesPage(),
+ ),
+ ),
+ GoRoute(
+ path: "streams",
+ name: StatsStreamsPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsStreamsPage(),
+ ),
+ ),
+ GoRoute(
+ path: "fees",
+ name: StatsStreamFeesPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsStreamFeesPage(),
+ ),
+ ),
+ GoRoute(
+ path: "artists",
+ name: StatsArtistsPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsArtistsPage(),
+ ),
+ ),
+ GoRoute(
+ path: "albums",
+ name: StatsAlbumsPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsAlbumsPage(),
+ ),
+ ),
+ GoRoute(
+ path: "playlists",
+ name: StatsPlaylistsPage.name,
+ pageBuilder: (context, state) => const SpotubePage(
+ child: StatsPlaylistsPage(),
+ ),
+ ),
+ ],
)
],
),
GoRoute(
path: "/mini-player",
+ name: MiniLyricsPage.name,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => SpotubePage(
child: MiniLyricsPage(prevSize: state.extra as Size),
@@ -216,6 +300,7 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/getting-started",
+ name: GettingStarting.name,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: GettingStarting(),
@@ -223,20 +308,15 @@ final routerProvider = Provider((ref) {
),
GoRoute(
path: "/login",
- parentNavigatorKey: rootNavigatorKey,
- pageBuilder: (context, state) => SpotubePage(
- child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
- ),
- ),
- GoRoute(
- path: "/login-tutorial",
+ name: WebViewLogin.name,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
- child: LoginTutorial(),
+ child: WebViewLogin(),
),
),
GoRoute(
path: "/lastfm-login",
+ name: LastFMLoginPage.name,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) =>
const SpotubePage(child: LastFMLoginPage()),
diff --git a/lib/collections/side_bar_tiles.dart b/lib/collections/side_bar_tiles.dart
index 551d70d72..4f23c049a 100644
--- a/lib/collections/side_bar_tiles.dart
+++ b/lib/collections/side_bar_tiles.dart
@@ -1,33 +1,82 @@
import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:spotube/pages/home/home.dart';
+import 'package:spotube/pages/library/library.dart';
+import 'package:spotube/pages/lyrics/lyrics.dart';
+import 'package:spotube/pages/search/search.dart';
+import 'package:spotube/pages/stats/stats.dart';
class SideBarTiles {
final IconData icon;
final String title;
final String id;
- SideBarTiles({required this.icon, required this.title, required this.id});
+ final String name;
+
+ SideBarTiles({
+ required this.icon,
+ required this.title,
+ required this.id,
+ required this.name,
+ });
}
List getSidebarTileList(AppLocalizations l10n) => [
- SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
- SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
SideBarTiles(
- id: "library", icon: SpotubeIcons.library, title: l10n.library),
- SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics),
+ id: "browse",
+ name: HomePage.name,
+ icon: SpotubeIcons.home,
+ title: l10n.browse,
+ ),
+ SideBarTiles(
+ id: "search",
+ name: SearchPage.name,
+ icon: SpotubeIcons.search,
+ title: l10n.search,
+ ),
+ SideBarTiles(
+ id: "library",
+ name: LibraryPage.name,
+ icon: SpotubeIcons.library,
+ title: l10n.library,
+ ),
+ SideBarTiles(
+ id: "lyrics",
+ name: LyricsPage.name,
+ icon: SpotubeIcons.music,
+ title: l10n.lyrics,
+ ),
+ SideBarTiles(
+ id: "stats",
+ name: StatsPage.name,
+ icon: SpotubeIcons.chart,
+ title: l10n.stats,
+ ),
];
List getNavbarTileList(AppLocalizations l10n) => [
- SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
- SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
+ SideBarTiles(
+ id: "browse",
+ name: HomePage.name,
+ icon: SpotubeIcons.home,
+ title: l10n.browse,
+ ),
+ SideBarTiles(
+ id: "search",
+ name: SearchPage.name,
+ icon: SpotubeIcons.search,
+ title: l10n.search,
+ ),
SideBarTiles(
id: "library",
+ name: LibraryPage.name,
icon: SpotubeIcons.library,
title: l10n.library,
),
SideBarTiles(
- id: "settings",
- icon: SpotubeIcons.settings,
- title: l10n.settings,
- )
+ id: "stats",
+ name: StatsPage.name,
+ icon: SpotubeIcons.chart,
+ title: l10n.stats,
+ ),
];
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index 6de212840..a45e581ed 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -121,4 +121,7 @@ abstract class SpotubeIcons {
static const monitor = FeatherIcons.monitor;
static const power = FeatherIcons.power;
static const bluetooth = FeatherIcons.bluetooth;
+ static const chart = FeatherIcons.barChart2;
+ static const folderAdd = FeatherIcons.folderPlus;
+ static const folderRemove = FeatherIcons.folderMinus;
}
diff --git a/lib/components/shared/adaptive/adaptive_list_tile.dart b/lib/components/adaptive/adaptive_list_tile.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_list_tile.dart
rename to lib/components/adaptive/adaptive_list_tile.dart
diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart
similarity index 97%
rename from lib/components/shared/adaptive/adaptive_pop_sheet_list.dart
rename to lib/components/adaptive/adaptive_pop_sheet_list.dart
index 21f56a220..97dc61321 100644
--- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart
+++ b/lib/components/adaptive/adaptive_pop_sheet_list.dart
@@ -187,7 +187,7 @@ class AdaptivePopSheetList extends StatelessWidget {
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
tooltip: tooltip,
style: theme.iconButtonTheme.style?.copyWith(
- shape: MaterialStatePropertyAll(
+ shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: borderRadius,
),
@@ -226,7 +226,10 @@ class _AdaptivePopSheetListItem extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
- child: IgnorePointer(child: item),
+ child: IconTheme.merge(
+ data: const IconThemeData(opacity: 1),
+ child: IgnorePointer(child: item),
+ ),
),
);
}
diff --git a/lib/components/shared/adaptive/adaptive_popup_menu_button.dart b/lib/components/adaptive/adaptive_popup_menu_button.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_popup_menu_button.dart
rename to lib/components/adaptive/adaptive_popup_menu_button.dart
diff --git a/lib/components/shared/adaptive/adaptive_select_tile.dart b/lib/components/adaptive/adaptive_select_tile.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_select_tile.dart
rename to lib/components/adaptive/adaptive_select_tile.dart
diff --git a/lib/components/shared/animated_gradient.dart b/lib/components/animated_gradient.dart
similarity index 100%
rename from lib/components/shared/animated_gradient.dart
rename to lib/components/animated_gradient.dart
diff --git a/lib/components/shared/bordered_text.dart b/lib/components/bordered_text.dart
similarity index 100%
rename from lib/components/shared/bordered_text.dart
rename to lib/components/bordered_text.dart
diff --git a/lib/components/shared/compact_search.dart b/lib/components/compact_search.dart
similarity index 100%
rename from lib/components/shared/compact_search.dart
rename to lib/components/compact_search.dart
diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart
deleted file mode 100644
index 2949fbae7..000000000
--- a/lib/components/desktop_login/login_form.dart
+++ /dev/null
@@ -1,73 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/extensions/context.dart';
-
-import 'package:spotube/provider/authentication_provider.dart';
-
-class TokenLoginForm extends HookConsumerWidget {
- final void Function()? onDone;
- const TokenLoginForm({
- super.key,
- this.onDone,
- });
-
- @override
- Widget build(BuildContext context, ref) {
- final authenticationNotifier = ref.watch(authenticationProvider.notifier);
- final directCodeController = useTextEditingController();
- final mounted = useIsMounted();
-
- final isLoading = useState(false);
-
- return ConstrainedBox(
- constraints: const BoxConstraints(
- maxWidth: 400,
- ),
- child: Column(
- children: [
- TextField(
- controller: directCodeController,
- decoration: InputDecoration(
- hintText: context.l10n.spotify_cookie("\"sp_dc\""),
- labelText: context.l10n.cookie_name_cookie("sp_dc"),
- ),
- keyboardType: TextInputType.visiblePassword,
- ),
- const SizedBox(height: 10),
- FilledButton(
- onPressed: isLoading.value
- ? null
- : () async {
- try {
- isLoading.value = true;
- if (directCodeController.text.isEmpty) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(context.l10n.fill_in_all_fields),
- behavior: SnackBarBehavior.floating,
- ),
- );
- return;
- }
- final cookieHeader =
- "sp_dc=${directCodeController.text.trim()}";
-
- authenticationNotifier.setCredentials(
- await AuthenticationCredentials.fromCookie(
- cookieHeader),
- );
- if (mounted()) {
- onDone?.call();
- }
- } finally {
- isLoading.value = false;
- }
- },
- child: Text(context.l10n.submit),
- )
- ],
- ),
- );
- }
-}
diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/dialogs/confirm_download_dialog.dart
similarity index 97%
rename from lib/components/shared/dialogs/confirm_download_dialog.dart
rename to lib/components/dialogs/confirm_download_dialog.dart
index 486310a7a..897c64cb2 100644
--- a/lib/components/shared/dialogs/confirm_download_dialog.dart
+++ b/lib/components/dialogs/confirm_download_dialog.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
diff --git a/lib/components/shared/dialogs/piped_down_dialog.dart b/lib/components/dialogs/piped_down_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/piped_down_dialog.dart
rename to lib/components/dialogs/piped_down_dialog.dart
diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart
similarity index 96%
rename from lib/components/shared/dialogs/playlist_add_track_dialog.dart
rename to lib/components/dialogs/playlist_add_track_dialog.dart
index 5d493a68d..5af9c9e4d 100644
--- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart
+++ b/lib/components/dialogs/playlist_add_track_dialog.dart
@@ -4,8 +4,8 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/playlist/playlist_create_dialog.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart';
diff --git a/lib/components/shared/dialogs/prompt_dialog.dart b/lib/components/dialogs/prompt_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/prompt_dialog.dart
rename to lib/components/dialogs/prompt_dialog.dart
diff --git a/lib/components/shared/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/replace_downloaded_dialog.dart
rename to lib/components/dialogs/replace_downloaded_dialog.dart
diff --git a/lib/components/shared/dialogs/select_device_dialog.dart b/lib/components/dialogs/select_device_dialog.dart
similarity index 88%
rename from lib/components/shared/dialogs/select_device_dialog.dart
rename to lib/components/dialogs/select_device_dialog.dart
index cd8dedb7c..3a3bde60a 100644
--- a/lib/components/shared/dialogs/select_device_dialog.dart
+++ b/lib/components/dialogs/select_device_dialog.dart
@@ -15,15 +15,12 @@ class SelectDeviceDialog extends HookConsumerWidget {
final remoteService = connectClients.asData!.value.resolvedService!;
return AlertDialog(
- title: const Text("Choose the device:"),
+ title: Text(context.l10n.choose_the_device),
insetPadding: const EdgeInsets.all(16),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
- const Text(
- "There are multiple device connected.\n"
- "Choose the device you want this action to take place",
- ),
+ Text(context.l10n.multiple_device_connected),
RadioListTile.adaptive(
title: Text(remoteService.name),
value: true,
@@ -33,7 +30,7 @@ class SelectDeviceDialog extends HookConsumerWidget {
},
),
RadioListTile.adaptive(
- title: const Text("This Device"),
+ title: Text(context.l10n.this_device),
value: false,
groupValue: isRemoteService.value,
onChanged: (value) {
diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart
similarity index 96%
rename from lib/components/shared/dialogs/track_details_dialog.dart
rename to lib/components/dialogs/track_details_dialog.dart
index da2a140b9..61bca7b1f 100644
--- a/lib/components/shared/dialogs/track_details_dialog.dart
+++ b/lib/components/dialogs/track_details_dialog.dart
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
-import 'package:spotube/components/shared/links/hyper_link.dart';
-import 'package:spotube/components/shared/links/link_text.dart';
+import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/links/hyper_link.dart';
+import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@@ -28,6 +28,7 @@ class TrackDetailsDialog extends HookWidget {
artists: track.artists ?? [],
mainAxisAlignment: WrapAlignment.start,
textStyle: const TextStyle(color: Colors.blue),
+ hideOverflowArtist: false,
),
context.l10n.album: LinkText(
track.album!.name!,
diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/expandable_search/expandable_search.dart
similarity index 100%
rename from lib/components/shared/expandable_search/expandable_search.dart
rename to lib/components/expandable_search/expandable_search.dart
diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart
similarity index 61%
rename from lib/components/shared/fallbacks/anonymous_fallback.dart
rename to lib/components/fallbacks/anonymous_fallback.dart
index 2f06b0b6f..62ed8ddd6 100644
--- a/lib/components/shared/fallbacks/anonymous_fallback.dart
+++ b/lib/components/fallbacks/anonymous_fallback.dart
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/pages/settings/settings.dart';
-import 'package:spotube/provider/authentication_provider.dart';
+import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget {
@@ -14,9 +15,13 @@ class AnonymousFallback extends ConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final isLoggedIn = ref.watch(authenticationProvider) != null;
+ final isLoggedIn = ref.watch(authenticationProvider);
- if (isLoggedIn && child != null) return child!;
+ if (isLoggedIn.isLoading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ if (isLoggedIn.asData?.value != null && child != null) return child!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -25,7 +30,7 @@ class AnonymousFallback extends ConsumerWidget {
const SizedBox(height: 10),
FilledButton(
child: Text(context.l10n.login_with_spotify),
- onPressed: () => ServiceUtils.push(context, "/settings"),
+ onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name),
)
],
),
diff --git a/lib/components/shared/fallbacks/not_found.dart b/lib/components/fallbacks/not_found.dart
similarity index 81%
rename from lib/components/shared/fallbacks/not_found.dart
rename to lib/components/fallbacks/not_found.dart
index 5a74f6720..ce168f178 100644
--- a/lib/components/shared/fallbacks/not_found.dart
+++ b/lib/components/fallbacks/not_found.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:spotube/collections/assets.gen.dart';
+import 'package:spotube/extensions/context.dart';
class NotFound extends StatelessWidget {
final bool vertical;
@@ -18,9 +19,9 @@ class NotFound extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Text("Nothing found", style: theme.textTheme.titleLarge),
+ Text(context.l10n.nothing_found, style: theme.textTheme.titleLarge),
Text(
- "The box is empty",
+ context.l10n.the_box_is_empty,
style: theme.textTheme.titleMedium,
),
],
diff --git a/lib/components/shared/heart_button.dart b/lib/components/heart_button/heart_button.dart
similarity index 66%
rename from lib/components/shared/heart_button.dart
rename to lib/components/heart_button/heart_button.dart
index c296d7a9c..fa4318cc0 100644
--- a/lib/components/shared/heart_button.dart
+++ b/lib/components/heart_button/heart_button.dart
@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
+import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/provider/scrobbler_provider.dart';
+import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HeartButton extends HookConsumerWidget {
@@ -27,7 +26,7 @@ class HeartButton extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
- if (auth == null) return const SizedBox.shrink();
+ if (auth.asData?.value == null) return const SizedBox.shrink();
return IconButton(
tooltip: tooltip,
@@ -55,38 +54,6 @@ class HeartButton extends HookConsumerWidget {
}
}
-typedef UseTrackToggleLike = ({
- bool isLiked,
- Future Function(Track track) toggleTrackLike,
-});
-
-UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
- final savedTracks = ref.watch(likedTracksProvider);
- final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
-
- final isLiked = useMemoized(
- () =>
- savedTracks.asData?.value.any((element) => element.id == track.id) ??
- false,
- [savedTracks.asData?.value, track.id],
- );
-
- final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
-
- return (
- isLiked: isLiked,
- toggleTrackLike: (track) async {
- await savedTracksNotifier.toggleFavorite(track);
-
- if (!isLiked) {
- await scrobblerNotifier.love(track);
- } else {
- await scrobblerNotifier.unlove(track);
- }
- },
- );
-}
-
class TrackHeartButton extends HookConsumerWidget {
final Track track;
const TrackHeartButton({
diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart
new file mode 100644
index 000000000..ba5cbee11
--- /dev/null
+++ b/lib/components/heart_button/use_track_toggle_like.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/provider/scrobbler/scrobbler.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
+
+typedef UseTrackToggleLike = ({
+ bool isLiked,
+ Future Function(Track track) toggleTrackLike,
+});
+
+UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
+ final savedTracks = ref.watch(likedTracksProvider);
+ final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
+
+ final isLiked = useMemoized(
+ () =>
+ savedTracks.asData?.value.any((element) => element.id == track.id) ??
+ false,
+ [savedTracks.asData?.value, track.id],
+ );
+
+ final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
+
+ return (
+ isLiked: isLiked,
+ toggleTrackLike: (track) async {
+ await savedTracksNotifier.toggleFavorite(track);
+
+ if (!isLiked) {
+ await scrobblerNotifier.love(track);
+ } else {
+ await scrobblerNotifier.unlove(track);
+ }
+ },
+ );
+}
diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
similarity index 95%
rename from lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
rename to lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
index e142cb35c..16204952d 100644
--- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
+++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
@@ -5,9 +5,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
-import 'package:spotube/components/album/album_card.dart';
-import 'package:spotube/components/artist/artist_card.dart';
-import 'package:spotube/components/playlist/playlist_card.dart';
+import 'package:spotube/modules/album/album_card.dart';
+import 'package:spotube/modules/artist/artist_card.dart';
+import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@@ -96,7 +96,7 @@ class HorizontalPlaybuttonCardView extends HookWidget {
return switch (item) {
PlaylistSimple() =>
PlaylistCard(item as PlaylistSimple),
- AlbumSimple() => AlbumCard(item as Album),
+ AlbumSimple() => AlbumCard(item as AlbumSimple),
Artist() => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
diff --git a/lib/components/shared/hover_builder.dart b/lib/components/hover_builder.dart
similarity index 100%
rename from lib/components/shared/hover_builder.dart
rename to lib/components/hover_builder.dart
diff --git a/lib/components/shared/image/universal_image.dart b/lib/components/image/universal_image.dart
similarity index 100%
rename from lib/components/shared/image/universal_image.dart
rename to lib/components/image/universal_image.dart
diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/inter_scrollbar/inter_scrollbar.dart
similarity index 80%
rename from lib/components/shared/inter_scrollbar/inter_scrollbar.dart
rename to lib/components/inter_scrollbar/inter_scrollbar.dart
index 2b3ce3192..8a86b6439 100644
--- a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart
+++ b/lib/components/inter_scrollbar/inter_scrollbar.dart
@@ -1,7 +1,7 @@
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:spotube/utils/platform.dart';
class InterScrollbar extends HookWidget {
final Widget child;
@@ -15,7 +15,7 @@ class InterScrollbar extends HookWidget {
@override
Widget build(BuildContext context) {
- if (DesktopTools.platform.isDesktop) return child;
+ if (kIsDesktop) return child;
return DraggableScrollbar.semicircle(
controller: controller,
diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart
deleted file mode 100644
index a7b2102b8..000000000
--- a/lib/components/library/user_local_tracks.dart
+++ /dev/null
@@ -1,331 +0,0 @@
-import 'dart:io';
-
-import 'package:catcher_2/catcher_2.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:collection/collection.dart';
-import 'package:fuzzywuzzy/fuzzywuzzy.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:metadata_god/metadata_god.dart';
-import 'package:mime/mime.dart';
-import 'package:path/path.dart';
-import 'package:path_provider/path_provider.dart';
-import 'package:skeletonizer/skeletonizer.dart';
-
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/fake.dart';
-import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
-import 'package:spotube/components/shared/fallbacks/not_found.dart';
-import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
-import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
-import 'package:spotube/components/shared/track_tile/track_tile.dart';
-import 'package:spotube/extensions/artist_simple.dart';
-import 'package:spotube/extensions/context.dart';
-import 'package:spotube/extensions/track.dart';
-import 'package:spotube/models/local_track.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/utils/service_utils.dart';
-// ignore: depend_on_referenced_packages
-import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
-
-const supportedAudioTypes = [
- "audio/webm",
- "audio/ogg",
- "audio/mpeg",
- "audio/mp4",
- "audio/opus",
- "audio/wav",
- "audio/aac",
-];
-
-const imgMimeToExt = {
- "image/png": ".png",
- "image/jpeg": ".jpg",
- "image/webp": ".webp",
- "image/gif": ".gif",
-};
-
-enum SortBy {
- none,
- ascending,
- descending,
- newest,
- oldest,
- duration,
- artist,
- album,
-}
-
-final localTracksProvider = FutureProvider>((ref) async {
- try {
- if (kIsWeb) return [];
- final downloadLocation = ref.watch(
- userPreferencesProvider.select((s) => s.downloadLocation),
- );
- if (downloadLocation.isEmpty) return [];
- final downloadDir = Directory(downloadLocation);
- if (!await downloadDir.exists()) {
- await downloadDir.create(recursive: true);
- return [];
- }
- final entities = downloadDir.listSync(recursive: true);
-
- final filesWithMetadata = (await Future.wait(
- entities.map((e) => File(e.path)).where((file) {
- final mimetype = lookupMimeType(file.path);
- return mimetype != null && supportedAudioTypes.contains(mimetype);
- }).map(
- (file) async {
- try {
- final metadata = await MetadataGod.readMetadata(file: file.path);
-
- final imageFile = File(join(
- (await getTemporaryDirectory()).path,
- "spotube",
- basenameWithoutExtension(file.path) +
- imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
- ));
- if (!await imageFile.exists() && metadata.picture != null) {
- await imageFile.create(recursive: true);
- await imageFile.writeAsBytes(
- metadata.picture?.data ?? [],
- mode: FileMode.writeOnly,
- );
- }
-
- return {"metadata": metadata, "file": file, "art": imageFile.path};
- } catch (e, stack) {
- if (e is FfiException) {
- return {"file": file};
- }
- Catcher2.reportCheckedError(e, stack);
- return {};
- }
- },
- ),
- ))
- .where((e) => e.isNotEmpty)
- .toList();
-
- final tracks = filesWithMetadata
- .map(
- (fileWithMetadata) => LocalTrack.fromTrack(
- track: Track().fromFile(
- fileWithMetadata["file"],
- metadata: fileWithMetadata["metadata"],
- art: fileWithMetadata["art"],
- ),
- path: fileWithMetadata["file"].path,
- ),
- )
- .toList();
-
- return tracks;
- } catch (e, stack) {
- Catcher2.reportCheckedError(e, stack);
- return [];
- }
-});
-
-class UserLocalTracks extends HookConsumerWidget {
- const UserLocalTracks({super.key});
-
- Future playLocalTracks(
- WidgetRef ref,
- List tracks, {
- LocalTrack? currentTrack,
- }) async {
- final playlist = ref.read(proxyPlaylistProvider);
- final playback = ref.read(proxyPlaylistProvider.notifier);
- currentTrack ??= tracks.first;
- final isPlaylistPlaying = playlist.containsTracks(tracks);
- if (!isPlaylistPlaying) {
- await playback.load(
- tracks,
- initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
- autoPlay: true,
- );
- } else if (isPlaylistPlaying &&
- currentTrack.id != null &&
- currentTrack.id != playlist.activeTrack?.id) {
- await playback.jumpToTrack(currentTrack);
- }
- }
-
- @override
- Widget build(BuildContext context, ref) {
- final sortBy = useState(SortBy.none);
- final playlist = ref.watch(proxyPlaylistProvider);
- final trackSnapshot = ref.watch(localTracksProvider);
- final isPlaylistPlaying =
- playlist.containsTracks(trackSnapshot.asData?.value ?? []);
-
- final searchController = useTextEditingController();
- useValueListenable(searchController);
- final searchFocus = useFocusNode();
- final isFiltering = useState(false);
-
- final controller = useScrollController();
-
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Row(
- children: [
- const SizedBox(width: 5),
- FilledButton(
- onPressed: trackSnapshot.asData?.value != null
- ? () async {
- if (trackSnapshot.asData?.value.isNotEmpty == true) {
- if (!isPlaylistPlaying) {
- await playLocalTracks(
- ref,
- trackSnapshot.asData!.value,
- );
- }
- }
- }
- : null,
- child: Row(
- children: [
- Text(context.l10n.play),
- Icon(
- isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
- )
- ],
- ),
- ),
- const Spacer(),
- ExpandableSearchButton(
- isFiltering: isFiltering.value,
- onPressed: (value) => isFiltering.value = value,
- searchFocus: searchFocus,
- ),
- const SizedBox(width: 10),
- SortTracksDropdown(
- value: sortBy.value,
- onChanged: (value) {
- sortBy.value = value;
- },
- ),
- const SizedBox(width: 5),
- FilledButton(
- child: const Icon(SpotubeIcons.refresh),
- onPressed: () {
- ref.invalidate(localTracksProvider);
- },
- )
- ],
- ),
- ),
- ExpandableSearchField(
- searchController: searchController,
- searchFocus: searchFocus,
- isFiltering: isFiltering.value,
- onChangeFiltering: (value) => isFiltering.value = value,
- ),
- trackSnapshot.when(
- data: (tracks) {
- final sortedTracks = useMemoized(() {
- return ServiceUtils.sortTracks(tracks, sortBy.value);
- }, [sortBy.value, tracks]);
-
- final filteredTracks = useMemoized(() {
- if (searchController.text.isEmpty) {
- return sortedTracks;
- }
- return sortedTracks
- .map((e) => (
- weightedRatio(
- "${e.name} - ${e.artists?.asString() ?? ""}",
- searchController.text,
- ),
- e,
- ))
- .toList()
- .sorted(
- (a, b) => b.$1.compareTo(a.$1),
- )
- .where((e) => e.$1 > 50)
- .map((e) => e.$2)
- .toList()
- .toList();
- }, [searchController.text, sortedTracks]);
-
- if (!trackSnapshot.isLoading && filteredTracks.isEmpty) {
- return const Expanded(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [NotFound()],
- ),
- );
- }
-
- return Expanded(
- child: RefreshIndicator(
- onRefresh: () async {
- ref.invalidate(localTracksProvider);
- },
- child: InterScrollbar(
- controller: controller,
- child: Skeletonizer(
- enabled: trackSnapshot.isLoading,
- child: ListView.builder(
- controller: controller,
- physics: const AlwaysScrollableScrollPhysics(),
- itemCount:
- trackSnapshot.isLoading ? 5 : filteredTracks.length,
- itemBuilder: (context, index) {
- if (trackSnapshot.isLoading) {
- return TrackTile(
- playlist: playlist,
- track: FakeData.track,
- index: index,
- );
- }
-
- final track = filteredTracks[index];
- return TrackTile(
- index: index,
- playlist: playlist,
- track: track,
- userPlaylist: false,
- onTap: () async {
- await playLocalTracks(
- ref,
- sortedTracks,
- currentTrack: track,
- );
- },
- );
- },
- ),
- ),
- ),
- ),
- );
- },
- loading: () => Expanded(
- child: Skeletonizer(
- enabled: true,
- child: ListView.builder(
- itemCount: 5,
- itemBuilder: (context, index) => TrackTile(
- track: FakeData.track,
- index: index,
- playlist: playlist,
- ),
- ),
- ),
- ),
- error: (error, stackTrace) =>
- Text(error.toString() + stackTrace.toString()),
- )
- ],
- );
- }
-}
diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/links/anchor_button.dart
similarity index 95%
rename from lib/components/shared/links/anchor_button.dart
rename to lib/components/links/anchor_button.dart
index d78bbf962..c6f0b889b 100644
--- a/lib/components/shared/links/anchor_button.dart
+++ b/lib/components/links/anchor_button.dart
@@ -29,7 +29,7 @@ class AnchorButton extends HookWidget {
onTapUp: (event) => tap.value = false,
onTap: onTap,
child: MouseRegion(
- cursor: MaterialStateMouseCursor.clickable,
+ cursor: WidgetStateMouseCursor.clickable,
child: Text(
text,
style: style.copyWith(
diff --git a/lib/components/links/artist_link.dart b/lib/components/links/artist_link.dart
new file mode 100644
index 000000000..9f06f1b37
--- /dev/null
+++ b/lib/components/links/artist_link.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/components/links/anchor_button.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/pages/artist/artist.dart';
+import 'package:spotube/utils/service_utils.dart';
+
+class ArtistLink extends StatelessWidget {
+ final List artists;
+ final WrapCrossAlignment crossAxisAlignment;
+ final WrapAlignment mainAxisAlignment;
+ final TextStyle textStyle;
+ final bool hideOverflowArtist;
+ final void Function(String route)? onRouteChange;
+ final VoidCallback? onOverflowArtistClick;
+
+ const ArtistLink({
+ super.key,
+ required this.artists,
+ this.crossAxisAlignment = WrapCrossAlignment.center,
+ this.mainAxisAlignment = WrapAlignment.center,
+ this.textStyle = const TextStyle(),
+ this.onRouteChange,
+ this.hideOverflowArtist = true,
+ this.onOverflowArtistClick,
+ }) : assert(hideOverflowArtist ? onOverflowArtistClick != null : true);
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData(:colorScheme) = Theme.of(context);
+
+ return Wrap(
+ crossAxisAlignment: crossAxisAlignment,
+ alignment: mainAxisAlignment,
+ children: [
+ ...(hideOverflowArtist ? artists.take(3).toList() : artists)
+ .asMap()
+ .entries
+ .map(
+ (artist) => Builder(builder: (context) {
+ if (artist.value.name == null) {
+ return Text("Spotify", style: textStyle);
+ }
+ return AnchorButton(
+ (artist.key != artists.length - 1)
+ ? "${artist.value.name}, "
+ : artist.value.name!,
+ onTap: () {
+ if (onRouteChange != null) {
+ onRouteChange?.call("/artist/${artist.value.id}");
+ } else {
+ ServiceUtils.pushNamed(
+ context,
+ ArtistPage.name,
+ pathParameters: {
+ "id": artist.value.id!,
+ },
+ );
+ }
+ },
+ overflow: TextOverflow.ellipsis,
+ style: textStyle,
+ );
+ }),
+ ),
+ if (hideOverflowArtist && artists.length > 3)
+ AnchorButton(
+ context.l10n.and_n_more(artists.length - 3),
+ onTap: () {
+ onOverflowArtistClick?.call();
+ },
+ overflow: TextOverflow.ellipsis,
+ style: textStyle.copyWith(
+ color: colorScheme.secondary,
+ decoration: TextDecoration.underline,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/components/shared/links/hyper_link.dart b/lib/components/links/hyper_link.dart
similarity index 92%
rename from lib/components/shared/links/hyper_link.dart
rename to lib/components/links/hyper_link.dart
index f84517b45..32d715e01 100644
--- a/lib/components/shared/links/hyper_link.dart
+++ b/lib/components/links/hyper_link.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/components/links/anchor_button.dart';
import 'package:url_launcher/url_launcher_string.dart';
class Hyperlink extends StatelessWidget {
diff --git a/lib/components/shared/links/link_text.dart b/lib/components/links/link_text.dart
similarity index 93%
rename from lib/components/shared/links/link_text.dart
rename to lib/components/links/link_text.dart
index db7b6358c..0cab71d0d 100644
--- a/lib/components/shared/links/link_text.dart
+++ b/lib/components/links/link_text.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/utils/service_utils.dart';
class LinkText extends StatelessWidget {
diff --git a/lib/components/shared/panels/controller.dart b/lib/components/panels/controller.dart
similarity index 99%
rename from lib/components/shared/panels/controller.dart
rename to lib/components/panels/controller.dart
index 65c2444e7..834e9ce6d 100644
--- a/lib/components/shared/panels/controller.dart
+++ b/lib/components/panels/controller.dart
@@ -1,4 +1,4 @@
-part of './sliding_up_panel.dart';
+part of 'sliding_up_panel.dart';
class PanelController extends ChangeNotifier {
SlidingUpPanelState? _panelState;
diff --git a/lib/components/shared/panels/helpers.dart b/lib/components/panels/helpers.dart
similarity index 98%
rename from lib/components/shared/panels/helpers.dart
rename to lib/components/panels/helpers.dart
index 6d0dde310..d79fa97c7 100644
--- a/lib/components/shared/panels/helpers.dart
+++ b/lib/components/panels/helpers.dart
@@ -1,4 +1,4 @@
-part of "./sliding_up_panel.dart";
+part of "sliding_up_panel.dart";
/// if you want to prevent the panel from being dragged using the widget,
/// wrap the widget with this
diff --git a/lib/components/shared/panels/sliding_up_panel.dart b/lib/components/panels/sliding_up_panel.dart
similarity index 100%
rename from lib/components/shared/panels/sliding_up_panel.dart
rename to lib/components/panels/sliding_up_panel.dart
diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/playbutton_card.dart
similarity index 91%
rename from lib/components/shared/playbutton_card.dart
rename to lib/components/playbutton_card.dart
index 80a27eb01..d540d31e2 100644
--- a/lib/components/shared/playbutton_card.dart
+++ b/lib/components/playbutton_card.dart
@@ -3,23 +3,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart';
-
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/hover_builder.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/hover_builder.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
-final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true);
-
-String? useDescription(String? description) {
- return useMemoized(() {
- if (description == null) return null;
- return description.replaceAll(htmlTagRegexp, '');
- }, [description]);
-}
-
class PlaybuttonCard extends HookWidget {
final void Function()? onTap;
final void Function()? onPlaybuttonPressed;
@@ -66,19 +58,18 @@ class PlaybuttonCard extends HookWidget {
others: 15,
);
- final cleanDescription = useDescription(description);
-
+ var unescapeHtml = description?.unescapeHtml();
return Container(
constraints: BoxConstraints(maxWidth: size),
margin: margin,
child: Material(
color: Color.lerp(
- theme.colorScheme.surfaceVariant,
+ theme.colorScheme.surfaceContainerHighest,
theme.colorScheme.surface,
useBrightnessValue(.9, .7),
),
borderRadius: radius,
- shadowColor: theme.colorScheme.background,
+ shadowColor: theme.colorScheme.surface,
elevation: 3,
child: InkWell(
mouseCursor: SystemMouseCursors.click,
@@ -137,7 +128,7 @@ class PlaybuttonCard extends HookWidget {
),
if (isHovered)
Text(
- "Owned by you",
+ context.l10n.owned_by_you,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white,
),
@@ -158,7 +149,7 @@ class PlaybuttonCard extends HookWidget {
Skeleton.keep(
child: IconButton(
style: IconButton.styleFrom(
- backgroundColor: theme.colorScheme.background,
+ backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.primary,
minimumSize: const Size.square(10),
),
@@ -205,11 +196,11 @@ class PlaybuttonCard extends HookWidget {
overflow: TextOverflow.ellipsis,
),
),
- if (cleanDescription != null)
+ if (description != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: AutoSizeText(
- cleanDescription,
+ unescapeHtml!,
maxLines: 2,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(.5),
diff --git a/lib/components/shared/links/artist_link.dart b/lib/components/shared/links/artist_link.dart
deleted file mode 100644
index af8b186af..000000000
--- a/lib/components/shared/links/artist_link.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-import 'package:flutter/widgets.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
-import 'package:spotube/utils/service_utils.dart';
-
-class ArtistLink extends StatelessWidget {
- final List artists;
- final WrapCrossAlignment crossAxisAlignment;
- final WrapAlignment mainAxisAlignment;
- final TextStyle textStyle;
- final void Function(String route)? onRouteChange;
-
- const ArtistLink({
- super.key,
- required this.artists,
- this.crossAxisAlignment = WrapCrossAlignment.center,
- this.mainAxisAlignment = WrapAlignment.center,
- this.textStyle = const TextStyle(),
- this.onRouteChange,
- });
-
- @override
- Widget build(BuildContext context) {
- return Wrap(
- crossAxisAlignment: crossAxisAlignment,
- alignment: mainAxisAlignment,
- children: artists
- .asMap()
- .entries
- .map(
- (artist) => Builder(builder: (context) {
- if (artist.value.name == null) {
- return Text("Spotify", style: textStyle);
- }
- return AnchorButton(
- (artist.key != artists.length - 1)
- ? "${artist.value.name}, "
- : artist.value.name!,
- onTap: () {
- if (onRouteChange != null) {
- onRouteChange?.call("/artist/${artist.value.id}");
- } else {
- ServiceUtils.push(
- context,
- "/artist/${artist.value.id}",
- );
- }
- },
- overflow: TextOverflow.ellipsis,
- style: textStyle,
- );
- }),
- )
- .toList(),
- );
- }
-}
diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart
deleted file mode 100644
index 37daefa95..000000000
--- a/lib/components/shared/page_window_title_bar.dart
+++ /dev/null
@@ -1,650 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/utils/platform.dart';
-import 'package:titlebar_buttons/titlebar_buttons.dart';
-import 'dart:math';
-import 'package:flutter/foundation.dart' show kIsWeb;
-import 'dart:io' show Platform;
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
-
-class PageWindowTitleBar extends StatefulHookConsumerWidget
- implements PreferredSizeWidget {
- final Widget? leading;
- final bool automaticallyImplyLeading;
- final List? actions;
- final Color? backgroundColor;
- final Color? foregroundColor;
- final IconThemeData? actionsIconTheme;
- final bool? centerTitle;
- final double? titleSpacing;
- final double toolbarOpacity;
- final double? leadingWidth;
- final TextStyle? toolbarTextStyle;
- final TextStyle? titleTextStyle;
- final double? titleWidth;
- final Widget? title;
-
- final bool _sliver;
-
- const PageWindowTitleBar({
- super.key,
- this.actions,
- this.title,
- this.toolbarOpacity = 1,
- this.backgroundColor,
- this.actionsIconTheme,
- this.automaticallyImplyLeading = false,
- this.centerTitle,
- this.foregroundColor,
- this.leading,
- this.leadingWidth,
- this.titleSpacing,
- this.titleTextStyle,
- this.titleWidth,
- this.toolbarTextStyle,
- }) : _sliver = false,
- pinned = false,
- floating = false,
- snap = false,
- stretch = false;
-
- final bool pinned;
- final bool floating;
- final bool snap;
- final bool stretch;
-
- const PageWindowTitleBar.sliver({
- super.key,
- this.actions,
- this.title,
- this.backgroundColor,
- this.actionsIconTheme,
- this.automaticallyImplyLeading = false,
- this.centerTitle,
- this.foregroundColor,
- this.leading,
- this.leadingWidth,
- this.titleSpacing,
- this.titleTextStyle,
- this.titleWidth,
- this.toolbarTextStyle,
- this.pinned = false,
- this.floating = false,
- this.snap = false,
- this.stretch = false,
- }) : _sliver = true,
- toolbarOpacity = 1;
-
- @override
- Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-
- @override
- ConsumerState createState() => _PageWindowTitleBarState();
-}
-
-class _PageWindowTitleBarState extends ConsumerState {
- void onDrag(details) {
- final systemTitleBar =
- ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
- if (kIsDesktop && !systemTitleBar) {
- DesktopTools.window.startDragging();
- }
- }
-
- @override
- Widget build(BuildContext context) {
- final mediaQuery = MediaQuery.of(context);
-
- if (widget._sliver) {
- return SliverLayoutBuilder(
- builder: (context, constraints) {
- final hasFullscreen =
- mediaQuery.size.width == constraints.crossAxisExtent;
- final hasLeadingOrCanPop =
- widget.leading != null || Navigator.canPop(context);
-
- return SliverPadding(
- padding: EdgeInsets.only(
- left: DesktopTools.platform.isMacOS &&
- hasFullscreen &&
- hasLeadingOrCanPop
- ? 65
- : 0,
- ),
- sliver: SliverAppBar(
- leading: widget.leading,
- automaticallyImplyLeading: widget.automaticallyImplyLeading,
- actions: [
- ...?widget.actions,
- WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
- ],
- backgroundColor: widget.backgroundColor,
- foregroundColor: widget.foregroundColor,
- actionsIconTheme: widget.actionsIconTheme,
- centerTitle: widget.centerTitle,
- titleSpacing: widget.titleSpacing,
- leadingWidth: widget.leadingWidth,
- toolbarTextStyle: widget.toolbarTextStyle,
- titleTextStyle: widget.titleTextStyle,
- title: widget.title,
- pinned: widget.pinned,
- floating: widget.floating,
- snap: widget.snap,
- stretch: widget.stretch,
- ),
- );
- },
- );
- }
-
- return LayoutBuilder(builder: (context, constrains) {
- final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
- final hasLeadingOrCanPop =
- widget.leading != null || Navigator.canPop(context);
-
- return GestureDetector(
- onHorizontalDragStart: onDrag,
- onVerticalDragStart: onDrag,
- child: Padding(
- padding: EdgeInsets.only(
- left: DesktopTools.platform.isMacOS &&
- hasFullscreen &&
- hasLeadingOrCanPop
- ? 65
- : 0,
- ),
- child: AppBar(
- leading: widget.leading,
- automaticallyImplyLeading: widget.automaticallyImplyLeading,
- actions: [
- ...?widget.actions,
- WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
- ],
- backgroundColor: widget.backgroundColor,
- foregroundColor: widget.foregroundColor,
- actionsIconTheme: widget.actionsIconTheme,
- centerTitle: widget.centerTitle,
- titleSpacing: widget.titleSpacing,
- toolbarOpacity: widget.toolbarOpacity,
- leadingWidth: widget.leadingWidth,
- toolbarTextStyle: widget.toolbarTextStyle,
- titleTextStyle: widget.titleTextStyle,
- title: widget.title,
- ),
- ),
- );
- });
- }
-}
-
-class WindowTitleBarButtons extends HookConsumerWidget {
- final Color? foregroundColor;
- const WindowTitleBarButtons({
- super.key,
- this.foregroundColor,
- });
-
- @override
- Widget build(BuildContext context, ref) {
- final preferences = ref.watch(userPreferencesProvider);
- final isMaximized = useState(null);
- const type = ThemeType.auto;
-
- Future onClose() async {
- await DesktopTools.window.close();
- }
-
- useEffect(() {
- if (kIsDesktop) {
- DesktopTools.window.isMaximized().then((value) {
- isMaximized.value = value;
- });
- }
- return null;
- }, []);
-
- if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
- return const SizedBox.shrink();
- }
-
- if (kIsWindows) {
- final theme = Theme.of(context);
- final colors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
- mouseOver: theme.colorScheme.onBackground.withOpacity(0.1),
- mouseDown: theme.colorScheme.onBackground.withOpacity(0.2),
- iconMouseOver: theme.colorScheme.onBackground,
- iconMouseDown: theme.colorScheme.onBackground,
- );
-
- final closeColors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
- mouseOver: Colors.red,
- mouseDown: Colors.red[800]!,
- iconMouseOver: Colors.white,
- iconMouseDown: Colors.black,
- );
-
- return Padding(
- padding: const EdgeInsets.only(bottom: 25),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- MinimizeWindowButton(
- onPressed: DesktopTools.window.minimize,
- colors: colors,
- ),
- if (isMaximized.value != true)
- MaximizeWindowButton(
- colors: colors,
- onPressed: () {
- DesktopTools.window.maximize();
- isMaximized.value = true;
- },
- )
- else
- RestoreWindowButton(
- colors: colors,
- onPressed: () {
- DesktopTools.window.unmaximize();
- isMaximized.value = false;
- },
- ),
- CloseWindowButton(
- colors: closeColors,
- onPressed: onClose,
- ),
- ],
- ),
- );
- }
-
- return Padding(
- padding: const EdgeInsets.only(bottom: 20, left: 10),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- DecoratedMinimizeButton(
- type: type,
- onPressed: DesktopTools.window.minimize,
- ),
- DecoratedMaximizeButton(
- type: type,
- onPressed: () async {
- if (await DesktopTools.window.isMaximized()) {
- await DesktopTools.window.unmaximize();
- isMaximized.value = false;
- } else {
- await DesktopTools.window.maximize();
- isMaximized.value = true;
- }
- },
- ),
- DecoratedCloseButton(
- type: type,
- onPressed: onClose,
- ),
- ],
- ),
- );
- }
-}
-
-typedef WindowButtonIconBuilder = Widget Function(
- WindowButtonContext buttonContext);
-typedef WindowButtonBuilder = Widget Function(
- WindowButtonContext buttonContext, Widget icon);
-
-class WindowButtonContext {
- BuildContext context;
- MouseState mouseState;
- Color? backgroundColor;
- Color iconColor;
- WindowButtonContext(
- {required this.context,
- required this.mouseState,
- this.backgroundColor,
- required this.iconColor});
-}
-
-class WindowButtonColors {
- late Color normal;
- late Color mouseOver;
- late Color mouseDown;
- late Color iconNormal;
- late Color iconMouseOver;
- late Color iconMouseDown;
- WindowButtonColors(
- {Color? normal,
- Color? mouseOver,
- Color? mouseDown,
- Color? iconNormal,
- Color? iconMouseOver,
- Color? iconMouseDown}) {
- this.normal = normal ?? _defaultButtonColors.normal;
- this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
- this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
- this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
- this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
- this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
- }
-}
-
-final _defaultButtonColors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: const Color(0xFF805306),
- mouseOver: const Color(0xFF404040),
- mouseDown: const Color(0xFF202020),
- iconMouseOver: const Color(0xFFFFFFFF),
- iconMouseDown: const Color(0xFFF0F0F0),
-);
-
-class WindowButton extends StatelessWidget {
- final WindowButtonBuilder? builder;
- final WindowButtonIconBuilder? iconBuilder;
- late final WindowButtonColors colors;
- final bool animate;
- final EdgeInsets? padding;
- final VoidCallback? onPressed;
-
- WindowButton(
- {super.key,
- WindowButtonColors? colors,
- this.builder,
- @required this.iconBuilder,
- this.padding,
- this.onPressed,
- this.animate = false}) {
- this.colors = colors ?? _defaultButtonColors;
- }
-
- Color getBackgroundColor(MouseState mouseState) {
- if (mouseState.isMouseDown) return colors.mouseDown;
- if (mouseState.isMouseOver) return colors.mouseOver;
- return colors.normal;
- }
-
- Color getIconColor(MouseState mouseState) {
- if (mouseState.isMouseDown) return colors.iconMouseDown;
- if (mouseState.isMouseOver) return colors.iconMouseOver;
- return colors.iconNormal;
- }
-
- @override
- Widget build(BuildContext context) {
- if (kIsWeb) {
- return Container();
- } else {
- // Don't show button on macOS
- if (Platform.isMacOS) {
- return Container();
- }
- }
-
- return MouseStateBuilder(
- builder: (context, mouseState) {
- WindowButtonContext buttonContext = WindowButtonContext(
- mouseState: mouseState,
- context: context,
- backgroundColor: getBackgroundColor(mouseState),
- iconColor: getIconColor(mouseState));
-
- var icon =
- (iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
-
- var fadeOutColor =
- getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0);
- var padding = this.padding ?? const EdgeInsets.all(10);
- var animationMs =
- mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
- Widget iconWithPadding = Padding(padding: padding, child: icon);
- iconWithPadding = AnimatedContainer(
- curve: Curves.easeOut,
- duration: Duration(milliseconds: animationMs),
- color: buttonContext.backgroundColor ?? fadeOutColor,
- child: iconWithPadding);
- var button =
- (builder != null) ? builder!(buttonContext, icon) : iconWithPadding;
- return SizedBox(
- width: 45,
- height: 32,
- child: button,
- );
- },
- onPressed: () {
- if (onPressed != null) onPressed!();
- },
- );
- }
-}
-
-class MinimizeWindowButton extends WindowButton {
- MinimizeWindowButton(
- {super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- MinimizeIcon(color: buttonContext.iconColor),
- );
-}
-
-class MaximizeWindowButton extends WindowButton {
- MaximizeWindowButton(
- {super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- MaximizeIcon(color: buttonContext.iconColor),
- );
-}
-
-class RestoreWindowButton extends WindowButton {
- RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- RestoreIcon(color: buttonContext.iconColor),
- );
-}
-
-final _defaultCloseButtonColors = WindowButtonColors(
- mouseOver: const Color(0xFFD32F2F),
- mouseDown: const Color(0xFFB71C1C),
- iconNormal: const Color(0xFF805306),
- iconMouseOver: const Color(0xFFFFFFFF));
-
-class CloseWindowButton extends WindowButton {
- CloseWindowButton(
- {super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
- : super(
- colors: colors ?? _defaultCloseButtonColors,
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- CloseIcon(color: buttonContext.iconColor),
- );
-}
-
-// Switched to CustomPaint icons by https://github.com/esDotDev
-
-/// Close
-class CloseIcon extends StatelessWidget {
- final Color color;
- const CloseIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => Align(
- alignment: Alignment.topLeft,
- child: Stack(children: [
- // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
- Transform.rotate(
- angle: pi * .25,
- child:
- Center(child: Container(width: 14, height: 1, color: color))),
- Transform.rotate(
- angle: pi * -.25,
- child:
- Center(child: Container(width: 14, height: 1, color: color))),
- ]),
- );
-}
-
-/// Maximize
-class MaximizeIcon extends StatelessWidget {
- final Color color;
- const MaximizeIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
-}
-
-class _MaximizePainter extends _IconPainter {
- _MaximizePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
- }
-}
-
-/// Restore
-class RestoreIcon extends StatelessWidget {
- final Color color;
- const RestoreIcon({
- super.key,
- required this.color,
- });
- @override
- Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
-}
-
-class _RestorePainter extends _IconPainter {
- _RestorePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
- canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
- canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
- canvas.drawLine(
- Offset(size.width, 0), Offset(size.width, size.height - 2), p);
- canvas.drawLine(Offset(size.width, size.height - 2),
- Offset(size.width - 2, size.height - 2), p);
- }
-}
-
-/// Minimize
-class MinimizeIcon extends StatelessWidget {
- final Color color;
- const MinimizeIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
-}
-
-class _MinimizePainter extends _IconPainter {
- _MinimizePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawLine(
- Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
- }
-}
-
-/// Helpers
-abstract class _IconPainter extends CustomPainter {
- _IconPainter(this.color);
- final Color color;
-
- @override
- bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
-}
-
-class _AlignedPaint extends StatelessWidget {
- const _AlignedPaint(this.painter);
- final CustomPainter painter;
-
- @override
- Widget build(BuildContext context) {
- return Align(
- alignment: Alignment.center,
- child: CustomPaint(size: const Size(10, 10), painter: painter));
- }
-}
-
-Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
- ..color = color
- ..style = PaintingStyle.stroke
- ..isAntiAlias = isAntiAlias
- ..strokeWidth = 1;
-
-typedef MouseStateBuilderCB = Widget Function(
- BuildContext context, MouseState mouseState);
-
-class MouseState {
- bool isMouseOver = false;
- bool isMouseDown = false;
- MouseState();
- @override
- String toString() {
- return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver";
- }
-}
-
-T? _ambiguate(T? value) => value;
-
-class MouseStateBuilder extends StatefulWidget {
- final MouseStateBuilderCB builder;
- final VoidCallback? onPressed;
- const MouseStateBuilder({super.key, required this.builder, this.onPressed});
- @override
- // ignore: library_private_types_in_public_api
- _MouseStateBuilderState createState() => _MouseStateBuilderState();
-}
-
-class _MouseStateBuilderState extends State {
- late MouseState _mouseState;
- _MouseStateBuilderState() {
- _mouseState = MouseState();
- }
-
- @override
- Widget build(BuildContext context) {
- return MouseRegion(
- onEnter: (event) {
- setState(() {
- _mouseState.isMouseOver = true;
- });
- },
- onExit: (event) {
- setState(() {
- _mouseState.isMouseOver = false;
- });
- },
- child: GestureDetector(
- onTapDown: (_) {
- setState(() {
- _mouseState.isMouseDown = true;
- });
- },
- onTapCancel: () {
- setState(() {
- _mouseState.isMouseDown = false;
- });
- },
- onTap: () {
- setState(() {
- _mouseState.isMouseDown = false;
- _mouseState.isMouseOver = false;
- });
- _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
- if (widget.onPressed != null) {
- widget.onPressed!();
- }
- });
- },
- onTapUp: (_) {},
- child: widget.builder(context, _mouseState)));
- }
-}
diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart
deleted file mode 100644
index 30912da22..000000000
--- a/lib/components/shared/track_tile/track_tile.dart
+++ /dev/null
@@ -1,268 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:skeletonizer/skeletonizer.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/hover_builder.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
-import 'package:spotube/components/shared/links/link_text.dart';
-import 'package:spotube/components/shared/track_tile/track_options.dart';
-import 'package:spotube/extensions/artist_simple.dart';
-import 'package:spotube/extensions/constrains.dart';
-import 'package:spotube/extensions/duration.dart';
-import 'package:spotube/extensions/image.dart';
-import 'package:spotube/models/local_track.dart';
-import 'package:spotube/provider/blacklist_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
-
-class TrackTile extends HookConsumerWidget {
- /// [index] will not be shown if null
- final int? index;
- final Track track;
- final bool selected;
- final ValueChanged? onChanged;
- final Future Function()? onTap;
- final VoidCallback? onLongPress;
- final bool userPlaylist;
- final String? playlistId;
- final ProxyPlaylist playlist;
-
- final List? leadingActions;
-
- const TrackTile({
- super.key,
- this.index,
- required this.track,
- this.selected = false,
- required this.playlist,
- this.onTap,
- this.onLongPress,
- this.onChanged,
- this.userPlaylist = false,
- this.playlistId,
- this.leadingActions,
- });
-
- @override
- Widget build(BuildContext context, ref) {
- final theme = Theme.of(context);
-
- final blacklist = ref.watch(blacklistProvider);
-
- final isBlackListed = useMemoized(
- () => blacklist.contains(
- BlacklistedElement.track(
- track.id!,
- track.name!,
- ),
- ),
- [blacklist, track],
- );
-
- final showOptionCbRef = useRef?>(null);
-
- final isLoading = useState(false);
-
- final isPlaying = playlist.activeTrack?.id == track.id;
-
- final isSelected = isPlaying || isLoading.value;
-
- return LayoutBuilder(builder: (context, constrains) {
- return Listener(
- onPointerDown: (event) {
- if (event.buttons != kSecondaryMouseButton) return;
- showOptionCbRef.value?.call(
- RelativeRect.fromLTRB(
- event.position.dx,
- event.position.dy,
- constrains.maxWidth - event.position.dx,
- constrains.maxHeight - event.position.dy,
- ),
- );
- },
- child: HoverBuilder(
- permanentState: isSelected || constrains.smAndDown ? true : null,
- builder: (context, isHovering) {
- return ListTile(
- selected: isSelected,
- onTap: () async {
- try {
- isLoading.value = true;
- await onTap?.call();
- } finally {
- if (context.mounted) {
- isLoading.value = false;
- }
- }
- },
- onLongPress: onLongPress,
- enabled: !isBlackListed,
- contentPadding: EdgeInsets.zero,
- tileColor:
- isBlackListed ? theme.colorScheme.errorContainer : null,
- horizontalTitleGap: 12,
- leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
- leading: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- ...?leadingActions,
- if (index != null && onChanged == null && constrains.mdAndUp)
- SizedBox(
- width: 50,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 6),
- child: Text(
- '${(index ?? 0) + 1}',
- maxLines: 1,
- style: theme.textTheme.bodySmall,
- textAlign: TextAlign.center,
- ),
- ),
- )
- else if (constrains.smAndDown)
- const SizedBox(width: 16),
- if (onChanged != null)
- Checkbox(
- value: selected,
- onChanged: onChanged,
- ),
- Stack(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(4),
- child: AspectRatio(
- aspectRatio: 1,
- child: UniversalImage(
- path: (track.album?.images).asUrlString(
- placeholder: ImagePlaceholder.albumArt,
- ),
- fit: BoxFit.cover,
- ),
- ),
- ),
- Positioned.fill(
- child: AnimatedContainer(
- duration: const Duration(milliseconds: 300),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(4),
- color: isHovering
- ? Colors.black.withOpacity(0.4)
- : Colors.transparent,
- ),
- ),
- ),
- Positioned.fill(
- child: Center(
- child: IconTheme(
- data: theme.iconTheme
- .copyWith(size: 26, color: Colors.white),
- child: Skeleton.ignore(
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 300),
- child: (isPlaying && playlist.isFetching) ||
- isLoading.value
- ? const SizedBox(
- width: 26,
- height: 26,
- child: CircularProgressIndicator(
- strokeWidth: 1.5,
- color: Colors.white,
- ),
- )
- : isPlaying
- ? Icon(
- SpotubeIcons.pause,
- color: theme.colorScheme.primary,
- )
- : !isHovering
- ? const SizedBox.shrink()
- : const Icon(SpotubeIcons.play),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- title: Row(
- children: [
- Expanded(
- flex: 6,
- child: LinkText(
- track.name!,
- "/track/${track.id}",
- push: true,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- if (constrains.mdAndUp) ...[
- const SizedBox(width: 8),
- Expanded(
- flex: 4,
- child: switch (track.runtimeType) {
- LocalTrack() => Text(
- track.album!.name!,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- _ => Align(
- alignment: Alignment.centerLeft,
- child: LinkText(
- track.album!.name!,
- "/album/${track.album?.id}",
- extra: track.album,
- push: true,
- overflow: TextOverflow.ellipsis,
- ),
- )
- },
- ),
- ],
- ],
- ),
- subtitle: Align(
- alignment: Alignment.centerLeft,
- child: track is LocalTrack
- ? Text(
- track.artists?.asString() ?? '',
- )
- : ClipRect(
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxHeight: 40),
- child: ArtistLink(artists: track.artists ?? []),
- ),
- ),
- ),
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const SizedBox(width: 8),
- Text(
- Duration(milliseconds: track.durationMs ?? 0)
- .toHumanReadableString(padZero: false),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- TrackOptions(
- track: track,
- playlistId: playlistId,
- userPlaylist: userPlaylist,
- showMenuCbRef: showOptionCbRef,
- ),
- ],
- ),
- );
- },
- ),
- );
- });
- }
-}
diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shimmers/shimmer_lyrics.dart
similarity index 100%
rename from lib/components/shared/shimmers/shimmer_lyrics.dart
rename to lib/components/shimmers/shimmer_lyrics.dart
diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/sort_tracks_dropdown.dart
similarity index 94%
rename from lib/components/shared/sort_tracks_dropdown.dart
rename to lib/components/sort_tracks_dropdown.dart
index be72d689b..167270134 100644
--- a/lib/components/shared/sort_tracks_dropdown.dart
+++ b/lib/components/sort_tracks_dropdown.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/library/user_local_tracks.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/modules/library/user_local_tracks.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/extensions/context.dart';
class SortTracksDropdown extends StatelessWidget {
diff --git a/lib/components/shared/spotube_page_route.dart b/lib/components/spotube_page_route.dart
similarity index 100%
rename from lib/components/shared/spotube_page_route.dart
rename to lib/components/spotube_page_route.dart
diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/themed_button_tab_bar.dart
similarity index 87%
rename from lib/components/shared/themed_button_tab_bar.dart
rename to lib/components/themed_button_tab_bar.dart
index 017f04aa8..c245e5f4e 100644
--- a/lib/components/shared/themed_button_tab_bar.dart
+++ b/lib/components/themed_button_tab_bar.dart
@@ -5,7 +5,8 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
final List tabs;
- const ThemedButtonsTabBar({super.key, required this.tabs});
+ final TabController? controller;
+ const ThemedButtonsTabBar({super.key, required this.tabs, this.controller});
@override
Widget build(BuildContext context) {
@@ -21,6 +22,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
bottom: 8,
),
child: ButtonsTabBar(
+ controller: controller,
radius: 100,
decoration: BoxDecoration(
color: bgColor,
@@ -32,7 +34,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
),
borderWidth: 0,
unselectedDecoration: BoxDecoration(
- color: theme.colorScheme.background,
+ color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(15),
),
unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith(
diff --git a/lib/components/titlebar/mouse_state.dart b/lib/components/titlebar/mouse_state.dart
new file mode 100644
index 000000000..9af2a8b0b
--- /dev/null
+++ b/lib/components/titlebar/mouse_state.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+
+typedef MouseStateBuilderCB = Widget Function(
+ BuildContext context, MouseState mouseState);
+
+class MouseState {
+ bool isMouseOver = false;
+ bool isMouseDown = false;
+ MouseState();
+ @override
+ String toString() {
+ return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver";
+ }
+}
+
+T? _ambiguate(T? value) => value;
+
+class MouseStateBuilder extends StatefulWidget {
+ final MouseStateBuilderCB builder;
+ final VoidCallback? onPressed;
+ const MouseStateBuilder({super.key, required this.builder, this.onPressed});
+ @override
+ // ignore: library_private_types_in_public_api
+ _MouseStateBuilderState createState() => _MouseStateBuilderState();
+}
+
+class _MouseStateBuilderState extends State {
+ late MouseState _mouseState;
+ _MouseStateBuilderState() {
+ _mouseState = MouseState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MouseRegion(
+ onEnter: (event) {
+ setState(() {
+ _mouseState.isMouseOver = true;
+ });
+ },
+ onExit: (event) {
+ setState(() {
+ _mouseState.isMouseOver = false;
+ });
+ },
+ child: GestureDetector(
+ onTapDown: (_) {
+ setState(() {
+ _mouseState.isMouseDown = true;
+ });
+ },
+ onTapCancel: () {
+ setState(() {
+ _mouseState.isMouseDown = false;
+ });
+ },
+ onTap: () {
+ setState(() {
+ _mouseState.isMouseDown = false;
+ _mouseState.isMouseOver = false;
+ });
+ _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
+ if (widget.onPressed != null) {
+ widget.onPressed!();
+ }
+ });
+ },
+ onTapUp: (_) {},
+ child: widget.builder(context, _mouseState),
+ ),
+ );
+ }
+}
diff --git a/lib/components/titlebar/titlebar.dart b/lib/components/titlebar/titlebar.dart
new file mode 100644
index 000000000..76a5ec8a5
--- /dev/null
+++ b/lib/components/titlebar/titlebar.dart
@@ -0,0 +1,179 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/components/titlebar/titlebar_buttons.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+import 'package:spotube/utils/platform.dart';
+
+import 'package:window_manager/window_manager.dart';
+
+class PageWindowTitleBar extends StatefulHookConsumerWidget
+ implements PreferredSizeWidget {
+ final Widget? leading;
+ final bool automaticallyImplyLeading;
+ final List? actions;
+ final Color? backgroundColor;
+ final Color? foregroundColor;
+ final IconThemeData? actionsIconTheme;
+ final bool? centerTitle;
+ final double? titleSpacing;
+ final double toolbarOpacity;
+ final double? leadingWidth;
+ final TextStyle? toolbarTextStyle;
+ final TextStyle? titleTextStyle;
+ final double? titleWidth;
+ final Widget? title;
+
+ final bool _sliver;
+
+ const PageWindowTitleBar({
+ super.key,
+ this.actions,
+ this.title,
+ this.toolbarOpacity = 1,
+ this.backgroundColor,
+ this.actionsIconTheme,
+ this.automaticallyImplyLeading = false,
+ this.centerTitle,
+ this.foregroundColor,
+ this.leading,
+ this.leadingWidth,
+ this.titleSpacing,
+ this.titleTextStyle,
+ this.titleWidth,
+ this.toolbarTextStyle,
+ }) : _sliver = false,
+ pinned = false,
+ floating = false,
+ snap = false,
+ stretch = false;
+
+ final bool pinned;
+ final bool floating;
+ final bool snap;
+ final bool stretch;
+
+ const PageWindowTitleBar.sliver({
+ super.key,
+ this.actions,
+ this.title,
+ this.backgroundColor,
+ this.actionsIconTheme,
+ this.automaticallyImplyLeading = false,
+ this.centerTitle,
+ this.foregroundColor,
+ this.leading,
+ this.leadingWidth,
+ this.titleSpacing,
+ this.titleTextStyle,
+ this.titleWidth,
+ this.toolbarTextStyle,
+ this.pinned = false,
+ this.floating = false,
+ this.snap = false,
+ this.stretch = false,
+ }) : _sliver = true,
+ toolbarOpacity = 1;
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+
+ @override
+ ConsumerState createState() => _PageWindowTitleBarState();
+}
+
+class _PageWindowTitleBarState extends ConsumerState {
+ void onDrag(details) {
+ final systemTitleBar =
+ ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
+ if (kIsDesktop && !systemTitleBar) {
+ windowManager.startDragging();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final mediaQuery = MediaQuery.of(context);
+
+ if (widget._sliver) {
+ return SliverLayoutBuilder(
+ builder: (context, constraints) {
+ final hasFullscreen =
+ mediaQuery.size.width == constraints.crossAxisExtent;
+ final hasLeadingOrCanPop =
+ widget.leading != null || Navigator.canPop(context);
+
+ return SliverPadding(
+ padding: EdgeInsets.only(
+ left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
+ ),
+ sliver: SliverAppBar(
+ leading: widget.leading,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ actions: [
+ ...?widget.actions,
+ WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
+ ],
+ backgroundColor: widget.backgroundColor,
+ foregroundColor: widget.foregroundColor,
+ actionsIconTheme: widget.actionsIconTheme,
+ centerTitle: widget.centerTitle,
+ titleSpacing: widget.titleSpacing,
+ leadingWidth: widget.leadingWidth,
+ toolbarTextStyle: widget.toolbarTextStyle,
+ titleTextStyle: widget.titleTextStyle,
+ title: SizedBox(
+ width: double.infinity, // workaround to force dragging
+ child: widget.title ?? const Text(""),
+ ),
+ pinned: widget.pinned,
+ floating: widget.floating,
+ snap: widget.snap,
+ stretch: widget.stretch,
+ ),
+ );
+ },
+ );
+ }
+
+ return LayoutBuilder(builder: (context, constrains) {
+ final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
+ final hasLeadingOrCanPop =
+ widget.leading != null || Navigator.canPop(context);
+
+ return GestureDetector(
+ onHorizontalDragStart: onDrag,
+ onVerticalDragStart: onDrag,
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
+ ),
+ child: AppBar(
+ leading: widget.leading,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ actions: [
+ ...?widget.actions,
+ WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
+ ],
+ backgroundColor: widget.backgroundColor,
+ foregroundColor: widget.foregroundColor,
+ actionsIconTheme: widget.actionsIconTheme,
+ centerTitle: widget.centerTitle,
+ titleSpacing: widget.titleSpacing,
+ toolbarOpacity: widget.toolbarOpacity,
+ leadingWidth: widget.leadingWidth,
+ toolbarTextStyle: widget.toolbarTextStyle,
+ titleTextStyle: widget.titleTextStyle,
+ title: SizedBox(
+ width: double.infinity, // workaround to force dragging
+ child: widget.title ?? const Text(""),
+ ),
+ scrolledUnderElevation: 0,
+ shadowColor: Colors.transparent,
+ forceMaterialTransparency: true,
+ elevation: 0,
+ ),
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/components/titlebar/titlebar_buttons.dart b/lib/components/titlebar/titlebar_buttons.dart
new file mode 100644
index 000000000..35cdf08e1
--- /dev/null
+++ b/lib/components/titlebar/titlebar_buttons.dart
@@ -0,0 +1,124 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart';
+import 'package:spotube/components/titlebar/window_button.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+import 'package:spotube/utils/platform.dart';
+import 'package:titlebar_buttons/titlebar_buttons.dart';
+import 'package:window_manager/window_manager.dart';
+
+class WindowTitleBarButtons extends HookConsumerWidget {
+ final Color? foregroundColor;
+ const WindowTitleBarButtons({
+ super.key,
+ this.foregroundColor,
+ });
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+ final isMaximized = useState(null);
+ const type = ThemeType.auto;
+
+ Future onClose() async {
+ await windowManager.close();
+ }
+
+ useEffect(() {
+ if (kIsDesktop) {
+ windowManager.isMaximized().then((value) {
+ isMaximized.value = value;
+ });
+ }
+ return null;
+ }, []);
+
+ if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
+ return const SizedBox.shrink();
+ }
+
+ if (kIsWindows) {
+ final theme = Theme.of(context);
+ final colors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
+ mouseOver: theme.colorScheme.onSurface.withOpacity(0.1),
+ mouseDown: theme.colorScheme.onSurface.withOpacity(0.2),
+ iconMouseOver: theme.colorScheme.onSurface,
+ iconMouseDown: theme.colorScheme.onSurface,
+ );
+
+ final closeColors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
+ mouseOver: Colors.red,
+ mouseDown: Colors.red[800]!,
+ iconMouseOver: Colors.white,
+ iconMouseDown: Colors.black,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 25),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MinimizeWindowButton(
+ onPressed: windowManager.minimize,
+ colors: colors,
+ ),
+ if (isMaximized.value != true)
+ MaximizeWindowButton(
+ colors: colors,
+ onPressed: () {
+ windowManager.maximize();
+ isMaximized.value = true;
+ },
+ )
+ else
+ RestoreWindowButton(
+ colors: colors,
+ onPressed: () {
+ windowManager.unmaximize();
+ isMaximized.value = false;
+ },
+ ),
+ CloseWindowButton(
+ colors: closeColors,
+ onPressed: onClose,
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 20, left: 10),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ DecoratedMinimizeButton(
+ type: type,
+ onPressed: windowManager.minimize,
+ ),
+ DecoratedMaximizeButton(
+ type: type,
+ onPressed: () async {
+ if (await windowManager.isMaximized()) {
+ await windowManager.unmaximize();
+ isMaximized.value = false;
+ } else {
+ await windowManager.maximize();
+ isMaximized.value = true;
+ }
+ },
+ ),
+ DecoratedCloseButton(
+ type: type,
+ onPressed: onClose,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/titlebar/titlebar_icon_buttons.dart b/lib/components/titlebar/titlebar_icon_buttons.dart
new file mode 100644
index 000000000..701702623
--- /dev/null
+++ b/lib/components/titlebar/titlebar_icon_buttons.dart
@@ -0,0 +1,161 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:spotube/components/titlebar/window_button.dart';
+
+class MinimizeWindowButton extends WindowButton {
+ MinimizeWindowButton(
+ {super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ MinimizeIcon(color: buttonContext.iconColor),
+ );
+}
+
+class MaximizeWindowButton extends WindowButton {
+ MaximizeWindowButton(
+ {super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ MaximizeIcon(color: buttonContext.iconColor),
+ );
+}
+
+class RestoreWindowButton extends WindowButton {
+ RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ RestoreIcon(color: buttonContext.iconColor),
+ );
+}
+
+final _defaultCloseButtonColors = WindowButtonColors(
+ mouseOver: const Color(0xFFD32F2F),
+ mouseDown: const Color(0xFFB71C1C),
+ iconNormal: const Color(0xFF805306),
+ iconMouseOver: const Color(0xFFFFFFFF));
+
+class CloseWindowButton extends WindowButton {
+ CloseWindowButton(
+ {super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
+ : super(
+ colors: colors ?? _defaultCloseButtonColors,
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ CloseIcon(color: buttonContext.iconColor),
+ );
+}
+
+// Switched to CustomPaint icons by https://github.com/esDotDev
+
+/// Close
+class CloseIcon extends StatelessWidget {
+ final Color color;
+ const CloseIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => Align(
+ alignment: Alignment.topLeft,
+ child: Stack(children: [
+ // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
+ Transform.rotate(
+ angle: pi * .25,
+ child:
+ Center(child: Container(width: 14, height: 1, color: color))),
+ Transform.rotate(
+ angle: pi * -.25,
+ child:
+ Center(child: Container(width: 14, height: 1, color: color))),
+ ]),
+ );
+}
+
+/// Maximize
+class MaximizeIcon extends StatelessWidget {
+ final Color color;
+ const MaximizeIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
+}
+
+class _MaximizePainter extends _IconPainter {
+ _MaximizePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
+ }
+}
+
+/// Restore
+class RestoreIcon extends StatelessWidget {
+ final Color color;
+ const RestoreIcon({
+ super.key,
+ required this.color,
+ });
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
+}
+
+class _RestorePainter extends _IconPainter {
+ _RestorePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
+ canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
+ canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
+ canvas.drawLine(
+ Offset(size.width, 0), Offset(size.width, size.height - 2), p);
+ canvas.drawLine(Offset(size.width, size.height - 2),
+ Offset(size.width - 2, size.height - 2), p);
+ }
+}
+
+/// Minimize
+class MinimizeIcon extends StatelessWidget {
+ final Color color;
+ const MinimizeIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
+}
+
+class _MinimizePainter extends _IconPainter {
+ _MinimizePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawLine(
+ Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
+ }
+}
+
+/// Helpers
+abstract class _IconPainter extends CustomPainter {
+ _IconPainter(this.color);
+ final Color color;
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
+
+class _AlignedPaint extends StatelessWidget {
+ const _AlignedPaint(this.painter);
+ final CustomPainter painter;
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.center,
+ child: CustomPaint(size: const Size(10, 10), painter: painter));
+ }
+}
+
+Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
+ ..color = color
+ ..style = PaintingStyle.stroke
+ ..isAntiAlias = isAntiAlias
+ ..strokeWidth = 1;
diff --git a/lib/components/titlebar/window_button.dart b/lib/components/titlebar/window_button.dart
new file mode 100644
index 000000000..3201d191a
--- /dev/null
+++ b/lib/components/titlebar/window_button.dart
@@ -0,0 +1,133 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:spotube/components/titlebar/mouse_state.dart';
+
+typedef WindowButtonIconBuilder = Widget Function(
+ WindowButtonContext buttonContext);
+typedef WindowButtonBuilder = Widget Function(
+ WindowButtonContext buttonContext, Widget icon);
+
+class WindowButtonContext {
+ BuildContext context;
+ MouseState mouseState;
+ Color? backgroundColor;
+ Color iconColor;
+ WindowButtonContext(
+ {required this.context,
+ required this.mouseState,
+ this.backgroundColor,
+ required this.iconColor});
+}
+
+class WindowButtonColors {
+ late Color normal;
+ late Color mouseOver;
+ late Color mouseDown;
+ late Color iconNormal;
+ late Color iconMouseOver;
+ late Color iconMouseDown;
+ WindowButtonColors(
+ {Color? normal,
+ Color? mouseOver,
+ Color? mouseDown,
+ Color? iconNormal,
+ Color? iconMouseOver,
+ Color? iconMouseDown}) {
+ this.normal = normal ?? _defaultButtonColors.normal;
+ this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
+ this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
+ this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
+ this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
+ this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
+ }
+}
+
+final _defaultButtonColors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: const Color(0xFF805306),
+ mouseOver: const Color(0xFF404040),
+ mouseDown: const Color(0xFF202020),
+ iconMouseOver: const Color(0xFFFFFFFF),
+ iconMouseDown: const Color(0xFFF0F0F0),
+);
+
+class WindowButton extends StatelessWidget {
+ final WindowButtonBuilder? builder;
+ final WindowButtonIconBuilder? iconBuilder;
+ late final WindowButtonColors colors;
+ final bool animate;
+ final EdgeInsets? padding;
+ final VoidCallback? onPressed;
+
+ WindowButton(
+ {super.key,
+ WindowButtonColors? colors,
+ this.builder,
+ @required this.iconBuilder,
+ this.padding,
+ this.onPressed,
+ this.animate = false}) {
+ this.colors = colors ?? _defaultButtonColors;
+ }
+
+ Color getBackgroundColor(MouseState mouseState) {
+ if (mouseState.isMouseDown) return colors.mouseDown;
+ if (mouseState.isMouseOver) return colors.mouseOver;
+ return colors.normal;
+ }
+
+ Color getIconColor(MouseState mouseState) {
+ if (mouseState.isMouseDown) return colors.iconMouseDown;
+ if (mouseState.isMouseOver) return colors.iconMouseOver;
+ return colors.iconNormal;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (kIsWeb) {
+ return Container();
+ } else {
+ // Don't show button on macOS
+ if (Platform.isMacOS) {
+ return Container();
+ }
+ }
+
+ return MouseStateBuilder(
+ builder: (context, mouseState) {
+ WindowButtonContext buttonContext = WindowButtonContext(
+ mouseState: mouseState,
+ context: context,
+ backgroundColor: getBackgroundColor(mouseState),
+ iconColor: getIconColor(mouseState));
+
+ var icon =
+ (iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
+
+ var fadeOutColor =
+ getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0);
+ var padding = this.padding ?? const EdgeInsets.all(10);
+ var animationMs =
+ mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
+ Widget iconWithPadding = Padding(padding: padding, child: icon);
+ iconWithPadding = AnimatedContainer(
+ curve: Curves.easeOut,
+ duration: Duration(milliseconds: animationMs),
+ color: buttonContext.backgroundColor ?? fadeOutColor,
+ child: iconWithPadding);
+ var button =
+ (builder != null) ? builder!(buttonContext, icon) : iconWithPadding;
+ return SizedBox(
+ width: 45,
+ height: 32,
+ child: button,
+ );
+ },
+ onPressed: () {
+ if (onPressed != null) onPressed!();
+ },
+ );
+ }
+}
diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart
similarity index 60%
rename from lib/components/shared/track_tile/track_options.dart
rename to lib/components/track_tile/track_options.dart
index a9ec36b94..84b0f41f6 100644
--- a/lib/components/shared/track_tile/track_options.dart
+++ b/lib/components/track_tile/track_options.dart
@@ -8,24 +8,27 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/library/user_local_tracks.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
-import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
-import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
-import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
-import 'package:spotube/components/shared/heart_button.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
+import 'package:spotube/components/dialogs/prompt_dialog.dart';
+import 'package:spotube/components/dialogs/track_details_dialog.dart';
+import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
+import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart';
-import 'package:spotube/provider/authentication_provider.dart';
+import 'package:spotube/pages/track/track.dart';
+import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
+import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
+import 'package:spotube/utils/service_utils.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -95,8 +98,8 @@ class TrackOptions extends HookConsumerWidget {
WidgetRef ref,
Track track,
) async {
- final playback = ref.read(proxyPlaylistProvider.notifier);
- final playlist = ref.read(proxyPlaylistProvider);
+ final playback = ref.read(audioPlayerProvider.notifier);
+ final playlist = ref.read(audioPlayerProvider);
final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio";
final pages =
@@ -159,8 +162,8 @@ class TrackOptions extends HookConsumerWidget {
final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context);
- final playlist = ref.watch(proxyPlaylistProvider);
- final playback = ref.watch(proxyPlaylistProvider.notifier);
+ final playlist = ref.watch(audioPlayerProvider);
+ final playback = ref.watch(audioPlayerProvider.notifier);
final auth = ref.watch(authenticationProvider);
ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier);
@@ -170,11 +173,8 @@ class TrackOptions extends HookConsumerWidget {
final favorites = useTrackToggleLike(track, ref);
final isBlackListed = useMemoized(
- () => blacklist.contains(
- BlacklistedElement.track(
- track.id!,
- track.name!,
- ),
+ () => blacklist.asData?.value.any(
+ (element) => element.elementId == track.id,
),
[blacklist, track],
);
@@ -197,6 +197,8 @@ class TrackOptions extends HookConsumerWidget {
return downloadManager.getProgressNotifier(spotubeTrack);
});
+ final isLocalTrack = track is LocalTrack;
+
final adaptivePopSheetList = AdaptivePopSheetList(
onSelected: (value) async {
switch (value) {
@@ -256,13 +258,16 @@ class TrackOptions extends HookConsumerWidget {
.removeTracks(playlistId ?? "", [track.id!]);
break;
case TrackOptionValue.blacklist:
- if (isBlackListed) {
- ref.read(blacklistProvider.notifier).remove(
- BlacklistedElement.track(track.id!, track.name!),
- );
+ if (isBlackListed == null) break;
+ if (isBlackListed == true) {
+ await ref.read(blacklistProvider.notifier).remove(track.id!);
} else {
- ref.read(blacklistProvider.notifier).add(
- BlacklistedElement.track(track.id!, track.name!),
+ await ref.read(blacklistProvider.notifier).add(
+ BlacklistTableCompanion.insert(
+ name: track.name!,
+ elementId: track.id!,
+ elementType: BlacklistedType.track,
+ ),
);
}
break;
@@ -310,122 +315,133 @@ class TrackOptions extends HookConsumerWidget {
),
subtitle: Align(
alignment: Alignment.centerLeft,
- child: ArtistLink(artists: track.artists!),
+ child: ArtistLink(
+ artists: track.artists!,
+ onOverflowArtistClick: () => ServiceUtils.pushNamed(
+ context,
+ TrackPage.name,
+ pathParameters: {
+ "id": track.id!,
+ },
+ ),
+ ),
),
),
],
- children: switch (track.runtimeType) {
- LocalTrack() => [
- PopSheetEntry(
- value: TrackOptionValue.delete,
- leading: const Icon(SpotubeIcons.trash),
- title: Text(context.l10n.delete),
- )
- ],
- _ => [
- if (mediaQuery.smAndDown)
- PopSheetEntry(
- value: TrackOptionValue.album,
- leading: const Icon(SpotubeIcons.album),
- title: Text(context.l10n.go_to_album),
- subtitle: Text(track.album!.name!),
- ),
- if (!playlist.containsTrack(track)) ...[
- PopSheetEntry(
- value: TrackOptionValue.addToQueue,
- leading: const Icon(SpotubeIcons.queueAdd),
- title: Text(context.l10n.add_to_queue),
- ),
- PopSheetEntry(
- value: TrackOptionValue.playNext,
- leading: const Icon(SpotubeIcons.lightning),
- title: Text(context.l10n.play_next),
- ),
- ] else
- PopSheetEntry(
- value: TrackOptionValue.removeFromQueue,
- enabled: playlist.activeTrack?.id != track.id,
- leading: const Icon(SpotubeIcons.queueRemove),
- title: Text(context.l10n.remove_from_queue),
- ),
- if (me.asData?.value != null)
- PopSheetEntry(
- value: TrackOptionValue.favorite,
- leading: favorites.isLiked
- ? const Icon(
- SpotubeIcons.heartFilled,
- color: Colors.pink,
- )
- : const Icon(SpotubeIcons.heart),
- title: Text(
- favorites.isLiked
- ? context.l10n.remove_from_favorites
- : context.l10n.save_as_favorite,
- ),
- ),
- if (auth != null) ...[
- PopSheetEntry(
- value: TrackOptionValue.startRadio,
- leading: const Icon(SpotubeIcons.radio),
- title: Text(context.l10n.start_a_radio),
- ),
- PopSheetEntry(
- value: TrackOptionValue.addToPlaylist,
- leading: const Icon(SpotubeIcons.playlistAdd),
- title: Text(context.l10n.add_to_playlist),
- ),
- ],
- if (userPlaylist && auth != null)
- PopSheetEntry(
- value: TrackOptionValue.removeFromPlaylist,
- leading: const Icon(SpotubeIcons.removeFilled),
- title: Text(context.l10n.remove_from_playlist),
- ),
- PopSheetEntry(
- value: TrackOptionValue.download,
- enabled: !isInQueue,
- leading: isInQueue
- ? HookBuilder(builder: (context) {
- final progress = useListenable(progressNotifier!);
- return CircularProgressIndicator(
- value: progress.value,
- );
- })
- : const Icon(SpotubeIcons.download),
- title: Text(context.l10n.download_track),
- ),
- PopSheetEntry(
- value: TrackOptionValue.blacklist,
- leading: const Icon(SpotubeIcons.playlistRemove),
- iconColor: !isBlackListed ? Colors.red[400] : null,
- textColor: !isBlackListed ? Colors.red[400] : null,
- title: Text(
- isBlackListed
- ? context.l10n.remove_from_blacklist
- : context.l10n.add_to_blacklist,
- ),
- ),
- PopSheetEntry(
- value: TrackOptionValue.share,
- leading: const Icon(SpotubeIcons.share),
- title: Text(context.l10n.share),
+ children: [
+ if (isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.delete,
+ leading: const Icon(SpotubeIcons.trash),
+ title: Text(context.l10n.delete),
+ ),
+ if (mediaQuery.smAndDown && !isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.album,
+ leading: const Icon(SpotubeIcons.album),
+ title: Text(context.l10n.go_to_album),
+ subtitle: Text(track.album!.name!),
+ ),
+ if (!playlist.containsTrack(track)) ...[
+ PopSheetEntry(
+ value: TrackOptionValue.addToQueue,
+ leading: const Icon(SpotubeIcons.queueAdd),
+ title: Text(context.l10n.add_to_queue),
+ ),
+ PopSheetEntry(
+ value: TrackOptionValue.playNext,
+ leading: const Icon(SpotubeIcons.lightning),
+ title: Text(context.l10n.play_next),
+ ),
+ ] else
+ PopSheetEntry(
+ value: TrackOptionValue.removeFromQueue,
+ enabled: playlist.activeTrack?.id != track.id,
+ leading: const Icon(SpotubeIcons.queueRemove),
+ title: Text(context.l10n.remove_from_queue),
+ ),
+ if (me.asData?.value != null && !isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.favorite,
+ leading: favorites.isLiked
+ ? const Icon(
+ SpotubeIcons.heartFilled,
+ color: Colors.pink,
+ )
+ : const Icon(SpotubeIcons.heart),
+ title: Text(
+ favorites.isLiked
+ ? context.l10n.remove_from_favorites
+ : context.l10n.save_as_favorite,
),
- PopSheetEntry(
- value: TrackOptionValue.songlink,
- leading: Assets.logos.songlinkTransparent.image(
- width: 22,
- height: 22,
- color: colorScheme.onSurface.withOpacity(0.5),
- ),
- title: Text(context.l10n.song_link),
+ ),
+ if (auth.asData?.value != null && !isLocalTrack) ...[
+ PopSheetEntry(
+ value: TrackOptionValue.startRadio,
+ leading: const Icon(SpotubeIcons.radio),
+ title: Text(context.l10n.start_a_radio),
+ ),
+ PopSheetEntry(
+ value: TrackOptionValue.addToPlaylist,
+ leading: const Icon(SpotubeIcons.playlistAdd),
+ title: Text(context.l10n.add_to_playlist),
+ ),
+ ],
+ if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.removeFromPlaylist,
+ leading: const Icon(SpotubeIcons.removeFilled),
+ title: Text(context.l10n.remove_from_playlist),
+ ),
+ if (!isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.download,
+ enabled: !isInQueue,
+ leading: isInQueue
+ ? HookBuilder(builder: (context) {
+ final progress = useListenable(progressNotifier!);
+ return CircularProgressIndicator(
+ value: progress.value,
+ );
+ })
+ : const Icon(SpotubeIcons.download),
+ title: Text(context.l10n.download_track),
+ ),
+ if (!isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.blacklist,
+ leading: const Icon(SpotubeIcons.playlistRemove),
+ iconColor: isBlackListed != true ? Colors.red[400] : null,
+ textColor: isBlackListed != true ? Colors.red[400] : null,
+ title: Text(
+ isBlackListed == true
+ ? context.l10n.remove_from_blacklist
+ : context.l10n.add_to_blacklist,
),
- PopSheetEntry(
- value: TrackOptionValue.details,
- leading: const Icon(SpotubeIcons.info),
- title: Text(context.l10n.details),
+ ),
+ if (!isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.share,
+ leading: const Icon(SpotubeIcons.share),
+ title: Text(context.l10n.share),
+ ),
+ if (!isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.songlink,
+ leading: Assets.logos.songlinkTransparent.image(
+ width: 22,
+ height: 22,
+ color: colorScheme.onSurface.withOpacity(0.5),
),
- ]
- },
+ title: Text(context.l10n.song_link),
+ ),
+ if (!isLocalTrack)
+ PopSheetEntry(
+ value: TrackOptionValue.details,
+ leading: const Icon(SpotubeIcons.info),
+ title: Text(context.l10n.details),
+ ),
+ ],
);
//! This is the most ANTI pattern I've ever done, but it works
diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart
new file mode 100644
index 000000000..12ce063f2
--- /dev/null
+++ b/lib/components/track_tile/track_tile.dart
@@ -0,0 +1,286 @@
+import 'dart:async';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/hover_builder.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/links/link_text.dart';
+import 'package:spotube/components/track_tile/track_options.dart';
+import 'package:spotube/extensions/artist_simple.dart';
+import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/duration.dart';
+import 'package:spotube/extensions/image.dart';
+import 'package:spotube/models/local_track.dart';
+import 'package:spotube/pages/track/track.dart';
+import 'package:spotube/provider/audio_player/querying_track_info.dart';
+import 'package:spotube/provider/audio_player/state.dart';
+import 'package:spotube/provider/blacklist_provider.dart';
+import 'package:spotube/utils/service_utils.dart';
+
+class TrackTile extends HookConsumerWidget {
+ /// [index] will not be shown if null
+ final int? index;
+ final Track track;
+ final bool selected;
+ final ValueChanged? onChanged;
+ final Future Function()? onTap;
+ final VoidCallback? onLongPress;
+ final bool userPlaylist;
+ final String? playlistId;
+ final AudioPlayerState playlist;
+
+ final List? leadingActions;
+
+ const TrackTile({
+ super.key,
+ this.index,
+ required this.track,
+ this.selected = false,
+ required this.playlist,
+ this.onTap,
+ this.onLongPress,
+ this.onChanged,
+ this.userPlaylist = false,
+ this.playlistId,
+ this.leadingActions,
+ });
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final theme = Theme.of(context);
+
+ final blacklist = ref.watch(blacklistProvider);
+ final blacklistNotifier = ref.watch(blacklistProvider.notifier);
+
+ final isBlackListed = useMemoized(
+ () => blacklistNotifier.contains(track),
+ [blacklist, track],
+ );
+
+ final showOptionCbRef = useRef?>(null);
+
+ final isLoading = useState(false);
+
+ final isPlaying = playlist.activeTrack?.id == track.id;
+
+ final isSelected = isPlaying || isLoading.value;
+
+ return LayoutBuilder(builder: (context, constrains) {
+ return Listener(
+ onPointerDown: (event) {
+ if (event.buttons != kSecondaryMouseButton) return;
+ showOptionCbRef.value?.call(
+ RelativeRect.fromLTRB(
+ event.position.dx,
+ event.position.dy,
+ constrains.maxWidth - event.position.dx,
+ constrains.maxHeight - event.position.dy,
+ ),
+ );
+ },
+ child: HoverBuilder(
+ permanentState: isSelected || constrains.smAndDown ? true : null,
+ builder: (context, isHovering) => ListTile(
+ selected: isSelected,
+ onTap: () async {
+ try {
+ isLoading.value = true;
+ await onTap?.call();
+ } finally {
+ if (context.mounted) {
+ isLoading.value = false;
+ }
+ }
+ },
+ onLongPress: onLongPress,
+ enabled: !isBlackListed,
+ contentPadding: EdgeInsets.zero,
+ tileColor: isBlackListed ? theme.colorScheme.errorContainer : null,
+ horizontalTitleGap: 12,
+ leadingAndTrailingTextStyle: theme.textTheme.bodyMedium,
+ leading: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ...?leadingActions,
+ if (index != null && onChanged == null && constrains.mdAndUp)
+ SizedBox(
+ width: 50,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: Text(
+ '${(index ?? 0) + 1}',
+ maxLines: 1,
+ style: theme.textTheme.bodySmall,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ )
+ else if (constrains.smAndDown)
+ const SizedBox(width: 16),
+ if (onChanged != null)
+ Checkbox(
+ value: selected,
+ onChanged: onChanged,
+ ),
+ Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: UniversalImage(
+ path: (track.album?.images).asUrlString(
+ placeholder: ImagePlaceholder.albumArt,
+ ),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ Positioned.fill(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(4),
+ color: isHovering
+ ? Colors.black.withOpacity(0.4)
+ : Colors.transparent,
+ ),
+ ),
+ ),
+ Positioned.fill(
+ child: Center(
+ child: IconTheme(
+ data: theme.iconTheme
+ .copyWith(size: 26, color: Colors.white),
+ child: Skeleton.ignore(
+ child: Consumer(
+ builder: (context, ref, _) {
+ final isFetchingActiveTrack =
+ ref.watch(queryingTrackInfoProvider);
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: (isPlaying && isFetchingActiveTrack) ||
+ isLoading.value
+ ? const SizedBox(
+ width: 26,
+ height: 26,
+ child: CircularProgressIndicator(
+ strokeWidth: 1.5,
+ color: Colors.white,
+ ),
+ )
+ : isPlaying
+ ? Icon(
+ SpotubeIcons.pause,
+ color: theme.colorScheme.primary,
+ )
+ : !isHovering
+ ? const SizedBox.shrink()
+ : const Icon(SpotubeIcons.play),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ title: Row(
+ children: [
+ Expanded(
+ flex: 6,
+ child: switch (track) {
+ LocalTrack() => Text(
+ track.name!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ _ => LinkText(
+ track.name!,
+ "/track/${track.id}",
+ push: true,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ },
+ ),
+ if (constrains.mdAndUp) ...[
+ const SizedBox(width: 8),
+ Expanded(
+ flex: 4,
+ child: switch (track) {
+ LocalTrack() => Text(
+ track.album!.name!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ _ => Align(
+ alignment: Alignment.centerLeft,
+ child: LinkText(
+ track.album!.name!,
+ "/album/${track.album?.id}",
+ extra: track.album,
+ push: true,
+ overflow: TextOverflow.ellipsis,
+ ),
+ )
+ },
+ ),
+ ],
+ ],
+ ),
+ subtitle: Align(
+ alignment: Alignment.centerLeft,
+ child: track is LocalTrack
+ ? Text(
+ track.artists?.asString() ?? '',
+ )
+ : ClipRect(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 40),
+ child: ArtistLink(
+ artists: track.artists ?? [],
+ onOverflowArtistClick: () => ServiceUtils.pushNamed(
+ context,
+ TrackPage.name,
+ pathParameters: {
+ "id": track.id!,
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(width: 8),
+ Text(
+ Duration(milliseconds: track.durationMs ?? 0)
+ .toHumanReadableString(padZero: false),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ TrackOptions(
+ track: track,
+ playlistId: playlistId,
+ userPlaylist: userPlaylist,
+ showMenuCbRef: showOptionCbRef,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart
similarity index 76%
rename from lib/components/shared/tracks_view/sections/body/track_view_body.dart
rename to lib/components/tracks_view/sections/body/track_view_body.dart
index f576ba0a1..df841b8d4 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart
+++ b/lib/components/tracks_view/sections/body/track_view_body.dart
@@ -8,16 +8,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
-import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
-import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
-import 'package:spotube/components/shared/track_tile/track_tile.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/dialogs/select_device_dialog.dart';
+import 'package:spotube/components/expandable_search/expandable_search.dart';
+import 'package:spotube/components/track_tile/track_tile.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_body_headers.dart';
+import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/history/history.dart';
+import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@@ -26,8 +27,9 @@ class TrackViewBodySection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final playlist = ref.watch(proxyPlaylistProvider);
- final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
+ final playlist = ref.watch(audioPlayerProvider);
+ final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
+ final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final props = InheritedTrackView.of(context);
final trackViewState = ref.watch(trackViewProvider(props.tracks));
@@ -146,11 +148,17 @@ class TrackViewBodySection extends HookConsumerWidget {
} else {
final tracks = await props.pagination.onFetchAll();
await remotePlayback.load(
- WebSocketLoadEventData(
- tracks: tracks,
- collectionId: props.collectionId,
- initialIndex: index,
- ),
+ props.collection is AlbumSimple
+ ? WebSocketLoadEventData.album(
+ tracks: tracks,
+ collection: props.collection as AlbumSimple,
+ initialIndex: index,
+ )
+ : WebSocketLoadEventData.playlist(
+ tracks: tracks,
+ collection: props.collection as PlaylistSimple,
+ initialIndex: index,
+ ),
);
}
} else {
@@ -164,6 +172,13 @@ class TrackViewBodySection extends HookConsumerWidget {
autoPlay: true,
);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier
+ .addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier
+ .addPlaylists([props.collection as PlaylistSimple]);
+ }
}
}
},
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/tracks_view/sections/body/track_view_body_headers.dart
similarity index 87%
rename from lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
rename to lib/components/tracks_view/sections/body/track_view_body_headers.dart
index 3a1538a39..564c85d0e 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
+++ b/lib/components/tracks_view/sections/body/track_view_body_headers.dart
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
-import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_options.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/expandable_search/expandable_search.dart';
+import 'package:spotube/components/sort_tracks_dropdown.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_options.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart
similarity index 75%
rename from lib/components/shared/tracks_view/sections/body/track_view_options.dart
rename to lib/components/tracks_view/sections/body/track_view_options.dart
index ff92b6638..23198aec6 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart
+++ b/lib/components/tracks_view/sections/body/track_view_options.dart
@@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
-import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart';
-import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
+import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/download_manager_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/history/history.dart';
+import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class TrackViewBodyOptions extends HookConsumerWidget {
const TrackViewBodyOptions({super.key});
@@ -22,7 +24,8 @@ class TrackViewBodyOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider);
final downloader = ref.watch(downloadManagerProvider.notifier);
- final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
+ final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
+ final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final audioSource =
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
@@ -72,6 +75,12 @@ class TrackViewBodyOptions extends HookConsumerWidget {
{
playlistNotifier.addTracksAtFirst(selectedTracks);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier.addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier
+ .addPlaylists([props.collection as PlaylistSimple]);
+ }
trackViewState.deselectAll();
break;
}
@@ -79,6 +88,12 @@ class TrackViewBodyOptions extends HookConsumerWidget {
{
playlistNotifier.addTracks(selectedTracks);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier.addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier
+ .addPlaylists([props.collection as PlaylistSimple]);
+ }
trackViewState.deselectAll();
break;
}
diff --git a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart b/lib/components/tracks_view/sections/body/use_is_user_playlist.dart
similarity index 100%
rename from lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart
rename to lib/components/tracks_view/sections/body/use_is_user_playlist.dart
diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/tracks_view/sections/header/flexible_header.dart
similarity index 89%
rename from lib/components/shared/tracks_view/sections/header/flexible_header.dart
rename to lib/components/tracks_view/sections/header/flexible_header.dart
index 4a7043023..6845cc3e8 100644
--- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart
+++ b/lib/components/tracks_view/sections/header/flexible_header.dart
@@ -1,17 +1,18 @@
import 'dart:ui';
import 'package:flutter/material.dart';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
+
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/playbutton_card.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/header_actions.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/header_buttons.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/tracks_view/sections/header/header_actions.dart';
+import 'package:spotube/components/tracks_view/sections/header/header_buttons.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:gap/gap.dart';
import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
+import 'package:spotube/utils/platform.dart';
class TrackViewFlexHeader extends HookConsumerWidget {
const TrackViewFlexHeader({super.key});
@@ -23,8 +24,6 @@ class TrackViewFlexHeader extends HookConsumerWidget {
final defaultTextStyle = DefaultTextStyle.of(context);
final mediaQuery = MediaQuery.of(context);
- final description = useDescription(props.description);
-
final palette = usePaletteColor(props.image, ref);
return IconTheme(
@@ -53,7 +52,7 @@ class TrackViewFlexHeader extends HookConsumerWidget {
floating: false,
pinned: true,
expandedHeight: 450,
- automaticallyImplyLeading: DesktopTools.platform.isMobile,
+ automaticallyImplyLeading: kIsMobile,
backgroundColor: palette.color,
title: isExpanded ? null : Text(props.title, style: headingStyle),
flexibleSpace: FlexibleSpaceBar(
@@ -126,10 +125,10 @@ class TrackViewFlexHeader extends HookConsumerWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
- if (description != null &&
- description.isNotEmpty)
+ if (props.description != null &&
+ props.description!.isNotEmpty)
Text(
- description,
+ props.description!.unescapeHtml(),
style:
defaultTextStyle.style.copyWith(
color: palette.bodyTextColor,
diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart
similarity index 69%
rename from lib/components/shared/tracks_view/sections/header/header_actions.dart
rename to lib/components/tracks_view/sections/header/header_actions.dart
index f6880485d..8e378f97c 100644
--- a/lib/components/shared/tracks_view/sections/header/header_actions.dart
+++ b/lib/components/tracks_view/sections/header/header_actions.dart
@@ -2,14 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/playlist/playlist_create_dialog.dart';
-import 'package:spotube/components/shared/heart_button.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
+import 'package:spotube/components/heart_button/heart_button.dart';
+import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/authentication/authentication.dart';
+import 'package:spotube/provider/history/history.dart';
+import 'package:spotube/provider/audio_player/audio_player.dart';
class TrackViewHeaderActions extends HookConsumerWidget {
const TrackViewHeaderActions({super.key});
@@ -18,8 +20,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context);
- final playlist = ref.watch(proxyPlaylistProvider);
- final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
+ final playlist = ref.watch(audioPlayerProvider);
+ final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
+ final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final isActive = playlist.collections.contains(props.collectionId);
@@ -29,6 +32,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
+ final copiedText =
+ context.l10n.copied_shareurl_to_clipboard(props.shareUrl);
+
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -45,7 +51,7 @@ class TrackViewHeaderActions extends HookConsumerWidget {
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
- "Copied ${props.shareUrl} to clipboard",
+ copiedText,
textAlign: TextAlign.center,
),
),
@@ -61,9 +67,16 @@ class TrackViewHeaderActions extends HookConsumerWidget {
final tracks = await props.pagination.onFetchAll();
await playlistNotifier.addTracks(tracks);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier
+ .addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier
+ .addPlaylists([props.collection as PlaylistSimple]);
+ }
},
),
- if (props.onHeart != null && auth != null)
+ if (props.onHeart != null && auth.asData?.value != null)
HeartButton(
isLiked: props.isLiked,
icon: isUserPlaylist ? SpotubeIcons.trash : null,
diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart
similarity index 62%
rename from lib/components/shared/tracks_view/sections/header/header_buttons.dart
rename to lib/components/tracks_view/sections/header/header_buttons.dart
index 50eeb7470..54e0f0cf7 100644
--- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart
+++ b/lib/components/tracks_view/sections/header/header_buttons.dart
@@ -5,13 +5,15 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
+import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/dialogs/select_device_dialog.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/provider/history/history.dart';
+import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
class TrackViewHeaderButtons extends HookConsumerWidget {
@@ -26,8 +28,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final props = InheritedTrackView.of(context);
- final playlist = ref.watch(proxyPlaylistProvider);
- final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
+ final playlist = ref.watch(audioPlayerProvider);
+ final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
+ final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final isActive = playlist.collections.contains(props.collectionId);
@@ -44,28 +47,45 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
try {
isLoading.value = true;
- final allTracks = await props.pagination.onFetchAll();
-
+ final initialTracks = props.tracks;
if (!context.mounted) return;
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
if (isRemoteDevice) {
+ final allTracks = await props.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load(
- WebSocketLoadEventData(
- tracks: allTracks,
- collectionId: props.collectionId,
- initialIndex: Random().nextInt(allTracks.length)),
+ props.collection is AlbumSimple
+ ? WebSocketLoadEventData.album(
+ tracks: allTracks,
+ collection: props.collection as AlbumSimple,
+ initialIndex: Random().nextInt(allTracks.length))
+ : WebSocketLoadEventData.playlist(
+ tracks: allTracks,
+ collection: props.collection as PlaylistSimple,
+ initialIndex: Random().nextInt(allTracks.length),
+ ),
);
await remotePlayback.setShuffle(true);
} else {
await playlistNotifier.load(
- allTracks,
+ initialTracks,
autoPlay: true,
- initialIndex: Random().nextInt(allTracks.length),
+ initialIndex: Random().nextInt(initialTracks.length),
);
await audioPlayer.setShuffle(true);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier.addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
+ }
+
+ final allTracks = await props.pagination.onFetchAll();
+
+ await playlistNotifier.addTracks(
+ allTracks.sublist(initialTracks.length),
+ );
}
} finally {
isLoading.value = false;
@@ -76,25 +96,44 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
try {
isLoading.value = true;
- final allTracks = await props.pagination.onFetchAll();
+ final initialTracks = props.tracks;
if (!context.mounted) return;
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
if (isRemoteDevice) {
+ final allTracks = await props.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load(
- WebSocketLoadEventData(
- tracks: allTracks,
- collectionId: props.collectionId,
- ),
+ props.collection is AlbumSimple
+ ? WebSocketLoadEventData.album(
+ tracks: allTracks,
+ collection: props.collection as AlbumSimple,
+ )
+ : WebSocketLoadEventData.playlist(
+ tracks: allTracks,
+ collection: props.collection as PlaylistSimple,
+ ),
);
} else {
- await playlistNotifier.load(allTracks, autoPlay: true);
+ await playlistNotifier.load(initialTracks, autoPlay: true);
playlistNotifier.addCollection(props.collectionId);
+ if (props.collection is AlbumSimple) {
+ historyNotifier.addAlbums([props.collection as AlbumSimple]);
+ } else {
+ historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
+ }
+
+ final allTracks = await props.pagination.onFetchAll();
+
+ await playlistNotifier.addTracks(
+ allTracks.sublist(initialTracks.length),
+ );
}
} finally {
- isLoading.value = false;
+ if (context.mounted) {
+ isLoading.value = false;
+ }
}
}
diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/tracks_view/track_view.dart
similarity index 71%
rename from lib/components/shared/tracks_view/track_view.dart
rename to lib/components/tracks_view/track_view.dart
index eb8f68712..2a3f5237e 100644
--- a/lib/components/shared/tracks_view/track_view.dart
+++ b/lib/components/tracks_view/track_view.dart
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
+
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
-import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
-import 'package:spotube/components/shared/page_window_title_bar.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
+import 'package:spotube/components/titlebar/titlebar.dart';
+import 'package:spotube/components/tracks_view/sections/header/flexible_header.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_body.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/utils/platform.dart';
class TrackView extends HookConsumerWidget {
const TrackView({super.key});
@@ -18,7 +19,7 @@ class TrackView extends HookConsumerWidget {
final controller = useScrollController();
return Scaffold(
- appBar: DesktopTools.platform.isDesktop
+ appBar: kIsDesktop
? const PageWindowTitleBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/tracks_view/track_view_props.dart
similarity index 88%
rename from lib/components/shared/tracks_view/track_view_props.dart
rename to lib/components/tracks_view/track_view_props.dart
index a1a07f84d..b0a00ae29 100644
--- a/lib/components/shared/tracks_view/track_view_props.dart
+++ b/lib/components/tracks_view/track_view_props.dart
@@ -39,7 +39,7 @@ class PaginationProps {
}
class InheritedTrackView extends InheritedWidget {
- final String collectionId;
+ final Object collection;
final String title;
final String? description;
final String image;
@@ -55,7 +55,7 @@ class InheritedTrackView extends InheritedWidget {
const InheritedTrackView({
super.key,
required super.child,
- required this.collectionId,
+ required this.collection,
required this.title,
this.description,
required this.image,
@@ -65,7 +65,11 @@ class InheritedTrackView extends InheritedWidget {
required this.shareUrl,
this.isLiked = false,
this.onHeart,
- });
+ }) : assert(collection is AlbumSimple || collection is PlaylistSimple);
+
+ String get collectionId => collection is AlbumSimple
+ ? (collection as AlbumSimple).id!
+ : (collection as PlaylistSimple).id!;
@override
bool updateShouldNotify(InheritedTrackView oldWidget) {
@@ -78,7 +82,7 @@ class InheritedTrackView extends InheritedWidget {
oldWidget.onHeart != onHeart ||
oldWidget.shareUrl != shareUrl ||
oldWidget.routePath != routePath ||
- oldWidget.collectionId != collectionId ||
+ oldWidget.collection != collection ||
oldWidget.child != child;
}
diff --git a/lib/components/shared/tracks_view/track_view_provider.dart b/lib/components/tracks_view/track_view_provider.dart
similarity index 95%
rename from lib/components/shared/tracks_view/track_view_provider.dart
rename to lib/components/tracks_view/track_view_provider.dart
index 14dc11369..16aa6d9c5 100644
--- a/lib/components/shared/tracks_view/track_view_provider.dart
+++ b/lib/components/tracks_view/track_view_provider.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/library/user_local_tracks.dart';
+import 'package:spotube/modules/library/user_local_tracks.dart';
class TrackViewNotifier extends ChangeNotifier {
List