mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
parent
ed48d25add
commit
a9c78b7863
@ -1,6 +0,0 @@
|
|||||||
build
|
|
||||||
dist
|
|
||||||
.dart_tool
|
|
||||||
.idea
|
|
||||||
.github
|
|
||||||
.git
|
|
@ -9,6 +9,3 @@ ENABLE_UPDATE_CHECK=
|
|||||||
|
|
||||||
LASTFM_API_KEY=
|
LASTFM_API_KEY=
|
||||||
LASTFM_API_SECRET=
|
LASTFM_API_SECRET=
|
||||||
|
|
||||||
# Release channel. Can be: nightly, stable
|
|
||||||
RELEASE_CHANNEL=
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.22.1",
|
"flutterSdkVersion": "3.19.1",
|
||||||
"flavors": {}
|
"flavors": {}
|
||||||
}
|
}
|
23
.github/Dockerfile
vendored
23
.github/Dockerfile
vendored
@ -1,23 +0,0 @@
|
|||||||
ARG FLUTTER_VERSION
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 krtirtho/flutter_distributor_arm64:${FLUTTER_VERSION}
|
|
||||||
|
|
||||||
ARG BUILD_VERSION
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN chown -R $(whoami) /app
|
|
||||||
|
|
||||||
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" ]
|
|
23
.github/Dockerfile.flutter_distributor
vendored
23
.github/Dockerfile.flutter_distributor
vendored
@ -1,23 +0,0 @@
|
|||||||
FROM --platform=linux/arm64 ubuntu:22.04
|
|
||||||
|
|
||||||
ARG FLUTTER_VERSION
|
|
||||||
|
|
||||||
RUN apt-get clean &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y bash curl file git unzip xz-utils zip libglu1-mesa cmake tar clang 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 rpm && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /home/flutter
|
|
||||||
|
|
||||||
RUN git clone https://github.com/flutter/flutter.git -b ${FLUTTER_VERSION} --single-branch flutter-sdk
|
|
||||||
|
|
||||||
RUN flutter-sdk/bin/flutter precache
|
|
||||||
|
|
||||||
RUN flutter-sdk/bin/flutter config --no-analytics
|
|
||||||
|
|
||||||
ENV PATH="$PATH:/home/flutter/flutter-sdk/bin"
|
|
||||||
ENV PATH="$PATH:/home/flutter/flutter-sdk/bin/cache/dart-sdk/bin"
|
|
||||||
ENV PATH="$PATH:/home/flutter/.pub-cache/bin"
|
|
||||||
ENV PUB_CACHE="/home/flutter/.pub-cache"
|
|
||||||
|
|
||||||
RUN dart pub global activate flutter_distributor
|
|
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: '3.22.1'
|
FLUTTER_VERSION: '3.19.5'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
2
.github/workflows/spotube-publish-binary.yml
vendored
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version to publish (x.x.x)
|
description: Version to publish (x.x.x)
|
||||||
default: 3.7.0
|
default: 3.1.0
|
||||||
required: true
|
required: true
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
description: Dry run
|
||||||
|
456
.github/workflows/spotube-release-binary.yml
vendored
456
.github/workflows/spotube-release-binary.yml
vendored
@ -2,109 +2,100 @@ name: Spotube Release Binary
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Version to release (x.x.x)
|
||||||
|
default: 3.6.0
|
||||||
|
required: true
|
||||||
channel:
|
channel:
|
||||||
type: choice
|
type: choice
|
||||||
|
description: Release Channel
|
||||||
|
required: true
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- nightly
|
- nightly
|
||||||
default: nightly
|
default: nightly
|
||||||
description: The release channel
|
|
||||||
debug:
|
debug:
|
||||||
|
description: Debug on failed when channel is nightly
|
||||||
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
description: Debug with SSH toggle
|
|
||||||
required: false
|
|
||||||
dry_run:
|
dry_run:
|
||||||
|
description: Dry run
|
||||||
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: Dry run without uploading to release
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.22.1
|
FLUTTER_VERSION: '3.19.1'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_platform:
|
windows:
|
||||||
strategy:
|
runs-on: windows-latest
|
||||||
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: subosito/flutter-action@v2.12.0
|
- uses: subosito/flutter-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- 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: Install ${{matrix.platform}} dependencies
|
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
||||||
|
if: ${{ inputs.channel == 'nightly' }}
|
||||||
run: |
|
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
|
flutter pub get
|
||||||
dart cli/cli.dart install-dependencies --platform=${{matrix.platform}}
|
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||||
|
|
||||||
- name: Sign Apk
|
- name: Build Windows Executable
|
||||||
if: ${{matrix.platform == 'android'}}
|
|
||||||
run: |
|
run: |
|
||||||
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
|
dart pub global activate flutter_distributor
|
||||||
echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
|
make innoinstall
|
||||||
|
flutter_distributor package --platform=windows --targets=exe --skip-clean
|
||||||
- name: Build ${{matrix.platform}} binaries
|
mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
|
||||||
run: dart cli/cli.dart build ${{matrix.platform}}
|
|
||||||
env:
|
- name: Create Chocolatey Package and set hash
|
||||||
CHANNEL: ${{inputs.channel}}
|
if: ${{ inputs.channel == 'stable' }}
|
||||||
DOTENV: ${{secrets.DOTENV_RELEASE}}
|
run: |
|
||||||
|
Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash
|
||||||
- uses: actions/upload-artifact@v3
|
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:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: Spotube-Release-Binaries
|
name: Spotube-Release-Binaries
|
||||||
path: ${{matrix.files}}
|
path: |
|
||||||
|
dist/Spotube-windows-x86_64.nupkg
|
||||||
|
dist/Spotube-windows-x86_64-setup.exe
|
||||||
|
|
||||||
- name: Debug With SSH When fails
|
- name: Debug With SSH When fails
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||||
@ -112,10 +103,314 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
limit-access-to-actor: true
|
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
|
||||||
|
|
||||||
|
- 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}}%|<release version="${{ env.BUILD_VERSION }}" date="${{ steps.date.outputs.date }}" />|' 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
|
||||||
|
|
||||||
|
|
||||||
|
- 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
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: subosito/flutter-action@v2.12.0
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||||
|
|
||||||
|
- name: Sign Apk
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Debug With SSH When fails
|
||||||
|
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||||
|
uses: mxschmitt/action-tmate@v3
|
||||||
|
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:
|
upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
needs:
|
needs:
|
||||||
- build_platform
|
- windows
|
||||||
|
- linux
|
||||||
|
- android
|
||||||
|
- macos
|
||||||
|
- iOS
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@ -131,10 +426,6 @@ jobs:
|
|||||||
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
|
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
|
||||||
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
||||||
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
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
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@ -149,7 +440,7 @@ jobs:
|
|||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: v${{ env.PUBSPEC_VERSION }} # mind the "v" prefix
|
tag: v${{ inputs.version }} # mind the "v" prefix
|
||||||
omitBodyDuringUpdate: true
|
omitBodyDuringUpdate: true
|
||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
@ -167,8 +458,3 @@ jobs:
|
|||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
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
|
|
||||||
|
16
.metadata
16
.metadata
@ -1,11 +1,11 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "300451adae589accbece3490f4396f10bdf15e6e"
|
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||||
channel: "stable"
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||||
- platform: windows
|
- platform: macos
|
||||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -24,6 +24,5 @@
|
|||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
|
"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",
|
"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",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
34
CHANGELOG.md
34
CHANGELOG.md
@ -2,39 +2,7 @@
|
|||||||
|
|
||||||
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.
|
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.7.0](https://github.com/krtirtho/spotube/compare/v3.6.0...v3.7.0) (2024-06-03)
|
## [3.6.0-0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0-0) (2024-04-15)
|
||||||
|
|
||||||
|
|
||||||
### 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
|
### Features
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
plugins {
|
|
||||||
id "com.android.application"
|
|
||||||
id "kotlin-android"
|
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
@ -12,6 +6,11 @@ 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')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
@ -22,6 +21,10 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
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 keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
@ -68,9 +71,6 @@ android {
|
|||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
debug {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "default"
|
flavorDimensions "default"
|
||||||
@ -81,19 +81,16 @@ android {
|
|||||||
resValue "string", "app_name_en", "Spotube Nightly"
|
resValue "string", "app_name_en", "Spotube Nightly"
|
||||||
applicationIdSuffix ".nightly"
|
applicationIdSuffix ".nightly"
|
||||||
versionNameSuffix "-nightly"
|
versionNameSuffix "-nightly"
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
}
|
||||||
dev {
|
dev {
|
||||||
dimension "default"
|
dimension "default"
|
||||||
resValue "string", "app_name_en", "Spotube Dev"
|
resValue "string", "app_name_en", "Spotube Dev"
|
||||||
applicationIdSuffix ".dev"
|
applicationIdSuffix ".dev"
|
||||||
versionNameSuffix "-dev"
|
versionNameSuffix "-dev"
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
}
|
||||||
stable {
|
stable {
|
||||||
dimension "default"
|
dimension "default"
|
||||||
resValue "string", "app_name_en", "Spotube"
|
resValue "string", "app_name_en", "Spotube"
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +101,15 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
|
|
||||||
// other deps so just ignore
|
// other deps so just ignore
|
||||||
|
@ -24,11 +24,6 @@
|
|||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
>
|
||||||
<!-- Enable Impeller -->
|
|
||||||
<!-- <meta-data
|
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
|
||||||
android:value="true" /> -->
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.8.22'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
pluginManagement {
|
include ':app'
|
||||||
def flutterSdkPath = {
|
|
||||||
def properties = new Properties()
|
|
||||||
file("local.properties").withInputStream { properties.load(it) }
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
return flutterSdkPath
|
|
||||||
}()
|
|
||||||
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||||
|
def properties = new Properties()
|
||||||
|
|
||||||
repositories {
|
assert localPropertiesFile.exists()
|
||||||
google()
|
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
id "com.android.application" version "7.2.1" apply false
|
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include ":app"
|
|
||||||
|
103
bin/gen-credits.dart
Normal file
103
bin/gen-credits.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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<String, Dependency>,
|
||||||
|
MapEntry<String, GitDependency>>(
|
||||||
|
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);
|
||||||
|
}
|
28
bin/translated_messages.dart
Normal file
28
bin/translated_messages.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
final translatedFile =
|
||||||
|
jsonDecode(await File('tm.json').readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
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<String, dynamic>;
|
||||||
|
|
||||||
|
final newContent = {
|
||||||
|
...fileContent,
|
||||||
|
...value,
|
||||||
|
};
|
||||||
|
|
||||||
|
await file.writeAsString(
|
||||||
|
const JsonEncoder.withIndent(' ').convert(newContent),
|
||||||
|
);
|
||||||
|
|
||||||
|
print('✅ Updated locale: $key');
|
||||||
|
}
|
||||||
|
}
|
50
bin/untranslated_messages.dart
Normal file
50
bin/untranslated_messages.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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<String> args) {
|
||||||
|
final file = jsonDecode(
|
||||||
|
File('untranslated_messages.json').readAsStringSync(),
|
||||||
|
) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final englishMessages =
|
||||||
|
jsonDecode(File('lib/l10n/app_en.arb').readAsStringSync())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final messagesWithValues = <String, dynamic>{};
|
||||||
|
|
||||||
|
for (final MapEntry(key: locale, value: messages) in file.entries) {
|
||||||
|
messagesWithValues[locale] = Map.fromEntries(
|
||||||
|
messages
|
||||||
|
.map(
|
||||||
|
(message) =>
|
||||||
|
MapEntry<String, dynamic>(message, englishMessages[message]),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
.cast<MapEntry<String, dynamic>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
22
bin/verify-pkgbuild.dart
Normal file
22
bin/verify-pkgbuild.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -2,9 +2,4 @@ targets:
|
|||||||
$default:
|
$default:
|
||||||
sources:
|
sources:
|
||||||
exclude:
|
exclude:
|
||||||
- bin/*.dart
|
- bin/*.dart
|
||||||
builders:
|
|
||||||
json_serializable:
|
|
||||||
options:
|
|
||||||
any_map: true
|
|
||||||
explicit_to_json: true
|
|
@ -1,4 +0,0 @@
|
|||||||
## Spotube Configuration CLI
|
|
||||||
|
|
||||||
This is used for building the project for multiple platforms and having utilities specific for the project.
|
|
||||||
Written in Dart
|
|
22
cli/cli.dart
22
cli/cli.dart
@ -1,22 +0,0 @@
|
|||||||
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<String> 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);
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
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<void> 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
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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")}
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
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,
|
|
||||||
'<release'
|
|
||||||
' version="$versionWithoutBuildNumber"'
|
|
||||||
' date="${DateFormat("yyyy-MM-dd").format(DateTime.now())}"'
|
|
||||||
'/>',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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/
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
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<void> 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();
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
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<String, Dependency>,
|
|
||||||
MapEntry<String, GitDependency>>(
|
|
||||||
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'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
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
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
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":
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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<String, dynamic>;
|
|
||||||
|
|
||||||
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<String, dynamic>;
|
|
||||||
|
|
||||||
final newContent = {...fileContent, ...value};
|
|
||||||
|
|
||||||
await file.writeAsString(
|
|
||||||
const JsonEncoder.withIndent(' ').convert(newContent),
|
|
||||||
);
|
|
||||||
|
|
||||||
stdout.writeln('✅ Updated locale: $key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
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<String, dynamic>;
|
|
||||||
|
|
||||||
final englishMessages = jsonDecode(
|
|
||||||
File(join(cwd.path, 'lib', 'l10n', 'app_en.arb')).readAsStringSync(),
|
|
||||||
) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
final messagesWithValues = <String, dynamic>{};
|
|
||||||
|
|
||||||
for (final MapEntry(key: locale, value: messages) in file.entries) {
|
|
||||||
messagesWithValues[locale] = Map.fromEntries(
|
|
||||||
messages
|
|
||||||
.map(
|
|
||||||
(message) =>
|
|
||||||
MapEntry<String, dynamic>(message, englishMessages[message]),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
.cast<MapEntry<String, dynamic>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
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"]!;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
extensions:
|
|
@ -69,6 +69,9 @@ PODS:
|
|||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Toast
|
- Toast
|
||||||
|
- FMDB (2.7.5):
|
||||||
|
- FMDB/standard (= 2.7.5)
|
||||||
|
- FMDB/standard (2.7.5)
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
@ -84,7 +87,7 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.1.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.18.8):
|
- SDWebImage (5.18.8):
|
||||||
- SDWebImage/Core (= 5.18.8)
|
- SDWebImage/Core (= 5.18.8)
|
||||||
@ -94,7 +97,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FMDB (>= 2.7.5)
|
||||||
- SwiftyGif (5.4.4)
|
- SwiftyGif (5.4.4)
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
@ -126,13 +129,14 @@ DEPENDENCIES:
|
|||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
|
- FMDB
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@ -190,44 +194,45 @@ EXTERNAL SOURCES:
|
|||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/darwin"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||||
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
|
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||||
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||||
file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d
|
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||||
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
||||||
fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db
|
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||||
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||||
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||||
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
||||||
metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7
|
metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||||
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
|
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
|
||||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
|
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
||||||
|
|
||||||
|
@ -324,7 +324,6 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */,
|
6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */,
|
||||||
46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -347,7 +346,6 @@
|
|||||||
B536BD992B405DB1009B3CE4 /* Embed Frameworks */,
|
B536BD992B405DB1009B3CE4 /* Embed Frameworks */,
|
||||||
B536BD9A2B405DB1009B3CE4 /* Thin Binary */,
|
B536BD9A2B405DB1009B3CE4 /* Thin Binary */,
|
||||||
A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */,
|
A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */,
|
||||||
2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -370,7 +368,6 @@
|
|||||||
B536BDB62B405FDE009B3CE4 /* Embed Frameworks */,
|
B536BDB62B405FDE009B3CE4 /* Embed Frameworks */,
|
||||||
B536BDB72B405FDE009B3CE4 /* Thin Binary */,
|
B536BDB72B405FDE009B3CE4 /* Thin Binary */,
|
||||||
244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */,
|
244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */,
|
||||||
4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -393,7 +390,6 @@
|
|||||||
B536BDD82B4060B3009B3CE4 /* Embed Frameworks */,
|
B536BDD82B4060B3009B3CE4 /* Embed Frameworks */,
|
||||||
B536BDD92B4060B3009B3CE4 /* Thin Binary */,
|
B536BDD92B4060B3009B3CE4 /* Thin Binary */,
|
||||||
D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */,
|
D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */,
|
||||||
5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -527,23 +523,6 @@
|
|||||||
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";
|
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;
|
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 */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@ -560,57 +539,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
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 */ = {
|
5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import 'package:envied/envied.dart';
|
import 'package:envied/envied.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
|
|
||||||
part 'env.g.dart';
|
part 'env.g.dart';
|
||||||
|
|
||||||
enum ReleaseChannel {
|
|
||||||
nightly,
|
|
||||||
stable,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
||||||
abstract class Env {
|
abstract class Env {
|
||||||
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
||||||
@ -30,15 +25,8 @@ abstract class Env {
|
|||||||
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
|
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
|
||||||
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
|
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 =>
|
static bool get enableUpdateChecker =>
|
||||||
kIsFlatpak || _enableUpdateChecker == "1";
|
DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1";
|
||||||
|
|
||||||
static String discordAppId = "1176718791388975124";
|
static String discordAppId = "1176718791388975124";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/spotify/home_feed.dart';
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
final compactNumberFormatter = NumberFormat.compact();
|
|
||||||
final usdFormatter = NumberFormat.compactCurrency(
|
|
||||||
locale: 'en-US',
|
|
||||||
symbol: r"$",
|
|
||||||
decimalDigits: 2,
|
|
||||||
);
|
|
@ -1,10 +1,9 @@
|
|||||||
import 'dart:io';
|
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';
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
Future<void> registerWindowsScheme(String scheme) async {
|
Future<void> registerWindowsScheme(String scheme) async {
|
||||||
if (!kIsWindows) return;
|
if (!DesktopTools.platform.isWindows) return;
|
||||||
String appPath = Platform.resolvedExecutable;
|
String appPath = Platform.resolvedExecutable;
|
||||||
|
|
||||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||||
|
@ -7,10 +7,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/components/player/player_controls.dart';
|
import 'package:spotube/components/player/player_controls.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.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/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -71,16 +67,16 @@ class HomeTabAction extends Action<HomeTabIntent> {
|
|||||||
final router = intent.ref.read(routerProvider);
|
final router = intent.ref.read(routerProvider);
|
||||||
switch (intent.tab) {
|
switch (intent.tab) {
|
||||||
case HomeTabs.browse:
|
case HomeTabs.browse:
|
||||||
router.goNamed(HomePage.name);
|
router.go("/");
|
||||||
break;
|
break;
|
||||||
case HomeTabs.search:
|
case HomeTabs.search:
|
||||||
router.goNamed(SearchPage.name);
|
router.go("/search");
|
||||||
break;
|
break;
|
||||||
case HomeTabs.library:
|
case HomeTabs.library:
|
||||||
router.goNamed(LibraryPage.name);
|
router.go("/library");
|
||||||
break;
|
break;
|
||||||
case HomeTabs.lyrics:
|
case HomeTabs.lyrics:
|
||||||
router.goNamed(LyricsPage.name);
|
router.go("/lyrics");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -81,10 +81,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Bashkir",
|
// name: "Bashkir",
|
||||||
// nativeName: "башҡорт теле",
|
// nativeName: "башҡорт теле",
|
||||||
// ),
|
// ),
|
||||||
"eu": const ISOLanguageName(
|
// "eu": const ISOLanguageName(
|
||||||
name: "Basque",
|
// name: "Basque",
|
||||||
nativeName: "euskara",
|
// nativeName: "euskara,",
|
||||||
),
|
// ),
|
||||||
// "be": const ISOLanguageName(
|
// "be": const ISOLanguageName(
|
||||||
// name: "Belarusian",
|
// name: "Belarusian",
|
||||||
// nativeName: "Беларуская",
|
// nativeName: "Беларуская",
|
||||||
@ -197,10 +197,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Fijian",
|
// name: "Fijian",
|
||||||
// nativeName: "vosa Vakaviti",
|
// nativeName: "vosa Vakaviti",
|
||||||
// ),
|
// ),
|
||||||
"fi": const ISOLanguageName(
|
// "fi": const ISOLanguageName(
|
||||||
name: "Finnish",
|
// name: "Finnish",
|
||||||
nativeName: "suomi",
|
// nativeName: "suomi",
|
||||||
),
|
// ),
|
||||||
"fr": const ISOLanguageName(
|
"fr": const ISOLanguageName(
|
||||||
name: "French",
|
name: "French",
|
||||||
nativeName: "français",
|
nativeName: "français",
|
||||||
@ -213,10 +213,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Galician",
|
// name: "Galician",
|
||||||
// nativeName: "Galego",
|
// nativeName: "Galego",
|
||||||
// ),
|
// ),
|
||||||
"ka": const ISOLanguageName(
|
// "ka": const ISOLanguageName(
|
||||||
name: "Georgian",
|
// name: "Georgian",
|
||||||
nativeName: "ქართული",
|
// nativeName: "ქართული",
|
||||||
),
|
// ),
|
||||||
"de": const ISOLanguageName(
|
"de": const ISOLanguageName(
|
||||||
name: "German",
|
name: "German",
|
||||||
nativeName: "Deutsch",
|
nativeName: "Deutsch",
|
||||||
@ -265,10 +265,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Interlingua",
|
// name: "Interlingua",
|
||||||
// nativeName: "Interlingua",
|
// nativeName: "Interlingua",
|
||||||
// ),
|
// ),
|
||||||
"id": const ISOLanguageName(
|
// "id": const ISOLanguageName(
|
||||||
name: "Indonesian",
|
// name: "Indonesian",
|
||||||
nativeName: "Bahasa Indonesia",
|
// nativeName: "Bahasa Indonesia",
|
||||||
),
|
// ),
|
||||||
// "ie": const ISOLanguageName(
|
// "ie": const ISOLanguageName(
|
||||||
// name: "Interlingue",
|
// name: "Interlingue",
|
||||||
// nativeName: "Occidental",
|
// nativeName: "Occidental",
|
||||||
|
@ -14,7 +14,6 @@ import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
|||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/pages/lastfm_login/lastfm_login.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.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
||||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
||||||
@ -25,13 +24,6 @@ import 'package:spotube/pages/search/search.dart';
|
|||||||
import 'package:spotube/pages/settings/blacklist.dart';
|
import 'package:spotube/pages/settings/blacklist.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
import 'package:spotube/pages/settings/about.dart';
|
||||||
import 'package:spotube/pages/settings/logs.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/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
@ -58,7 +50,6 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/",
|
path: "/",
|
||||||
name: HomePage.name,
|
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
final authNotifier = ref.read(authenticationProvider.notifier);
|
final authNotifier = ref.read(authenticationProvider.notifier);
|
||||||
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
||||||
@ -75,13 +66,11 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genres",
|
path: "genres",
|
||||||
name: GenrePage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: GenrePage()),
|
const SpotubePage(child: GenrePage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genre/:categoryId",
|
path: "genre/:categoryId",
|
||||||
name: GenrePlaylistsPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: GenrePlaylistsPage(
|
child: GenrePlaylistsPage(
|
||||||
category: state.extra as Category,
|
category: state.extra as Category,
|
||||||
@ -90,7 +79,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "feeds/:feedId",
|
path: "feeds/:feedId",
|
||||||
name: HomeFeedSectionPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: HomeFeedSectionPage(
|
child: HomeFeedSectionPage(
|
||||||
sectionUri: state.pathParameters["feedId"] as String,
|
sectionUri: state.pathParameters["feedId"] as String,
|
||||||
@ -101,62 +89,45 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/search",
|
path: "/search",
|
||||||
name: SearchPage.name,
|
name: "Search",
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: SearchPage()),
|
const SpotubePage(child: SearchPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/library",
|
path: "/library",
|
||||||
name: LibraryPage.name,
|
name: "Library",
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LibraryPage()),
|
const SpotubePage(child: LibraryPage()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "generate",
|
path: "generate",
|
||||||
name: PlaylistGeneratorPage.name,
|
pageBuilder: (context, state) =>
|
||||||
pageBuilder: (context, state) =>
|
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
routes: [
|
||||||
routes: [
|
GoRoute(
|
||||||
GoRoute(
|
path: "result",
|
||||||
path: "result",
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
name: PlaylistGenerateResultPage.name,
|
child: PlaylistGenerateResultPage(
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
state: state.extra as GeneratePlaylistProviderInput,
|
||||||
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(
|
GoRoute(
|
||||||
path: "/lyrics",
|
path: "/lyrics",
|
||||||
name: LyricsPage.name,
|
name: "Lyrics",
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LyricsPage()),
|
const SpotubePage(child: LyricsPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
name: SettingsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: SettingsPage(),
|
child: SettingsPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "blacklist",
|
path: "blacklist",
|
||||||
name: BlackListPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const BlackListPage(),
|
child: const BlackListPage(),
|
||||||
),
|
),
|
||||||
@ -164,14 +135,12 @@ final routerProvider = Provider((ref) {
|
|||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "logs",
|
path: "logs",
|
||||||
name: LogsPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const LogsPage(),
|
child: const LogsPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "about",
|
path: "about",
|
||||||
name: AboutSpotube.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const AboutSpotube(),
|
child: const AboutSpotube(),
|
||||||
),
|
),
|
||||||
@ -180,7 +149,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/album/:id",
|
path: "/album/:id",
|
||||||
name: AlbumPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is AlbumSimple);
|
assert(state.extra is AlbumSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -190,7 +158,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/artist/:id",
|
path: "/artist/:id",
|
||||||
name: ArtistPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.pathParameters["id"] != null);
|
assert(state.pathParameters["id"] != null);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -199,7 +166,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/playlist/:id",
|
path: "/playlist/:id",
|
||||||
name: PlaylistPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is PlaylistSimple);
|
assert(state.extra is PlaylistSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -211,7 +177,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/track/:id",
|
path: "/track/:id",
|
||||||
name: TrackPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final id = state.pathParameters["id"]!;
|
final id = state.pathParameters["id"]!;
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -221,14 +186,12 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/connect",
|
path: "/connect",
|
||||||
name: ConnectPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: ConnectPage(),
|
child: ConnectPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "control",
|
path: "control",
|
||||||
name: ConnectControlPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
return const SpotubePage(
|
return const SpotubePage(
|
||||||
child: ConnectControlPage(),
|
child: ConnectControlPage(),
|
||||||
@ -239,66 +202,13 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/profile",
|
path: "/profile",
|
||||||
name: ProfilePage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: ProfilePage()),
|
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(
|
GoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
name: MiniLyricsPage.name,
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||||
@ -306,7 +216,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/getting-started",
|
path: "/getting-started",
|
||||||
name: GettingStarting.name,
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: GettingStarting(),
|
child: GettingStarting(),
|
||||||
@ -314,7 +223,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: WebViewLogin.name,
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
||||||
@ -322,7 +230,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/login-tutorial",
|
path: "/login-tutorial",
|
||||||
name: LoginTutorial.name,
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: LoginTutorial(),
|
child: LoginTutorial(),
|
||||||
@ -330,7 +237,6 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lastfm-login",
|
path: "/lastfm-login",
|
||||||
name: LastFMLoginPage.name,
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
const SpotubePage(child: LastFMLoginPage()),
|
||||||
|
@ -1,82 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.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 {
|
class SideBarTiles {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
SideBarTiles({required this.icon, required this.title, required this.id});
|
||||||
|
|
||||||
SideBarTiles({
|
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
||||||
|
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
||||||
|
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "browse",
|
id: "library", icon: SpotubeIcons.library, title: l10n.library),
|
||||||
name: HomePage.name,
|
SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics),
|
||||||
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<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
||||||
SideBarTiles(
|
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
||||||
id: "browse",
|
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
||||||
name: HomePage.name,
|
|
||||||
icon: SpotubeIcons.home,
|
|
||||||
title: l10n.browse,
|
|
||||||
),
|
|
||||||
SideBarTiles(
|
|
||||||
id: "search",
|
|
||||||
name: SearchPage.name,
|
|
||||||
icon: SpotubeIcons.search,
|
|
||||||
title: l10n.search,
|
|
||||||
),
|
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library",
|
id: "library",
|
||||||
name: LibraryPage.name,
|
|
||||||
icon: SpotubeIcons.library,
|
icon: SpotubeIcons.library,
|
||||||
title: l10n.library,
|
title: l10n.library,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "stats",
|
id: "settings",
|
||||||
name: StatsPage.name,
|
icon: SpotubeIcons.settings,
|
||||||
icon: SpotubeIcons.chart,
|
title: l10n.settings,
|
||||||
title: l10n.stats,
|
)
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
@ -121,7 +121,4 @@ abstract class SpotubeIcons {
|
|||||||
static const monitor = FeatherIcons.monitor;
|
static const monitor = FeatherIcons.monitor;
|
||||||
static const power = FeatherIcons.power;
|
static const power = FeatherIcons.power;
|
||||||
static const bluetooth = FeatherIcons.bluetooth;
|
static const bluetooth = FeatherIcons.bluetooth;
|
||||||
static const chart = FeatherIcons.barChart2;
|
|
||||||
static const folderAdd = FeatherIcons.folderPlus;
|
|
||||||
static const folderRemove = FeatherIcons.folderMinus;
|
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -34,7 +32,6 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() => playlist.containsCollection(album.id!),
|
() => playlist.containsCollection(album.id!),
|
||||||
@ -65,14 +62,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
description:
|
description:
|
||||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.push(context, "/album/${album.id}", extra: album);
|
||||||
context,
|
|
||||||
AlbumPage.name,
|
|
||||||
pathParameters: {
|
|
||||||
"id": album.id!,
|
|
||||||
},
|
|
||||||
extra: album,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onPlaybuttonPressed: () async {
|
onPlaybuttonPressed: () async {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
@ -89,15 +79,14 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.album(
|
WebSocketLoadEventData(
|
||||||
tracks: fetchedTracks,
|
tracks: fetchedTracks,
|
||||||
collection: album,
|
collectionId: album.id!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(album.id!);
|
playlistNotifier.addCollection(album.id!);
|
||||||
historyNotifier.addAlbums([album]);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
@ -115,7 +104,6 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(album.id!);
|
playlistNotifier.addCollection(album.id!);
|
||||||
historyNotifier.addAlbums([album]);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
|
@ -9,7 +9,6 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -35,10 +34,6 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
final radius = BorderRadius.circular(15);
|
final radius = BorderRadius.circular(15);
|
||||||
|
|
||||||
final bgColor = useBrightnessValue(
|
|
||||||
theme.colorScheme.surface,
|
|
||||||
theme.colorScheme.surfaceContainerHigh,
|
|
||||||
);
|
|
||||||
final double size = useBreakpointValue<double>(
|
final double size = useBreakpointValue<double>(
|
||||||
xs: 130,
|
xs: 130,
|
||||||
sm: 130,
|
sm: 130,
|
||||||
@ -50,8 +45,12 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
width: size,
|
width: size,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 5),
|
margin: const EdgeInsets.symmetric(vertical: 5),
|
||||||
child: Material(
|
child: Material(
|
||||||
shadowColor: theme.colorScheme.surface,
|
shadowColor: theme.colorScheme.background,
|
||||||
color: bgColor,
|
color: Color.lerp(
|
||||||
|
theme.colorScheme.surfaceVariant,
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
useBrightnessValue(.9, .7),
|
||||||
|
),
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
@ -64,13 +63,7 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.push(context, "/artist/${artist.id}");
|
||||||
context,
|
|
||||||
ArtistPage.name,
|
|
||||||
pathParameters: {
|
|
||||||
"id": artist.id!,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -3,7 +3,6 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/connect/connect.dart';
|
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ServiceUtils.pushNamed(context, ConnectPage.name);
|
ServiceUtils.push(context, "/connect");
|
||||||
},
|
},
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@ -60,7 +59,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.pushNamed(context, ConnectPage.name);
|
ServiceUtils.push(context, "/connect");
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
@ -112,7 +111,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
foregroundColor: colorScheme.onPrimary,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ServiceUtils.pushNamed(context, ConnectPage.name);
|
ServiceUtils.push(context, "/connect");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -16,6 +16,7 @@ class TokenLoginForm extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
||||||
final directCodeController = useTextEditingController();
|
final directCodeController = useTextEditingController();
|
||||||
|
final mounted = useIsMounted();
|
||||||
|
|
||||||
final isLoading = useState(false);
|
final isLoading = useState(false);
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ class TokenLoginForm extends HookConsumerWidget {
|
|||||||
await AuthenticationCredentials.fromCookie(
|
await AuthenticationCredentials.fromCookie(
|
||||||
cookieHeader),
|
cookieHeader),
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
if (mounted()) {
|
||||||
onDone?.call();
|
onDone?.call();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/pages/home/feed/feed_section.dart';
|
|
||||||
import 'package:spotube/provider/spotify/views/home.dart';
|
import 'package:spotube/provider/spotify/views/home.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -42,13 +41,8 @@ class HomePageFeedSection extends HookConsumerWidget {
|
|||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
label: const Text("Browse More"),
|
label: const Text("Browse More"),
|
||||||
icon: const Icon(SpotubeIcons.angleRight),
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
onPressed: () => ServiceUtils.pushNamed(
|
onPressed: () =>
|
||||||
context,
|
ServiceUtils.push(context, "/feeds/${section.uri}"),
|
||||||
HomeFeedSectionPage.name,
|
|
||||||
pathParameters: {
|
|
||||||
"feedId": section.uri,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/components/home/sections/friends/friend_item.dart';
|
import 'package:spotube/components/home/sections/friends/friend_item.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomePageFriendsSection extends HookConsumerWidget {
|
class HomePageFriendsSection extends HookConsumerWidget {
|
||||||
@ -16,7 +14,6 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(authenticationProvider);
|
|
||||||
final friendsQuery = ref.watch(friendsProvider);
|
final friendsQuery = ref.watch(friendsProvider);
|
||||||
final friends =
|
final friends =
|
||||||
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
|
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
|
||||||
@ -30,36 +27,32 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
|||||||
xxl: 7,
|
xxl: 7,
|
||||||
);
|
);
|
||||||
|
|
||||||
final friendGroup = useMemoized(
|
final friendGroup = friends.fold<List<List<SpotifyFriendActivity>>>(
|
||||||
() => friends.fold<List<List<SpotifyFriendActivity>>>(
|
[],
|
||||||
[],
|
(previousValue, element) {
|
||||||
(previousValue, element) {
|
if (previousValue.isEmpty) {
|
||||||
if (previousValue.isEmpty) {
|
|
||||||
return [
|
|
||||||
[element]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastGroup = previousValue.last;
|
|
||||||
if (lastGroup.length < groupCount) {
|
|
||||||
return [
|
|
||||||
...previousValue.sublist(0, previousValue.length - 1),
|
|
||||||
[...lastGroup, element]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...previousValue,
|
|
||||||
[element]
|
[element]
|
||||||
];
|
];
|
||||||
},
|
}
|
||||||
),
|
|
||||||
[friends, groupCount],
|
final lastGroup = previousValue.last;
|
||||||
|
if (lastGroup.length < groupCount) {
|
||||||
|
return [
|
||||||
|
...previousValue.sublist(0, previousValue.length - 1),
|
||||||
|
[...lastGroup, element]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...previousValue,
|
||||||
|
[element]
|
||||||
|
];
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (friendsQuery.isLoading ||
|
if (friendsQuery.isLoading ||
|
||||||
friendsQuery.asData?.value.friends.isEmpty == true ||
|
friendsQuery.asData?.value.friends.isEmpty == true) {
|
||||||
auth == null) {
|
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
child: SizedBox.shrink(),
|
child: SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
|
@ -6,9 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
|
||||||
import 'package:spotube/pages/track/track.dart';
|
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
|
||||||
class FriendItem extends HookConsumerWidget {
|
class FriendItem extends HookConsumerWidget {
|
||||||
@ -30,7 +27,7 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.surfaceContainer,
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
@ -60,9 +57,7 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
text: friend.track.name,
|
text: friend.track.name,
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
context.pushNamed(TrackPage.name, pathParameters: {
|
context.push("/track/${friend.track.id}");
|
||||||
"id": friend.track.id,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const TextSpan(text: " • "),
|
const TextSpan(text: " • "),
|
||||||
@ -76,12 +71,8 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
text: " ${friend.track.artist.name}",
|
text: " ${friend.track.artist.name}",
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
context.pushNamed(
|
context.push(
|
||||||
ArtistPage.name,
|
"/artist/${friend.track.artist.id}",
|
||||||
pathParameters: {
|
|
||||||
"id": friend.track.artist.id,
|
|
||||||
},
|
|
||||||
extra: friend.track.artist,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -114,11 +105,8 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
final album =
|
final album =
|
||||||
await spotify.albums.get(friend.track.album.id);
|
await spotify.albums.get(friend.track.album.id);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.pushNamed(
|
context.push(
|
||||||
AlbumPage.name,
|
"/album/${friend.track.album.id}",
|
||||||
pathParameters: {
|
|
||||||
"id": friend.track.album.id,
|
|
||||||
},
|
|
||||||
extra: album,
|
extra: album,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomeGenresSection extends HookConsumerWidget {
|
class HomeGenresSection extends HookConsumerWidget {
|
||||||
@ -52,11 +50,11 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(GenrePage.name);
|
context.push('/genres');
|
||||||
},
|
},
|
||||||
icon: const Icon(SpotubeIcons.angleRight),
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
label: Text(
|
label: Text(
|
||||||
context.l10n.browse_all,
|
"Browse All",
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.secondary,
|
color: colorScheme.secondary,
|
||||||
),
|
),
|
||||||
@ -112,13 +110,7 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.push('/genre/${category.id}', extra: category);
|
||||||
GenrePlaylistsPage.name,
|
|
||||||
pathParameters: {
|
|
||||||
"categoryId": category.id!,
|
|
||||||
},
|
|
||||||
extra: category,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
@ -134,7 +126,7 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceVariant,
|
||||||
gradient: categoriesQuery.isLoading ? null : gradient,
|
gradient: categoriesQuery.isLoading ? null : gradient,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
|
||||||
import 'package:spotube/provider/history/recent.dart';
|
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
|
|
||||||
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
|
||||||
const HomeRecentlyPlayedSection({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final history = ref.watch(recentlyPlayedItems);
|
|
||||||
|
|
||||||
if (history.isEmpty) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView(
|
|
||||||
title: const Text('Recently Played'),
|
|
||||||
items: [
|
|
||||||
for (final item in history)
|
|
||||||
if (item is PlaybackHistoryPlaylist)
|
|
||||||
item.playlist
|
|
||||||
else if (item is PlaybackHistoryAlbum)
|
|
||||||
item.album
|
|
||||||
],
|
|
||||||
hasNextPage: false,
|
|
||||||
isLoadingNextPage: false,
|
|
||||||
onFetchMore: () {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
|
||||||
import 'package:spotube/pages/library/local_folder.dart';
|
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
||||||
|
|
||||||
class LocalFolderItem extends HookConsumerWidget {
|
|
||||||
final String folder;
|
|
||||||
const LocalFolderItem({super.key, required this.folder});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final ThemeData(:colorScheme) = Theme.of(context);
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
|
||||||
|
|
||||||
final lerpValue = useBrightnessValue(.9, .7);
|
|
||||||
|
|
||||||
final downloadFolder =
|
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
|
|
||||||
|
|
||||||
final isDownloadFolder = folder == downloadFolder;
|
|
||||||
|
|
||||||
final Uri(:pathSegments) = Uri.parse(
|
|
||||||
folder
|
|
||||||
.replaceFirst(RegExp(r'^/Volumes/[^/]+/Users/'), "")
|
|
||||||
.replaceFirst(r'C:\Users\', "")
|
|
||||||
.replaceFirst(r'/home/', ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
// if length > 5, we ... all the middle segments after 2 and the last 2
|
|
||||||
final segments = pathSegments.length > 5
|
|
||||||
? [
|
|
||||||
...pathSegments.take(2),
|
|
||||||
"...",
|
|
||||||
...pathSegments.skip(pathSegments.length - 3).toList()
|
|
||||||
..removeLast(),
|
|
||||||
]
|
|
||||||
: pathSegments.take(pathSegments.length - 1).toList();
|
|
||||||
|
|
||||||
final trackSnapshot = ref.watch(
|
|
||||||
localTracksProvider.select(
|
|
||||||
(s) => s.whenData((tracks) => tracks[folder]?.take(4).toList()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final tracks = trackSnapshot.value ?? [];
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
context.goNamed(
|
|
||||||
LocalLibraryPage.name,
|
|
||||||
queryParameters: {
|
|
||||||
if (isDownloadFolder) "downloads": "true",
|
|
||||||
},
|
|
||||||
extra: folder,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Ink(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: Color.lerp(
|
|
||||||
colorScheme.surfaceContainerHighest,
|
|
||||||
colorScheme.surface,
|
|
||||||
lerpValue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (tracks.isEmpty)
|
|
||||||
Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Icon(
|
|
||||||
SpotubeIcons.folder,
|
|
||||||
size: mediaQuery.smAndDown
|
|
||||||
? 95
|
|
||||||
: mediaQuery.mdAndDown
|
|
||||||
? 100
|
|
||||||
: 142,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: GridView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: max((tracks.length / 2).ceil(), 2),
|
|
||||||
),
|
|
||||||
itemCount: tracks.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final track = tracks[index];
|
|
||||||
return UniversalImage(
|
|
||||||
path: (track.album?.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
isDownloadFolder
|
|
||||||
? context.l10n.downloads
|
|
||||||
: basename(folder),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isDownloadFolder)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: PopupMenuButton(
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(3),
|
|
||||||
child: Icon(Icons.more_vert),
|
|
||||||
),
|
|
||||||
itemBuilder: (context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem(
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(SpotubeIcons.folderRemove),
|
|
||||||
iconColor: colorScheme.error,
|
|
||||||
title:
|
|
||||||
Text(context.l10n.remove_library_location),
|
|
||||||
onTap: () {
|
|
||||||
final libraryLocations = ref
|
|
||||||
.read(userPreferencesProvider)
|
|
||||||
.localLibraryLocation;
|
|
||||||
ref
|
|
||||||
.read(userPreferencesProvider.notifier)
|
|
||||||
.setLocalLibraryLocation(
|
|
||||||
libraryLocations
|
|
||||||
.where((e) => e != folder)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Wrap(
|
|
||||||
spacing: 2,
|
|
||||||
runSpacing: 2,
|
|
||||||
children: [
|
|
||||||
for (final MapEntry(key: index, value: segment)
|
|
||||||
in segments.asMap().entries)
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
if (index != 0)
|
|
||||||
TextSpan(
|
|
||||||
text: "/ ",
|
|
||||||
style: TextStyle(color: colorScheme.primary),
|
|
||||||
),
|
|
||||||
TextSpan(text: segment),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -71,7 +71,7 @@ class MultiSelectField<T> extends HookWidget {
|
|||||||
: theme.colorScheme.onSurface.withOpacity(0.1),
|
: theme.colorScheme.onSurface.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
mouseCursor: WidgetStateMouseCursor.textable,
|
mouseCursor: MaterialStateMouseCursor.textable,
|
||||||
onPressed: !enabled
|
onPressed: !enabled
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
@ -1,18 +1,52 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'dart:io';
|
||||||
import 'package:file_selector/file_selector.dart';
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/library/local_folder/local_folder_item.dart';
|
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||||
import 'package:spotube/extensions/constrains.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/context.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.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/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// 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 {
|
enum SortBy {
|
||||||
none,
|
none,
|
||||||
@ -25,77 +59,273 @@ enum SortBy {
|
|||||||
album,
|
album,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final localTracksProvider = FutureProvider<List<LocalTrack>>((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 {
|
class UserLocalTracks extends HookConsumerWidget {
|
||||||
const UserLocalTracks({super.key});
|
const UserLocalTracks({super.key});
|
||||||
|
|
||||||
|
Future<void> playLocalTracks(
|
||||||
|
WidgetRef ref,
|
||||||
|
List<LocalTrack> 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
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
final sortBy = useState<SortBy>(SortBy.none);
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
|
final isPlaylistPlaying =
|
||||||
|
playlist.containsTracks(trackSnapshot.asData?.value ?? []);
|
||||||
|
|
||||||
final addLocalLibraryLocation = useCallback(() async {
|
final searchController = useTextEditingController();
|
||||||
if (kIsMobile || kIsMacOS) {
|
useValueListenable(searchController);
|
||||||
final dirStr = await FilePicker.platform.getDirectoryPath(
|
final searchFocus = useFocusNode();
|
||||||
initialDirectory: preferences.downloadLocation,
|
final isFiltering = useState(false);
|
||||||
);
|
|
||||||
if (dirStr == null) return;
|
|
||||||
if (preferences.localLibraryLocation.contains(dirStr)) return;
|
|
||||||
preferencesNotifier.setLocalLibraryLocation(
|
|
||||||
[...preferences.localLibraryLocation, dirStr]);
|
|
||||||
} else {
|
|
||||||
String? dirStr = await getDirectoryPath(
|
|
||||||
initialDirectory: preferences.downloadLocation,
|
|
||||||
);
|
|
||||||
if (dirStr == null) return;
|
|
||||||
if (preferences.localLibraryLocation.contains(dirStr)) return;
|
|
||||||
preferencesNotifier.setLocalLibraryLocation(
|
|
||||||
[...preferences.localLibraryLocation, dirStr]);
|
|
||||||
}
|
|
||||||
}, [preferences.localLibraryLocation]);
|
|
||||||
|
|
||||||
// This is just to pre-load the tracks.
|
final controller = useScrollController();
|
||||||
// For now, this gets all of them.
|
|
||||||
ref.watch(localTracksProvider);
|
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constrains) {
|
return Column(
|
||||||
return Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(8.0),
|
||||||
children: [
|
child: Row(
|
||||||
Align(
|
children: [
|
||||||
alignment: Alignment.centerRight,
|
const SizedBox(width: 5),
|
||||||
child: TextButton.icon(
|
FilledButton(
|
||||||
icon: const Icon(SpotubeIcons.folderAdd),
|
onPressed: trackSnapshot.asData?.value != null
|
||||||
label: Text(context.l10n.add_library_location),
|
? () async {
|
||||||
onPressed: addLocalLibraryLocation,
|
if (trackSnapshot.asData?.value.isNotEmpty == true) {
|
||||||
),
|
if (!isPlaylistPlaying) {
|
||||||
),
|
await playLocalTracks(
|
||||||
const Gap(8),
|
ref,
|
||||||
Expanded(
|
trackSnapshot.asData!.value,
|
||||||
child: GridView.builder(
|
);
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
}
|
||||||
maxCrossAxisExtent: 200,
|
}
|
||||||
mainAxisExtent: constrains.isXs
|
}
|
||||||
? 210
|
: null,
|
||||||
: constrains.mdAndDown
|
child: Row(
|
||||||
? 280
|
children: [
|
||||||
: 250,
|
Text(context.l10n.play),
|
||||||
crossAxisSpacing: 10,
|
Icon(
|
||||||
mainAxisSpacing: 10,
|
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
itemCount: preferences.localLibraryLocation.length + 1,
|
),
|
||||||
itemBuilder: (context, index) {
|
const Spacer(),
|
||||||
return LocalFolderItem(
|
ExpandableSearchButton(
|
||||||
folder: index == 0
|
isFiltering: isFiltering.value,
|
||||||
? preferences.downloadLocation
|
onPressed: (value) => isFiltering.value = value,
|
||||||
: preferences.localLibraryLocation[index - 1],
|
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()),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,8 +122,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
top: 5.0,
|
top: 5.0,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
child: CallbackShortcuts(
|
child: CallbackShortcuts(
|
||||||
|
@ -208,8 +208,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
: mediaQuery.size.height * .6,
|
: mediaQuery.size.height * .6,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
color:
|
color: theme.colorScheme.surfaceVariant.withOpacity(.5),
|
||||||
theme.colorScheme.surfaceContainerHighest.withOpacity(.5),
|
|
||||||
),
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|
||||||
@ -30,17 +31,11 @@ class VolumeSlider extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: SliderTheme(
|
child: Slider(
|
||||||
data: const SliderThemeData(
|
min: 0,
|
||||||
showValueIndicator: ShowValueIndicator.always,
|
max: 1,
|
||||||
),
|
value: value,
|
||||||
child: Slider(
|
onChanged: onChanged,
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
label: (value * 100).toStringAsFixed(0),
|
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -6,9 +6,7 @@ import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
|||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -24,8 +22,6 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
@ -36,23 +32,12 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(meProvider);
|
||||||
|
|
||||||
Future<List<Track>> fetchInitialTracks() async {
|
Future<List<Track>> fetchAllTracks() async {
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
return await ref.read(likedTracksProvider.future);
|
return await ref.read(likedTracksProvider.future);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result =
|
await ref.read(playlistTracksProvider(playlist.id!).future);
|
||||||
await ref.read(playlistTracksProvider(playlist.id!).future);
|
|
||||||
|
|
||||||
return result.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTracks() async {
|
|
||||||
final initialTracks = await fetchInitialTracks();
|
|
||||||
|
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
|
||||||
return initialTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
||||||
}
|
}
|
||||||
@ -70,12 +55,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
||||||
me.asData?.value.id != null,
|
me.asData?.value.id != null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.push(
|
||||||
context,
|
context,
|
||||||
PlaylistPage.name,
|
"/playlist/${playlist.id}",
|
||||||
pathParameters: {
|
|
||||||
"id": playlist.id!,
|
|
||||||
},
|
|
||||||
extra: playlist,
|
extra: playlist,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -88,29 +70,22 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return audioPlayer.resume();
|
return audioPlayer.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
final fetchedInitialTracks = await fetchInitialTracks();
|
List<Track> fetchedTracks = await fetchAllTracks();
|
||||||
|
|
||||||
if (fetchedInitialTracks.isEmpty || !context.mounted) return;
|
if (fetchedTracks.isEmpty || !context.mounted) return;
|
||||||
|
|
||||||
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
final allTracks = await fetchAllTracks();
|
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.playlist(
|
WebSocketLoadEventData(
|
||||||
tracks: allTracks,
|
tracks: fetchedTracks,
|
||||||
collection: playlist,
|
collectionId: playlist.id!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedInitialTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
historyNotifier.addPlaylists([playlist]);
|
|
||||||
|
|
||||||
final allTracks = await fetchAllTracks();
|
|
||||||
|
|
||||||
await playlistNotifier
|
|
||||||
.addTracks(allTracks.sublist(fetchedInitialTracks.length));
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@ -123,22 +98,20 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying) return;
|
if (isPlaylistPlaying) return;
|
||||||
|
|
||||||
final fetchedInitialTracks = await fetchAllTracks();
|
final fetchedTracks = await fetchAllTracks();
|
||||||
|
|
||||||
if (fetchedInitialTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
|
|
||||||
playlistNotifier.addTracks(fetchedInitialTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
historyNotifier.addPlaylists([playlist]);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content:
|
content: Text("Added ${fetchedTracks.length} tracks to queue"),
|
||||||
Text("Added ${fetchedInitialTracks.length} tracks to queue"),
|
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier
|
playlistNotifier
|
||||||
.removeTracks(fetchedInitialTracks.map((e) => e.id!));
|
.removeTracks(fetchedTracks.map((e) => e.id!));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -14,6 +15,7 @@ import 'package:spotube/components/player/volume_slider.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
@ -22,7 +24,6 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
|||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
class BottomPlayer extends HookConsumerWidget {
|
class BottomPlayer extends HookConsumerWidget {
|
||||||
BottomPlayer({super.key});
|
BottomPlayer({super.key});
|
||||||
@ -48,6 +49,12 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final bg = theme.colorScheme.surfaceVariant;
|
||||||
|
|
||||||
|
final bgColor = useBrightnessValue(
|
||||||
|
Color.lerp(bg, Colors.white, 0.7),
|
||||||
|
Color.lerp(bg, Colors.black, 0.45)!,
|
||||||
|
);
|
||||||
|
|
||||||
// returning an empty non spacious Container as the overlay will take
|
// returning an empty non spacious Container as the overlay will take
|
||||||
// place in the global overlay stack aka [_entries]
|
// place in the global overlay stack aka [_entries]
|
||||||
@ -60,9 +67,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: bgColor?.withOpacity(0.8)),
|
||||||
color: theme.colorScheme.surfaceContainer.withOpacity(.8),
|
|
||||||
),
|
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
textStyle: theme.textTheme.bodyMedium!,
|
textStyle: theme.textTheme.bodyMedium!,
|
||||||
@ -90,19 +95,19 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
tooltip: context.l10n.mini_player,
|
tooltip: context.l10n.mini_player,
|
||||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (!kIsDesktop) return;
|
final prevSize =
|
||||||
|
await DesktopTools.window.getSize();
|
||||||
final prevSize = await windowManager.getSize();
|
await DesktopTools.window.setMinimumSize(
|
||||||
await windowManager.setMinimumSize(
|
|
||||||
const Size(300, 300),
|
const Size(300, 300),
|
||||||
);
|
);
|
||||||
await windowManager.setAlwaysOnTop(true);
|
await DesktopTools.window.setAlwaysOnTop(true);
|
||||||
if (!kIsLinux) {
|
if (!kIsLinux) {
|
||||||
await windowManager.setHasShadow(false);
|
await DesktopTools.window.setHasShadow(false);
|
||||||
}
|
}
|
||||||
await windowManager
|
await DesktopTools.window
|
||||||
.setAlignment(Alignment.topRight);
|
.setAlignment(Alignment.topRight);
|
||||||
await windowManager.setSize(const Size(400, 500));
|
await DesktopTools.window
|
||||||
|
.setSize(const Size(400, 500));
|
||||||
await Future.delayed(
|
await Future.delayed(
|
||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
() async {
|
() async {
|
||||||
|
@ -14,9 +14,8 @@ import 'package:spotube/components/shared/image/universal_image.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
||||||
import 'package:spotube/pages/profile/profile.dart';
|
|
||||||
import 'package:spotube/pages/settings/settings.dart';
|
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
@ -27,9 +26,13 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
|
final int? selectedIndex;
|
||||||
|
final void Function(int) onSelectedIndexChanged;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const Sidebar({
|
const Sidebar({
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onSelectedIndexChanged,
|
||||||
required this.child,
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@ -44,9 +47,12 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void goToSettings(BuildContext context) {
|
||||||
|
GoRouter.of(context).go("/settings");
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final routerState = GoRouterState.of(context);
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
||||||
@ -54,22 +60,41 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
|
final controller = useSidebarXController(
|
||||||
|
selectedIndex: selectedIndex ?? 0,
|
||||||
|
extended: mediaQuery.lgAndUp,
|
||||||
|
);
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final bg = theme.colorScheme.surfaceVariant;
|
||||||
|
|
||||||
|
final bgColor = useBrightnessValue(
|
||||||
|
Color.lerp(bg, Colors.white, 0.7),
|
||||||
|
Color.lerp(bg, Colors.black, 0.45)!,
|
||||||
|
);
|
||||||
|
|
||||||
final sidebarTileList = useMemoized(
|
final sidebarTileList = useMemoized(
|
||||||
() => getSidebarTileList(context.l10n),
|
() => getSidebarTileList(context.l10n),
|
||||||
[context.l10n],
|
[context.l10n],
|
||||||
);
|
);
|
||||||
|
|
||||||
final selectedIndex = sidebarTileList.indexWhere(
|
useEffect(() {
|
||||||
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
|
if (controller.selectedIndex != selectedIndex && selectedIndex != null) {
|
||||||
);
|
controller.selectIndex(selectedIndex!);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
final controller = useSidebarXController(
|
useEffect(() {
|
||||||
selectedIndex: selectedIndex,
|
void listener() {
|
||||||
extended: mediaQuery.lgAndUp,
|
onSelectedIndexChanged(controller.selectedIndex);
|
||||||
);
|
}
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
controller.addListener(listener);
|
||||||
final bg = theme.colorScheme.surfaceContainer;
|
return () {
|
||||||
|
controller.removeListener(listener);
|
||||||
|
};
|
||||||
|
}, [controller]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@ -81,13 +106,6 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [mediaQuery, controller]);
|
}, [mediaQuery, controller]);
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (controller.selectedIndex != selectedIndex) {
|
|
||||||
controller.selectIndex(selectedIndex);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [selectedIndex]);
|
|
||||||
|
|
||||||
if (layoutMode == LayoutMode.compact ||
|
if (layoutMode == LayoutMode.compact ||
|
||||||
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
|
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
|
||||||
return Scaffold(body: child);
|
return Scaffold(body: child);
|
||||||
@ -101,28 +119,23 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
items: sidebarTileList.mapIndexed(
|
items: sidebarTileList.mapIndexed(
|
||||||
(index, e) {
|
(index, e) {
|
||||||
return SidebarXItem(
|
return SidebarXItem(
|
||||||
onTap: () {
|
iconWidget: Badge(
|
||||||
context.goNamed(e.name);
|
backgroundColor: theme.colorScheme.primary,
|
||||||
},
|
isLabelVisible: e.title == "Library" && downloadCount > 0,
|
||||||
iconBuilder: (selected, hovered) {
|
label: Text(
|
||||||
return Badge(
|
downloadCount.toString(),
|
||||||
backgroundColor: theme.colorScheme.primary,
|
style: const TextStyle(
|
||||||
isLabelVisible: e.title == "Library" && downloadCount > 0,
|
color: Colors.white,
|
||||||
label: Text(
|
fontSize: 10,
|
||||||
downloadCount.toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Icon(
|
),
|
||||||
e.icon,
|
child: Icon(
|
||||||
color: selected || hovered
|
e.icon,
|
||||||
? theme.colorScheme.primary
|
color: selectedIndex == index
|
||||||
: null,
|
? theme.colorScheme.primary
|
||||||
),
|
: null,
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
label: e.title,
|
label: e.title,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -153,7 +166,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bg,
|
color: bgColor?.withOpacity(0.8),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(10),
|
topRight: Radius.circular(10),
|
||||||
bottomRight: Radius.circular(10),
|
bottomRight: Radius.circular(10),
|
||||||
@ -244,7 +257,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
if (mediaQuery.mdAndDown) {
|
if (mediaQuery.mdAndDown) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(SpotubeIcons.settings),
|
icon: const Icon(SpotubeIcons.settings),
|
||||||
onPressed: () => ServiceUtils.navigateNamed(context, SettingsPage.name),
|
onPressed: () => Sidebar.goToSettings(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +278,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.pushNamed(context, ProfilePage.name);
|
ServiceUtils.push(context, "/profile");
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -297,7 +310,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.settings),
|
icon: const Icon(SpotubeIcons.settings),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ServiceUtils.pushNamed(context, SettingsPage.name);
|
Sidebar.goToSettings(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,54 +3,55 @@ import 'dart:ui';
|
|||||||
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
|
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/side_bar_tiles.dart';
|
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||||
|
import 'package:spotube/components/root/sidebar.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
final navigationPanelHeight = StateProvider<double>((ref) => 50);
|
final navigationPanelHeight = StateProvider<double>((ref) => 50);
|
||||||
|
|
||||||
class SpotubeNavigationBar extends HookConsumerWidget {
|
class SpotubeNavigationBar extends HookConsumerWidget {
|
||||||
|
final int? selectedIndex;
|
||||||
|
final void Function(int) onSelectedIndexChanged;
|
||||||
|
|
||||||
const SpotubeNavigationBar({
|
const SpotubeNavigationBar({
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onSelectedIndexChanged,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final routerState = GoRouterState.of(context);
|
|
||||||
|
|
||||||
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
|
final insideSelectedIndex = useState<int>(selectedIndex ?? 0);
|
||||||
|
|
||||||
final buttonColor = useBrightnessValue(
|
final buttonColor = useBrightnessValue(
|
||||||
theme.colorScheme.inversePrimary,
|
theme.colorScheme.inversePrimary,
|
||||||
theme.colorScheme.primary.withOpacity(0.2),
|
theme.colorScheme.primary.withOpacity(0.2),
|
||||||
);
|
);
|
||||||
|
|
||||||
final navbarTileList = useMemoized(
|
final navbarTileList =
|
||||||
() => getNavbarTileList(context.l10n),
|
useMemoized(() => getNavbarTileList(context.l10n), [context.l10n]);
|
||||||
[context.l10n],
|
|
||||||
);
|
|
||||||
|
|
||||||
final panelHeight = ref.watch(navigationPanelHeight);
|
final panelHeight = ref.watch(navigationPanelHeight);
|
||||||
|
|
||||||
final selectedIndex = useMemoized(() {
|
useEffect(() {
|
||||||
final index = navbarTileList.indexWhere(
|
if (selectedIndex != null) {
|
||||||
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
|
insideSelectedIndex.value = selectedIndex!;
|
||||||
);
|
}
|
||||||
|
return null;
|
||||||
return index == -1 ? 0 : index;
|
}, [selectedIndex]);
|
||||||
}, [navbarTileList, routerState.matchedLocation]);
|
|
||||||
|
|
||||||
if (layoutMode == LayoutMode.extended ||
|
if (layoutMode == LayoutMode.extended ||
|
||||||
(mediaQuery.mdAndUp && layoutMode == LayoutMode.adaptive) ||
|
(mediaQuery.mdAndUp && layoutMode == LayoutMode.adaptive) ||
|
||||||
@ -68,7 +69,7 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
theme.colorScheme.secondaryContainer.withOpacity(0.72),
|
theme.colorScheme.secondaryContainer.withOpacity(0.72),
|
||||||
buttonBackgroundColor: buttonColor,
|
buttonBackgroundColor: buttonColor,
|
||||||
color: theme.colorScheme.surface,
|
color: theme.colorScheme.background,
|
||||||
height: panelHeight,
|
height: panelHeight,
|
||||||
animationDuration: const Duration(milliseconds: 350),
|
animationDuration: const Duration(milliseconds: 350),
|
||||||
items: navbarTileList.map(
|
items: navbarTileList.map(
|
||||||
@ -90,9 +91,14 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
index: selectedIndex,
|
index: insideSelectedIndex.value,
|
||||||
onTap: (i) {
|
onTap: (i) {
|
||||||
ServiceUtils.navigateNamed(context, navbarTileList[i].name);
|
insideSelectedIndex.value = i;
|
||||||
|
if (navbarTileList[i].id == "settings") {
|
||||||
|
Sidebar.goToSettings(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectedIndexChanged(i);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
class RootAppUpdateDialog extends StatelessWidget {
|
|
||||||
final Version? version;
|
|
||||||
final int? nightlyBuildNum;
|
|
||||||
|
|
||||||
const RootAppUpdateDialog({super.key, this.version}) : nightlyBuildNum = null;
|
|
||||||
const RootAppUpdateDialog.nightly({super.key, required this.nightlyBuildNum})
|
|
||||||
: version = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const url = "https://spotube.krtirtho.dev/downloads";
|
|
||||||
const nightlyUrl = "https://spotube.krtirtho.dev/downloads/nightly";
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text("Spotube has an update"),
|
|
||||||
actions: [
|
|
||||||
FilledButton(
|
|
||||||
child: const Text("Download Now"),
|
|
||||||
onPressed: () => launchUrlString(
|
|
||||||
nightlyBuildNum != null ? nightlyUrl : url,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
nightlyBuildNum != null
|
|
||||||
? "Spotube Nightly $nightlyBuildNum has been released"
|
|
||||||
: "Spotube v$version has been released",
|
|
||||||
),
|
|
||||||
if (nightlyBuildNum == null)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text("Read the latest "),
|
|
||||||
AnchorButton(
|
|
||||||
"release notes",
|
|
||||||
style: const TextStyle(color: Colors.blue),
|
|
||||||
onTap: () => launchUrlString(
|
|
||||||
url,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -179,9 +179,9 @@ class ColorTile extends StatelessWidget {
|
|||||||
colorScheme.primaryContainer,
|
colorScheme.primaryContainer,
|
||||||
colorScheme.secondary,
|
colorScheme.secondary,
|
||||||
colorScheme.secondaryContainer,
|
colorScheme.secondaryContainer,
|
||||||
|
colorScheme.background,
|
||||||
colorScheme.surface,
|
colorScheme.surface,
|
||||||
colorScheme.surface,
|
colorScheme.surfaceVariant,
|
||||||
colorScheme.surfaceContainerHighest,
|
|
||||||
colorScheme.onPrimary,
|
colorScheme.onPrimary,
|
||||||
colorScheme.onSurface,
|
colorScheme.onSurface,
|
||||||
];
|
];
|
||||||
|
@ -187,7 +187,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
style: theme.iconButtonTheme.style?.copyWith(
|
style: theme.iconButtonTheme.style?.copyWith(
|
||||||
shape: WidgetStatePropertyAll(
|
shape: MaterialStatePropertyAll(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/extensions/context.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_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -26,7 +25,7 @@ class AnonymousFallback extends ConsumerWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: Text(context.l10n.login_with_spotify),
|
child: Text(context.l10n.login_with_spotify),
|
||||||
onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name),
|
onPressed: () => ServiceUtils.push(context, "/settings"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -96,7 +96,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
return switch (item) {
|
return switch (item) {
|
||||||
PlaylistSimple() =>
|
PlaylistSimple() =>
|
||||||
PlaylistCard(item as PlaylistSimple),
|
PlaylistCard(item as PlaylistSimple),
|
||||||
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
AlbumSimple() => AlbumCard(item as Album),
|
||||||
Artist() => Padding(
|
Artist() => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12.0),
|
horizontal: 12.0),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
|
|
||||||
class InterScrollbar extends HookWidget {
|
class InterScrollbar extends HookWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -15,7 +15,7 @@ class InterScrollbar extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (kIsDesktop) return child;
|
if (DesktopTools.platform.isDesktop) return child;
|
||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
return DraggableScrollbar.semicircle(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
@ -29,7 +29,7 @@ class AnchorButton<T> extends HookWidget {
|
|||||||
onTapUp: (event) => tap.value = false,
|
onTapUp: (event) => tap.value = false,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: WidgetStateMouseCursor.clickable,
|
cursor: MaterialStateMouseCursor.clickable,
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: style.copyWith(
|
style: style.copyWith(
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class ArtistLink extends StatelessWidget {
|
class ArtistLink extends StatelessWidget {
|
||||||
@ -41,12 +40,9 @@ class ArtistLink extends StatelessWidget {
|
|||||||
if (onRouteChange != null) {
|
if (onRouteChange != null) {
|
||||||
onRouteChange?.call("/artist/${artist.value.id}");
|
onRouteChange?.call("/artist/${artist.value.id}");
|
||||||
} else {
|
} else {
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.push(
|
||||||
context,
|
context,
|
||||||
ArtistPage.name,
|
"/artist/${artist.value.id}",
|
||||||
pathParameters: {
|
|
||||||
"id": artist.value.id!,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7,8 +7,7 @@ import 'package:titlebar_buttons/titlebar_buttons.dart';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
@ -90,7 +89,7 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
|||||||
final systemTitleBar =
|
final systemTitleBar =
|
||||||
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
|
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
|
||||||
if (kIsDesktop && !systemTitleBar) {
|
if (kIsDesktop && !systemTitleBar) {
|
||||||
windowManager.startDragging();
|
DesktopTools.window.startDragging();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +107,11 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
|||||||
|
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
left: DesktopTools.platform.isMacOS &&
|
||||||
|
hasFullscreen &&
|
||||||
|
hasLeadingOrCanPop
|
||||||
|
? 65
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
leading: widget.leading,
|
leading: widget.leading,
|
||||||
@ -146,7 +149,11 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
|||||||
onVerticalDragStart: onDrag,
|
onVerticalDragStart: onDrag,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
left: DesktopTools.platform.isMacOS &&
|
||||||
|
hasFullscreen &&
|
||||||
|
hasLeadingOrCanPop
|
||||||
|
? 65
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
leading: widget.leading,
|
leading: widget.leading,
|
||||||
@ -165,10 +172,6 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
|||||||
toolbarTextStyle: widget.toolbarTextStyle,
|
toolbarTextStyle: widget.toolbarTextStyle,
|
||||||
titleTextStyle: widget.titleTextStyle,
|
titleTextStyle: widget.titleTextStyle,
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
scrolledUnderElevation: 0,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
forceMaterialTransparency: true,
|
|
||||||
elevation: 0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -190,12 +193,12 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
const type = ThemeType.auto;
|
const type = ThemeType.auto;
|
||||||
|
|
||||||
Future<void> onClose() async {
|
Future<void> onClose() async {
|
||||||
await windowManager.close();
|
await DesktopTools.window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
windowManager.isMaximized().then((value) {
|
DesktopTools.window.isMaximized().then((value) {
|
||||||
isMaximized.value = value;
|
isMaximized.value = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -210,16 +213,16 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = WindowButtonColors(
|
final colors = WindowButtonColors(
|
||||||
normal: Colors.transparent,
|
normal: Colors.transparent,
|
||||||
iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
|
iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
|
||||||
mouseOver: theme.colorScheme.onSurface.withOpacity(0.1),
|
mouseOver: theme.colorScheme.onBackground.withOpacity(0.1),
|
||||||
mouseDown: theme.colorScheme.onSurface.withOpacity(0.2),
|
mouseDown: theme.colorScheme.onBackground.withOpacity(0.2),
|
||||||
iconMouseOver: theme.colorScheme.onSurface,
|
iconMouseOver: theme.colorScheme.onBackground,
|
||||||
iconMouseDown: theme.colorScheme.onSurface,
|
iconMouseDown: theme.colorScheme.onBackground,
|
||||||
);
|
);
|
||||||
|
|
||||||
final closeColors = WindowButtonColors(
|
final closeColors = WindowButtonColors(
|
||||||
normal: Colors.transparent,
|
normal: Colors.transparent,
|
||||||
iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
|
iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
|
||||||
mouseOver: Colors.red,
|
mouseOver: Colors.red,
|
||||||
mouseDown: Colors.red[800]!,
|
mouseDown: Colors.red[800]!,
|
||||||
iconMouseOver: Colors.white,
|
iconMouseOver: Colors.white,
|
||||||
@ -232,14 +235,14 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MinimizeWindowButton(
|
MinimizeWindowButton(
|
||||||
onPressed: windowManager.minimize,
|
onPressed: DesktopTools.window.minimize,
|
||||||
colors: colors,
|
colors: colors,
|
||||||
),
|
),
|
||||||
if (isMaximized.value != true)
|
if (isMaximized.value != true)
|
||||||
MaximizeWindowButton(
|
MaximizeWindowButton(
|
||||||
colors: colors,
|
colors: colors,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
windowManager.maximize();
|
DesktopTools.window.maximize();
|
||||||
isMaximized.value = true;
|
isMaximized.value = true;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -247,7 +250,7 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
RestoreWindowButton(
|
RestoreWindowButton(
|
||||||
colors: colors,
|
colors: colors,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
windowManager.unmaximize();
|
DesktopTools.window.unmaximize();
|
||||||
isMaximized.value = false;
|
isMaximized.value = false;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -267,16 +270,16 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
DecoratedMinimizeButton(
|
DecoratedMinimizeButton(
|
||||||
type: type,
|
type: type,
|
||||||
onPressed: windowManager.minimize,
|
onPressed: DesktopTools.window.minimize,
|
||||||
),
|
),
|
||||||
DecoratedMaximizeButton(
|
DecoratedMaximizeButton(
|
||||||
type: type,
|
type: type,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (await windowManager.isMaximized()) {
|
if (await DesktopTools.window.isMaximized()) {
|
||||||
await windowManager.unmaximize();
|
await DesktopTools.window.unmaximize();
|
||||||
isMaximized.value = false;
|
isMaximized.value = false;
|
||||||
} else {
|
} else {
|
||||||
await windowManager.maximize();
|
await DesktopTools.window.maximize();
|
||||||
isMaximized.value = true;
|
isMaximized.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -53,10 +53,6 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final radius = BorderRadius.circular(15);
|
final radius = BorderRadius.circular(15);
|
||||||
|
|
||||||
final bgColor = useBrightnessValue(
|
|
||||||
theme.colorScheme.surface,
|
|
||||||
theme.colorScheme.surfaceContainerHigh,
|
|
||||||
);
|
|
||||||
final double size = useBreakpointValue<double>(
|
final double size = useBreakpointValue<double>(
|
||||||
xs: 130,
|
xs: 130,
|
||||||
sm: 130,
|
sm: 130,
|
||||||
@ -76,9 +72,13 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
constraints: BoxConstraints(maxWidth: size),
|
constraints: BoxConstraints(maxWidth: size),
|
||||||
margin: margin,
|
margin: margin,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: bgColor,
|
color: Color.lerp(
|
||||||
|
theme.colorScheme.surfaceVariant,
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
useBrightnessValue(.9, .7),
|
||||||
|
),
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
shadowColor: theme.colorScheme.surface,
|
shadowColor: theme.colorScheme.background,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
mouseCursor: SystemMouseCursors.click,
|
mouseCursor: SystemMouseCursors.click,
|
||||||
@ -158,7 +158,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
Skeleton.keep(
|
Skeleton.keep(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.surface,
|
backgroundColor: theme.colorScheme.background,
|
||||||
foregroundColor: theme.colorScheme.primary,
|
foregroundColor: theme.colorScheme.primary,
|
||||||
minimumSize: const Size.square(10),
|
minimumSize: const Size.square(10),
|
||||||
),
|
),
|
||||||
|
@ -5,8 +5,7 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
|||||||
|
|
||||||
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
||||||
final List<Widget> tabs;
|
final List<Widget> tabs;
|
||||||
final TabController? controller;
|
const ThemedButtonsTabBar({super.key, required this.tabs});
|
||||||
const ThemedButtonsTabBar({super.key, required this.tabs, this.controller});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -22,7 +21,6 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
|||||||
bottom: 8,
|
bottom: 8,
|
||||||
),
|
),
|
||||||
child: ButtonsTabBar(
|
child: ButtonsTabBar(
|
||||||
controller: controller,
|
|
||||||
radius: 100,
|
radius: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bgColor,
|
color: bgColor,
|
||||||
@ -34,7 +32,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
unselectedDecoration: BoxDecoration(
|
unselectedDecoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surface,
|
color: theme.colorScheme.background,
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith(
|
unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.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/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.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/prompt_dialog.dart';
|
||||||
@ -22,7 +23,6 @@ import 'package:spotube/models/local_track.dart';
|
|||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
@ -197,8 +197,6 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
return downloadManager.getProgressNotifier(spotubeTrack);
|
||||||
});
|
});
|
||||||
|
|
||||||
final isLocalTrack = track is LocalTrack;
|
|
||||||
|
|
||||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||||
onSelected: (value) async {
|
onSelected: (value) async {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -316,120 +314,118 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
children: [
|
children: switch (track.runtimeType) {
|
||||||
if (isLocalTrack)
|
LocalTrack() => [
|
||||||
PopSheetEntry(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.delete,
|
value: TrackOptionValue.delete,
|
||||||
leading: const Icon(SpotubeIcons.trash),
|
leading: const Icon(SpotubeIcons.trash),
|
||||||
title: Text(context.l10n.delete),
|
title: Text(context.l10n.delete),
|
||||||
),
|
)
|
||||||
if (mediaQuery.smAndDown)
|
],
|
||||||
PopSheetEntry(
|
_ => [
|
||||||
value: TrackOptionValue.album,
|
if (mediaQuery.smAndDown)
|
||||||
leading: const Icon(SpotubeIcons.album),
|
PopSheetEntry(
|
||||||
title: Text(context.l10n.go_to_album),
|
value: TrackOptionValue.album,
|
||||||
subtitle: Text(track.album!.name!),
|
leading: const Icon(SpotubeIcons.album),
|
||||||
),
|
title: Text(context.l10n.go_to_album),
|
||||||
if (!playlist.containsTrack(track)) ...[
|
subtitle: Text(track.album!.name!),
|
||||||
PopSheetEntry(
|
),
|
||||||
value: TrackOptionValue.addToQueue,
|
if (!playlist.containsTrack(track)) ...[
|
||||||
leading: const Icon(SpotubeIcons.queueAdd),
|
PopSheetEntry(
|
||||||
title: Text(context.l10n.add_to_queue),
|
value: TrackOptionValue.addToQueue,
|
||||||
),
|
leading: const Icon(SpotubeIcons.queueAdd),
|
||||||
PopSheetEntry(
|
title: Text(context.l10n.add_to_queue),
|
||||||
value: TrackOptionValue.playNext,
|
),
|
||||||
leading: const Icon(SpotubeIcons.lightning),
|
PopSheetEntry(
|
||||||
title: Text(context.l10n.play_next),
|
value: TrackOptionValue.playNext,
|
||||||
),
|
leading: const Icon(SpotubeIcons.lightning),
|
||||||
] else
|
title: Text(context.l10n.play_next),
|
||||||
PopSheetEntry(
|
),
|
||||||
value: TrackOptionValue.removeFromQueue,
|
] else
|
||||||
enabled: playlist.activeTrack?.id != track.id,
|
PopSheetEntry(
|
||||||
leading: const Icon(SpotubeIcons.queueRemove),
|
value: TrackOptionValue.removeFromQueue,
|
||||||
title: Text(context.l10n.remove_from_queue),
|
enabled: playlist.activeTrack?.id != track.id,
|
||||||
),
|
leading: const Icon(SpotubeIcons.queueRemove),
|
||||||
if (me.asData?.value != null && !isLocalTrack)
|
title: Text(context.l10n.remove_from_queue),
|
||||||
PopSheetEntry(
|
),
|
||||||
value: TrackOptionValue.favorite,
|
if (me.asData?.value != null)
|
||||||
leading: favorites.isLiked
|
PopSheetEntry(
|
||||||
? const Icon(
|
value: TrackOptionValue.favorite,
|
||||||
SpotubeIcons.heartFilled,
|
leading: favorites.isLiked
|
||||||
color: Colors.pink,
|
? const Icon(
|
||||||
)
|
SpotubeIcons.heartFilled,
|
||||||
: const Icon(SpotubeIcons.heart),
|
color: Colors.pink,
|
||||||
title: Text(
|
)
|
||||||
favorites.isLiked
|
: const Icon(SpotubeIcons.heart),
|
||||||
? context.l10n.remove_from_favorites
|
title: Text(
|
||||||
: context.l10n.save_as_favorite,
|
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(
|
||||||
if (auth != null && !isLocalTrack) ...[
|
value: TrackOptionValue.blacklist,
|
||||||
PopSheetEntry(
|
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||||
value: TrackOptionValue.startRadio,
|
iconColor: !isBlackListed ? Colors.red[400] : null,
|
||||||
leading: const Icon(SpotubeIcons.radio),
|
textColor: !isBlackListed ? Colors.red[400] : null,
|
||||||
title: Text(context.l10n.start_a_radio),
|
title: Text(
|
||||||
),
|
isBlackListed
|
||||||
PopSheetEntry(
|
? context.l10n.remove_from_blacklist
|
||||||
value: TrackOptionValue.addToPlaylist,
|
: context.l10n.add_to_blacklist,
|
||||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
),
|
||||||
title: Text(context.l10n.add_to_playlist),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (userPlaylist && auth != 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 ? Colors.red[400] : null,
|
|
||||||
textColor: !isBlackListed ? Colors.red[400] : null,
|
|
||||||
title: Text(
|
|
||||||
isBlackListed
|
|
||||||
? context.l10n.remove_from_blacklist
|
|
||||||
: context.l10n.add_to_blacklist,
|
|
||||||
),
|
),
|
||||||
),
|
PopSheetEntry(
|
||||||
if (!isLocalTrack)
|
value: TrackOptionValue.share,
|
||||||
PopSheetEntry(
|
leading: const Icon(SpotubeIcons.share),
|
||||||
value: TrackOptionValue.share,
|
title: Text(context.l10n.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),
|
PopSheetEntry(
|
||||||
),
|
value: TrackOptionValue.songlink,
|
||||||
if (!isLocalTrack)
|
leading: Assets.logos.songlinkTransparent.image(
|
||||||
PopSheetEntry(
|
width: 22,
|
||||||
value: TrackOptionValue.details,
|
height: 22,
|
||||||
leading: const Icon(SpotubeIcons.info),
|
color: colorScheme.onSurface.withOpacity(0.5),
|
||||||
title: Text(context.l10n.details),
|
),
|
||||||
),
|
title: Text(context.l10n.song_link),
|
||||||
],
|
),
|
||||||
|
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
|
//! This is the most ANTI pattern I've ever done, but it works
|
||||||
|
@ -195,26 +195,19 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 6,
|
flex: 6,
|
||||||
child: switch (track) {
|
child: LinkText(
|
||||||
LocalTrack() => Text(
|
track.name!,
|
||||||
track.name!,
|
"/track/${track.id}",
|
||||||
maxLines: 1,
|
push: true,
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
_ => LinkText(
|
),
|
||||||
track.name!,
|
|
||||||
"/track/${track.id}",
|
|
||||||
push: true,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
if (constrains.mdAndUp) ...[
|
if (constrains.mdAndUp) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: switch (track) {
|
child: switch (track.runtimeType) {
|
||||||
LocalTrack() => Text(
|
LocalTrack() => Text(
|
||||||
track.album!.name!,
|
track.album!.name!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
@ -17,7 +17,6 @@ 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/shared/tracks_view/track_view_provider.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@ -29,7 +28,6 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
||||||
|
|
||||||
@ -148,17 +146,11 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
final tracks = await props.pagination.onFetchAll();
|
final tracks = await props.pagination.onFetchAll();
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
props.collection is AlbumSimple
|
WebSocketLoadEventData(
|
||||||
? WebSocketLoadEventData.album(
|
tracks: tracks,
|
||||||
tracks: tracks,
|
collectionId: props.collectionId,
|
||||||
collection: props.collection as AlbumSimple,
|
initialIndex: index,
|
||||||
initialIndex: index,
|
),
|
||||||
)
|
|
||||||
: WebSocketLoadEventData.playlist(
|
|
||||||
tracks: tracks,
|
|
||||||
collection: props.collection as PlaylistSimple,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -172,13 +164,6 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
if (props.collection is AlbumSimple) {
|
|
||||||
historyNotifier
|
|
||||||
.addAlbums([props.collection as AlbumSimple]);
|
|
||||||
} else {
|
|
||||||
historyNotifier
|
|
||||||
.addPlaylists([props.collection as PlaylistSimple]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.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/confirm_download_dialog.dart';
|
||||||
@ -9,7 +8,6 @@ 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/shared/tracks_view/track_view_provider.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
@ -25,7 +23,6 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
|
||||||
final audioSource =
|
final audioSource =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
||||||
|
|
||||||
@ -75,12 +72,6 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracksAtFirst(selectedTracks);
|
playlistNotifier.addTracksAtFirst(selectedTracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
if (props.collection is AlbumSimple) {
|
|
||||||
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
|
||||||
} else {
|
|
||||||
historyNotifier
|
|
||||||
.addPlaylists([props.collection as PlaylistSimple]);
|
|
||||||
}
|
|
||||||
trackViewState.deselectAll();
|
trackViewState.deselectAll();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -88,12 +79,6 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracks(selectedTracks);
|
playlistNotifier.addTracks(selectedTracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
if (props.collection is AlbumSimple) {
|
|
||||||
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
|
||||||
} else {
|
|
||||||
historyNotifier
|
|
||||||
.addPlaylists([props.collection as PlaylistSimple]);
|
|
||||||
}
|
|
||||||
trackViewState.deselectAll();
|
trackViewState.deselectAll();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
@ -12,7 +12,6 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
|
|
||||||
class TrackViewFlexHeader extends HookConsumerWidget {
|
class TrackViewFlexHeader extends HookConsumerWidget {
|
||||||
const TrackViewFlexHeader({super.key});
|
const TrackViewFlexHeader({super.key});
|
||||||
@ -54,7 +53,7 @@ class TrackViewFlexHeader extends HookConsumerWidget {
|
|||||||
floating: false,
|
floating: false,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 450,
|
expandedHeight: 450,
|
||||||
automaticallyImplyLeading: kIsMobile,
|
automaticallyImplyLeading: DesktopTools.platform.isMobile,
|
||||||
backgroundColor: palette.color,
|
backgroundColor: palette.color,
|
||||||
title: isExpanded ? null : Text(props.title, style: headingStyle),
|
title: isExpanded ? null : Text(props.title, style: headingStyle),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/shared/heart_button.dart';
|
import 'package:spotube/components/shared/heart_button.dart';
|
||||||
@ -10,7 +9,6 @@ import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_
|
|||||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
|
||||||
class TrackViewHeaderActions extends HookConsumerWidget {
|
class TrackViewHeaderActions extends HookConsumerWidget {
|
||||||
@ -22,7 +20,6 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -64,13 +61,6 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
final tracks = await props.pagination.onFetchAll();
|
final tracks = await props.pagination.onFetchAll();
|
||||||
await playlistNotifier.addTracks(tracks);
|
await playlistNotifier.addTracks(tracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
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 != null)
|
||||||
|
@ -5,14 +5,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/select_device_dialog.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/shared/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
@ -30,7 +28,6 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -47,45 +44,28 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final initialTracks = props.tracks;
|
final allTracks = await props.pagination.onFetchAll();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final allTracks = await props.pagination.onFetchAll();
|
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
props.collection is AlbumSimple
|
WebSocketLoadEventData(
|
||||||
? WebSocketLoadEventData.album(
|
tracks: allTracks,
|
||||||
tracks: allTracks,
|
collectionId: props.collectionId,
|
||||||
collection: props.collection as AlbumSimple,
|
initialIndex: Random().nextInt(allTracks.length)),
|
||||||
initialIndex: Random().nextInt(allTracks.length))
|
|
||||||
: WebSocketLoadEventData.playlist(
|
|
||||||
tracks: allTracks,
|
|
||||||
collection: props.collection as PlaylistSimple,
|
|
||||||
initialIndex: Random().nextInt(allTracks.length),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
await remotePlayback.setShuffle(true);
|
await remotePlayback.setShuffle(true);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(
|
await playlistNotifier.load(
|
||||||
initialTracks,
|
allTracks,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
initialIndex: Random().nextInt(initialTracks.length),
|
initialIndex: Random().nextInt(allTracks.length),
|
||||||
);
|
);
|
||||||
await audioPlayer.setShuffle(true);
|
await audioPlayer.setShuffle(true);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
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 {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -96,39 +76,22 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final initialTracks = props.tracks;
|
final allTracks = await props.pagination.onFetchAll();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final allTracks = await props.pagination.onFetchAll();
|
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
props.collection is AlbumSimple
|
WebSocketLoadEventData(
|
||||||
? WebSocketLoadEventData.album(
|
tracks: allTracks,
|
||||||
tracks: allTracks,
|
collectionId: props.collectionId,
|
||||||
collection: props.collection as AlbumSimple,
|
),
|
||||||
)
|
|
||||||
: WebSocketLoadEventData.playlist(
|
|
||||||
tracks: allTracks,
|
|
||||||
collection: props.collection as PlaylistSimple,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(initialTracks, autoPlay: true);
|
await playlistNotifier.load(allTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
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 {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
@ -8,7 +8,6 @@ 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/header/flexible_header.dart';
|
||||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.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/shared/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
|
|
||||||
class TrackView extends HookConsumerWidget {
|
class TrackView extends HookConsumerWidget {
|
||||||
const TrackView({super.key});
|
const TrackView({super.key});
|
||||||
@ -19,7 +18,7 @@ class TrackView extends HookConsumerWidget {
|
|||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: kIsDesktop
|
appBar: DesktopTools.platform.isDesktop
|
||||||
? const PageWindowTitleBar(
|
? const PageWindowTitleBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
@ -39,7 +39,7 @@ class PaginationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InheritedTrackView extends InheritedWidget {
|
class InheritedTrackView extends InheritedWidget {
|
||||||
final Object collection;
|
final String collectionId;
|
||||||
final String title;
|
final String title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final String image;
|
final String image;
|
||||||
@ -55,7 +55,7 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
const InheritedTrackView({
|
const InheritedTrackView({
|
||||||
super.key,
|
super.key,
|
||||||
required super.child,
|
required super.child,
|
||||||
required this.collection,
|
required this.collectionId,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.description,
|
this.description,
|
||||||
required this.image,
|
required this.image,
|
||||||
@ -65,11 +65,7 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
required this.shareUrl,
|
required this.shareUrl,
|
||||||
this.isLiked = false,
|
this.isLiked = false,
|
||||||
this.onHeart,
|
this.onHeart,
|
||||||
}) : assert(collection is AlbumSimple || collection is PlaylistSimple);
|
});
|
||||||
|
|
||||||
String get collectionId => collection is AlbumSimple
|
|
||||||
? (collection as AlbumSimple).id!
|
|
||||||
: (collection as PlaylistSimple).id!;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(InheritedTrackView oldWidget) {
|
bool updateShouldNotify(InheritedTrackView oldWidget) {
|
||||||
@ -82,7 +78,7 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
oldWidget.onHeart != onHeart ||
|
oldWidget.onHeart != onHeart ||
|
||||||
oldWidget.shareUrl != shareUrl ||
|
oldWidget.shareUrl != shareUrl ||
|
||||||
oldWidget.routePath != routePath ||
|
oldWidget.routePath != routePath ||
|
||||||
oldWidget.collection != collection ||
|
oldWidget.collectionId != collectionId ||
|
||||||
oldWidget.child != child;
|
oldWidget.child != child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ class Waypoint extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isMounted = useIsMounted();
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
return null;
|
return null;
|
||||||
@ -30,19 +32,19 @@ class Waypoint extends HookWidget {
|
|||||||
|
|
||||||
// scrollController fetches the next paginated data when the current
|
// scrollController fetches the next paginated data when the current
|
||||||
// position of the user on the screen has surpassed
|
// position of the user on the screen has surpassed
|
||||||
if (controller.position.pixels >= nextPageTrigger && context.mounted) {
|
if (controller.position.pixels >= nextPageTrigger && isMounted()) {
|
||||||
await onTouchEdge?.call();
|
await onTouchEdge?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (controller.hasClients && context.mounted) {
|
if (controller.hasClients && isMounted()) {
|
||||||
listener();
|
listener();
|
||||||
controller.addListener(listener);
|
controller.addListener(listener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => controller.removeListener(listener);
|
return () => controller.removeListener(listener);
|
||||||
}, [controller, onTouchEdge]);
|
}, [controller, onTouchEdge, isMounted]);
|
||||||
|
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/album/album_card.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/components/shared/links/artist_link.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/pages/album/album.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
class StatsAlbumItem extends StatelessWidget {
|
|
||||||
final AlbumSimple album;
|
|
||||||
final Widget info;
|
|
||||||
const StatsAlbumItem({super.key, required this.album, required this.info});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
horizontalTitleGap: 8,
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: UniversalImage(
|
|
||||||
path: (album.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(album.name!),
|
|
||||||
subtitle: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text("${album.albumType?.formatted} • "),
|
|
||||||
Flexible(
|
|
||||||
child: ArtistLink(
|
|
||||||
artists: album.artists ?? [],
|
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: info,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(
|
|
||||||
context,
|
|
||||||
AlbumPage.name,
|
|
||||||
pathParameters: {"id": album.id!},
|
|
||||||
extra: album,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
class StatsArtistItem extends StatelessWidget {
|
|
||||||
final Artist artist;
|
|
||||||
final Widget info;
|
|
||||||
const StatsArtistItem({
|
|
||||||
super.key,
|
|
||||||
required this.artist,
|
|
||||||
required this.info,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(artist.name!),
|
|
||||||
horizontalTitleGap: 8,
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
|
||||||
(artist.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.artist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: info,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(
|
|
||||||
context,
|
|
||||||
ArtistPage.name,
|
|
||||||
pathParameters: {"id": artist.id!},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
class StatsPlaylistItem extends StatelessWidget {
|
|
||||||
final PlaylistSimple playlist;
|
|
||||||
final Widget info;
|
|
||||||
const StatsPlaylistItem(
|
|
||||||
{super.key, required this.playlist, required this.info});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
horizontalTitleGap: 8,
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: UniversalImage(
|
|
||||||
path: (playlist.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
|
||||||
),
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(playlist.name!),
|
|
||||||
subtitle: Text(
|
|
||||||
playlist.description!.replaceAll(htmlTagRegexp, ''),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
trailing: info,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(
|
|
||||||
context,
|
|
||||||
PlaylistPage.name,
|
|
||||||
pathParameters: {"id": playlist.id!},
|
|
||||||
extra: playlist,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/components/shared/links/artist_link.dart';
|
|
||||||
import 'package:spotube/extensions/image.dart';
|
|
||||||
import 'package:spotube/pages/track/track.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
class StatsTrackItem extends StatelessWidget {
|
|
||||||
final Track track;
|
|
||||||
final Widget info;
|
|
||||||
const StatsTrackItem({
|
|
||||||
super.key,
|
|
||||||
required this.track,
|
|
||||||
required this.info,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
horizontalTitleGap: 8,
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: UniversalImage(
|
|
||||||
path: (track.album?.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(track.name!),
|
|
||||||
subtitle: ArtistLink(
|
|
||||||
artists: track.artists!,
|
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
|
||||||
),
|
|
||||||
trailing: info,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(
|
|
||||||
context,
|
|
||||||
TrackPage.name,
|
|
||||||
pathParameters: {
|
|
||||||
"id": track.id!,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/collections/formatters.dart';
|
|
||||||
import 'package:spotube/components/stats/summary/summary_card.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.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/streams/streams.dart';
|
|
||||||
import 'package:spotube/provider/history/summary.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
|
|
||||||
class StatsPageSummarySection extends HookConsumerWidget {
|
|
||||||
const StatsPageSummarySection({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final summary = ref.watch(playbackHistorySummaryProvider);
|
|
||||||
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
|
||||||
return SliverGrid(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: constrains.isXs
|
|
||||||
? 2
|
|
||||||
: constrains.smAndDown
|
|
||||||
? 3
|
|
||||||
: constrains.mdAndDown
|
|
||||||
? 4
|
|
||||||
: constrains.lgAndDown
|
|
||||||
? 5
|
|
||||||
: 6,
|
|
||||||
mainAxisSpacing: 10,
|
|
||||||
crossAxisSpacing: 10,
|
|
||||||
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
|
|
||||||
),
|
|
||||||
delegate: SliverChildListDelegate([
|
|
||||||
SummaryCard(
|
|
||||||
title: summary.duration.inMinutes.toDouble(),
|
|
||||||
unit: "minutes",
|
|
||||||
description: 'Listened to music',
|
|
||||||
color: Colors.purple,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SummaryCard(
|
|
||||||
title: summary.tracks.toDouble(),
|
|
||||||
unit: "songs",
|
|
||||||
description: 'Streamed overall',
|
|
||||||
color: Colors.lightBlue,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SummaryCard.unformatted(
|
|
||||||
title: usdFormatter.format(summary.fees.toDouble()),
|
|
||||||
unit: "",
|
|
||||||
description: 'Owed to artists\nthis month',
|
|
||||||
color: Colors.green,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SummaryCard(
|
|
||||||
title: summary.artists.toDouble(),
|
|
||||||
unit: "artist's",
|
|
||||||
description: 'Music reached you',
|
|
||||||
color: Colors.yellow,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SummaryCard(
|
|
||||||
title: summary.albums.toDouble(),
|
|
||||||
unit: "full albums",
|
|
||||||
description: 'Got your love',
|
|
||||||
color: Colors.pink,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SummaryCard(
|
|
||||||
title: summary.playlists.toDouble(),
|
|
||||||
unit: "playlists",
|
|
||||||
description: 'Were on repeat',
|
|
||||||
color: Colors.teal,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:spotube/collections/formatters.dart';
|
|
||||||
|
|
||||||
class SummaryCard extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String unit;
|
|
||||||
final String description;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
final MaterialColor color;
|
|
||||||
|
|
||||||
SummaryCard({
|
|
||||||
super.key,
|
|
||||||
required double title,
|
|
||||||
required this.unit,
|
|
||||||
required this.description,
|
|
||||||
required this.color,
|
|
||||||
this.onTap,
|
|
||||||
}) : title = compactNumberFormatter.format(title);
|
|
||||||
|
|
||||||
const SummaryCard.unformatted({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.unit,
|
|
||||||
required this.description,
|
|
||||||
required this.color,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData(:textTheme, :brightness) = Theme.of(context);
|
|
||||||
|
|
||||||
final descriptionNewLines = description.split("").where((s) => s == "\n");
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
color: brightness == Brightness.dark ? color.shade100 : color.shade50,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
onTap: onTap,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
AutoSizeText.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: title,
|
|
||||||
style: textTheme.headlineLarge?.copyWith(
|
|
||||||
color: color.shade900,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: " $unit",
|
|
||||||
style: textTheme.titleMedium?.copyWith(
|
|
||||||
color: color.shade900,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
const Gap(5),
|
|
||||||
AutoSizeText(
|
|
||||||
description,
|
|
||||||
maxLines: description.contains("\n")
|
|
||||||
? descriptionNewLines.length + 1
|
|
||||||
: 1,
|
|
||||||
minFontSize: 9,
|
|
||||||
style: textTheme.labelMedium!.copyWith(
|
|
||||||
color: color.shade900,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/collections/formatters.dart';
|
|
||||||
import 'package:spotube/components/stats/common/album_item.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
|
||||||
|
|
||||||
class TopAlbums extends HookConsumerWidget {
|
|
||||||
const TopAlbums({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
|
||||||
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
|
||||||
.select((value) => value.albums));
|
|
||||||
|
|
||||||
return SliverList.builder(
|
|
||||||
itemCount: albums.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final album = albums[index];
|
|
||||||
return StatsAlbumItem(
|
|
||||||
album: album.album,
|
|
||||||
info: Text(
|
|
||||||
"${compactNumberFormatter.format(album.count)} plays",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/collections/formatters.dart';
|
|
||||||
import 'package:spotube/components/stats/common/artist_item.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
|
||||||
|
|
||||||
class TopArtists extends HookConsumerWidget {
|
|
||||||
const TopArtists({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
|
||||||
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
|
||||||
.select((value) => value.artists));
|
|
||||||
|
|
||||||
return SliverList.builder(
|
|
||||||
itemCount: artists.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final artist = artists[index];
|
|
||||||
return StatsArtistItem(
|
|
||||||
artist: artist.artist,
|
|
||||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/components/shared/themed_button_tab_bar.dart';
|
|
||||||
import 'package:spotube/components/stats/top/albums.dart';
|
|
||||||
import 'package:spotube/components/stats/top/artists.dart';
|
|
||||||
import 'package:spotube/components/stats/top/tracks.dart';
|
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
|
||||||
|
|
||||||
class StatsPageTopSection extends HookConsumerWidget {
|
|
||||||
const StatsPageTopSection({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final tabController = useTabController(initialLength: 3);
|
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
|
||||||
final historyDurationNotifier =
|
|
||||||
ref.watch(playbackHistoryTopDurationProvider.notifier);
|
|
||||||
|
|
||||||
return SliverMainAxisGroup(
|
|
||||||
slivers: [
|
|
||||||
SliverAppBar(
|
|
||||||
floating: true,
|
|
||||||
flexibleSpace: ThemedButtonsTabBar(
|
|
||||||
controller: tabController,
|
|
||||||
tabs: const [
|
|
||||||
Tab(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: Text("Top Tracks"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: Text("Top Artists"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: Text("Top Albums"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: DropdownButton(
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!,
|
|
||||||
isDense: true,
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
underline: const SizedBox(),
|
|
||||||
value: historyDuration,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
historyDurationNotifier.update((_) => value);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.days7,
|
|
||||||
child: Text("This week"),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.days30,
|
|
||||||
child: Text("This month"),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.months6,
|
|
||||||
child: Text("Last 6 months"),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.year,
|
|
||||||
child: Text("This year"),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.years2,
|
|
||||||
child: Text("Last 2 years"),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: HistoryDuration.allTime,
|
|
||||||
child: Text("All time"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListenableBuilder(
|
|
||||||
listenable: tabController,
|
|
||||||
builder: (context, _) {
|
|
||||||
return switch (tabController.index) {
|
|
||||||
1 => const TopArtists(),
|
|
||||||
2 => const TopAlbums(),
|
|
||||||
_ => const TopTracks(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/collections/formatters.dart';
|
|
||||||
import 'package:spotube/components/stats/common/track_item.dart';
|
|
||||||
import 'package:spotube/provider/history/top.dart';
|
|
||||||
|
|
||||||
class TopTracks extends HookConsumerWidget {
|
|
||||||
const TopTracks({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
|
||||||
final tracks = ref.watch(
|
|
||||||
playbackHistoryTopProvider(historyDuration)
|
|
||||||
.select((value) => value.tracks),
|
|
||||||
);
|
|
||||||
|
|
||||||
return SliverList.builder(
|
|
||||||
itemCount: tracks.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final track = tracks[index];
|
|
||||||
return StatsTrackItem(
|
|
||||||
track: track.track,
|
|
||||||
info: Text(
|
|
||||||
"${compactNumberFormatter.format(track.count)} plays",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,21 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
extension AlbumExtensions on AlbumSimple {
|
extension AlbumExtensions on AlbumSimple {
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"albumType": albumType?.name,
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"images": images
|
||||||
|
?.map((image) => {
|
||||||
|
"height": image.height,
|
||||||
|
"url": image.url,
|
||||||
|
"width": image.width,
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Album toAlbum() {
|
Album toAlbum() {
|
||||||
Album album = Album();
|
Album album = Album();
|
||||||
album.albumType = albumType;
|
album.albumType = albumType;
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
extension ArtistJson on ArtistSimple {
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"href": href,
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"type": type,
|
||||||
|
"uri": uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ArtistExtension on List<ArtistSimple> {
|
extension ArtistExtension on List<ArtistSimple> {
|
||||||
String asString() {
|
String asString() {
|
||||||
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||||
|
@ -3,6 +3,8 @@ import 'dart:io';
|
|||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/extensions/album_simple.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
extension TrackExtensions on Track {
|
extension TrackExtensions on Track {
|
||||||
@ -37,6 +39,33 @@ extension TrackExtensions on Track {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return TrackExtensions.trackToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> trackToJson(Track track) {
|
||||||
|
return {
|
||||||
|
"album": track.album?.toJson(),
|
||||||
|
"artists": track.artists?.map((artist) => artist.toJson()).toList(),
|
||||||
|
"available_markets": track.availableMarkets?.map((e) => e.name).toList(),
|
||||||
|
"disc_number": track.discNumber,
|
||||||
|
"duration_ms": track.durationMs,
|
||||||
|
"explicit": track.explicit,
|
||||||
|
// "external_ids"track.: externalIds,
|
||||||
|
// "external_urls"track.: externalUrls,
|
||||||
|
"href": track.href,
|
||||||
|
"id": track.id,
|
||||||
|
"is_playable": track.isPlayable,
|
||||||
|
// "linked_from"track.: linkedFrom,
|
||||||
|
"name": track.name,
|
||||||
|
"popularity": track.popularity,
|
||||||
|
"preview_rrl": track.previewUrl,
|
||||||
|
"track_number": track.trackNumber,
|
||||||
|
"type": track.type,
|
||||||
|
"uri": track.uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrackSimpleExtensions on TrackSimple {
|
extension TrackSimpleExtensions on TrackSimple {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user