mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-08 00:17:29 +00:00
Merge branch 'dev' into master
This commit is contained in:
commit
55a47e27f3
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
build
|
||||
dist
|
||||
.dart_tool
|
||||
.idea
|
||||
.github
|
||||
.git
|
||||
@ -9,3 +9,6 @@ ENABLE_UPDATE_CHECK=
|
||||
|
||||
LASTFM_API_KEY=
|
||||
LASTFM_API_SECRET=
|
||||
|
||||
# Release channel. Can be: nightly, stable
|
||||
RELEASE_CHANNEL=
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.19.1",
|
||||
"flutterSdkVersion": "3.19.5",
|
||||
"flavors": {}
|
||||
}
|
||||
23
.github/Dockerfile
vendored
Normal file
23
.github/Dockerfile
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
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
Normal file
23
.github/Dockerfile.flutter_distributor
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
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/spotube-publish-binary.yml
vendored
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to publish (x.x.x)
|
||||
default: 3.1.0
|
||||
default: 3.6.0
|
||||
required: true
|
||||
dry_run:
|
||||
description: Dry run
|
||||
|
||||
445
.github/workflows/spotube-release-binary.yml
vendored
445
.github/workflows/spotube-release-binary.yml
vendored
@ -2,279 +2,108 @@ name: Spotube Release Binary
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release (x.x.x)
|
||||
default: 3.6.0
|
||||
required: true
|
||||
channel:
|
||||
type: choice
|
||||
description: Release Channel
|
||||
required: true
|
||||
options:
|
||||
- stable
|
||||
- nightly
|
||||
default: nightly
|
||||
description: The release channel
|
||||
debug:
|
||||
description: Debug on failed when channel is nightly
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
description: Debug with SSH toggle
|
||||
required: false
|
||||
dry_run:
|
||||
description: Dry run
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
default: false
|
||||
description: Dry run without uploading to release
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: '3.19.1'
|
||||
FLUTTER_VERSION: 3.19.5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
build_platform:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
files: |
|
||||
dist/Spotube-linux-x86_64.deb
|
||||
dist/Spotube-linux-x86_64.rpm
|
||||
dist/spotube-linux-*-x86_64.tar.xz
|
||||
- os: ubuntu-latest
|
||||
platform: linux_arm
|
||||
files: |
|
||||
dist/Spotube-linux-aarch64.deb
|
||||
dist/spotube-linux-*-aarch64.tar.xz
|
||||
- os: ubuntu-latest
|
||||
platform: android
|
||||
files: |
|
||||
build/Spotube-android-all-arch.apk
|
||||
build/Spotube-playstore-all-arch.aab
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
files: |
|
||||
dist/Spotube-windows-x86_64.nupkg
|
||||
dist/Spotube-windows-x86_64-setup.exe
|
||||
- os: macos-latest
|
||||
platform: ios
|
||||
files: |
|
||||
Spotube-iOS.ipa
|
||||
- os: macos-14
|
||||
platform: macos
|
||||
files: |
|
||||
build/Spotube-macos-universal.dmg
|
||||
build/Spotube-macos-universal.pkg
|
||||
runs-on: ${{matrix.os}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2.12.0
|
||||
with:
|
||||
cache: true
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
||||
if: ${{ inputs.channel == 'nightly' }}
|
||||
run: |
|
||||
choco install sed make yq -y
|
||||
yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
|
||||
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
|
||||
"BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $env:GITHUB_ENV
|
||||
|
||||
- name: BUILD_VERSION Env (stable)
|
||||
if: ${{ inputs.channel == 'stable' }}
|
||||
run: |
|
||||
"BUILD_VERSION=${{ inputs.version }}" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Replace version in files
|
||||
run: |
|
||||
choco install sed make -y
|
||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" windows/runner/Runner.rc
|
||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/tools/VERIFICATION.txt
|
||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/spotube.nuspec
|
||||
|
||||
- name: Create Stable .env
|
||||
if: ${{ inputs.channel == 'stable' }}
|
||||
run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
|
||||
|
||||
- name: Create Nightly .env
|
||||
if: ${{ inputs.channel == 'nightly' }}
|
||||
run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
|
||||
|
||||
- name: Generating Secrets
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
flutter pub get
|
||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||
|
||||
- name: Build Windows Executable
|
||||
run: |
|
||||
dart pub global activate flutter_distributor
|
||||
make innoinstall
|
||||
flutter_distributor package --platform=windows --targets=exe --skip-clean
|
||||
mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
|
||||
|
||||
- name: Create Chocolatey Package and set hash
|
||||
if: ${{ inputs.channel == 'stable' }}
|
||||
run: |
|
||||
Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash
|
||||
sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt
|
||||
make choco
|
||||
mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg
|
||||
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Setup Java
|
||||
if: ${{matrix.platform == 'android'}}
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
path: |
|
||||
dist/Spotube-windows-x86_64.nupkg
|
||||
dist/Spotube-windows-x86_64-setup.exe
|
||||
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: Debug With SSH When fails
|
||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2.12.0
|
||||
with:
|
||||
cache: true
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev
|
||||
|
||||
- name: Install AppImage Tool
|
||||
run: |
|
||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x appimagetool
|
||||
mv appimagetool /usr/local/bin/
|
||||
|
||||
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
||||
if: ${{ inputs.channel == 'nightly' }}
|
||||
run: |
|
||||
curl -sS https://webi.sh/yq | sh
|
||||
yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
|
||||
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
|
||||
echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: BUILD_VERSION Env (stable)
|
||||
if: ${{ inputs.channel == 'stable' }}
|
||||
run: |
|
||||
echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Stable .env
|
||||
if: ${{ inputs.channel == 'stable' }}
|
||||
run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
|
||||
|
||||
- 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
|
||||
- name: Install ${{matrix.platform}} dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||
dart cli/cli.dart install-dependencies --platform=${{matrix.platform}}
|
||||
|
||||
- name: Sign Apk
|
||||
if: ${{matrix.platform == 'android'}}
|
||||
run: |
|
||||
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
|
||||
echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
|
||||
|
||||
- name: Build Apk
|
||||
run: |
|
||||
flutter build apk --flavor ${{ inputs.channel }}
|
||||
mv build/app/outputs/flutter-apk/app-${{ inputs.channel }}-release.apk build/Spotube-android-all-arch.apk
|
||||
|
||||
- name: Build Playstore AppBundle
|
||||
run: |
|
||||
echo 'ENABLE_UPDATE_CHECK=0' >> .env
|
||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
||||
export MANIFEST=android/app/src/main/AndroidManifest.xml
|
||||
xmlstarlet ed -d '//meta-data[@android:name="com.google.android.gms.car.application"]' $MANIFEST > $MANIFEST.tmp
|
||||
mv $MANIFEST.tmp $MANIFEST
|
||||
flutter build appbundle --flavor ${{ inputs.channel }}
|
||||
mv build/app/outputs/bundle/${{ inputs.channel }}Release/app-${{ inputs.channel }}-release.aab build/Spotube-playstore-all-arch.aab
|
||||
|
||||
|
||||
|
||||
- name: Build ${{matrix.platform}} binaries
|
||||
run: dart cli/cli.dart build ${{matrix.platform}}
|
||||
env:
|
||||
CHANNEL: ${{inputs.channel}}
|
||||
DOTENV: ${{secrets.DOTENV_RELEASE}}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
path: |
|
||||
build/Spotube-android-all-arch.apk
|
||||
build/Spotube-playstore-all-arch.aab
|
||||
path: ${{matrix.files}}
|
||||
|
||||
- name: Debug With SSH When fails
|
||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||
@ -282,135 +111,10 @@ jobs:
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs:
|
||||
- windows
|
||||
- linux
|
||||
- android
|
||||
- macos
|
||||
- iOS
|
||||
- build_platform
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
@ -426,6 +130,10 @@ jobs:
|
||||
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
|
||||
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
||||
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
||||
|
||||
- name: Extract pubspec version
|
||||
run: |
|
||||
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@ -440,7 +148,7 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: v${{ inputs.version }} # mind the "v" prefix
|
||||
tag: v${{ env.PUBSPEC_VERSION }} # mind the "v" prefix
|
||||
omitBodyDuringUpdate: true
|
||||
omitNameDuringUpdate: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
@ -458,3 +166,8 @@ jobs:
|
||||
omitPrereleaseDuringUpdate: true
|
||||
allowUpdates: true
|
||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||
body: |
|
||||
Build Number: ${{github.run_number}}
|
||||
|
||||
Nightly release includes newest features but may contain bugs
|
||||
It is preferred to use the stable version unless you know what you're doing
|
||||
|
||||
16
.metadata
16
.metadata
@ -1,11 +1,11 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
channel: stable
|
||||
revision: "300451adae589accbece3490f4396f10bdf15e6e"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
@ -13,11 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
- platform: macos
|
||||
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
- platform: windows
|
||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -24,5 +24,6 @@
|
||||
"explorer.fileNesting.patterns": {
|
||||
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
|
||||
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
||||
"*.dart": "${capture}.g.dart,${capture}.freezed.dart",
|
||||
}
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:pub_api_client/pub_api_client.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
|
||||
void main() async {
|
||||
final client = PubClient();
|
||||
|
||||
final pubspec = Pubspec.parse(File('pubspec.yaml').readAsStringSync());
|
||||
|
||||
final allDeps = [
|
||||
...pubspec.dependencies.entries,
|
||||
...pubspec.devDependencies.entries,
|
||||
];
|
||||
|
||||
final dependencies = allDeps
|
||||
.where((d) => d.value is HostedDependency)
|
||||
.map((d) => d.key)
|
||||
.toSet();
|
||||
final packageInfo = await Future.wait(dependencies.map(client.packageInfo));
|
||||
|
||||
final gitDepsList = List.castFrom<MapEntry<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);
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// Generate JSON output for untranslated messages with English values
|
||||
/// for quick translation in ChatGPT
|
||||
///
|
||||
/// Usage: dart bin/untranslated_messages.dart [locale?]
|
||||
///
|
||||
/// Example: dart bin/untranslated_messages.dart
|
||||
///
|
||||
/// or with specific locale (e.g. bn (Bengali))
|
||||
///
|
||||
/// Example: dart bin/untranslated_messages.dart bn
|
||||
|
||||
void main(List<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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
Process.run("sh", ["-c", '"./scripts/pkgbuild2json.sh aur-struct/PKGBUILD"'])
|
||||
.then((result) {
|
||||
try {
|
||||
final pkgbuild = jsonDecode(result.stdout);
|
||||
if (pkgbuild["version"] !=
|
||||
Platform.environment["RELEASE_VERSION"]?.substring(1)) {
|
||||
throw Exception(
|
||||
"PKGBUILD version doesn't match current RELEASE_VERSION");
|
||||
}
|
||||
if (pkgbuild["release"] != "1") {
|
||||
throw Exception("In new releases pkgrel should be 1");
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print("[Failed to parse PKGBUILD] $e");
|
||||
}
|
||||
});
|
||||
}
|
||||
4
cli/README.md
Normal file
4
cli/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
## Spotube Configuration CLI
|
||||
|
||||
This is used for building the project for multiple platforms and having utilities specific for the project.
|
||||
Written in Dart
|
||||
22
cli/cli.dart
Normal file
22
cli/cli.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
|
||||
import 'commands/build.dart';
|
||||
import 'commands/credits.dart';
|
||||
import 'commands/install-dependencies.dart';
|
||||
import 'commands/translated.dart';
|
||||
import 'commands/untranslated.dart';
|
||||
|
||||
void main(List<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);
|
||||
}
|
||||
25
cli/commands/build.dart
Normal file
25
cli/commands/build.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
|
||||
import 'build/android.dart';
|
||||
import 'build/ios.dart';
|
||||
import 'build/linux.dart';
|
||||
import 'build/linux_arm.dart';
|
||||
import 'build/macos.dart';
|
||||
import 'build/windows.dart';
|
||||
|
||||
class BuildCommand extends Command {
|
||||
@override
|
||||
String get description => "Build for different platforms";
|
||||
|
||||
@override
|
||||
String get name => "build";
|
||||
|
||||
BuildCommand() {
|
||||
addSubcommand(AndroidBuildCommand());
|
||||
addSubcommand(IosBuildCommand());
|
||||
addSubcommand(LinuxBuildCommand());
|
||||
addSubcommand(LinuxArmBuildCommand());
|
||||
addSubcommand(MacosBuildCommand());
|
||||
addSubcommand(WindowsBuildCommand());
|
||||
}
|
||||
}
|
||||
90
cli/commands/build/android.dart
Normal file
90
cli/commands/build/android.dart
Normal file
@ -0,0 +1,90 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
|
||||
class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "Build for android";
|
||||
|
||||
@override
|
||||
String get name => "android";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
await bootstrap();
|
||||
|
||||
await shell.run(
|
||||
"flutter build apk --flavor ${CliEnv.channel.name}",
|
||||
);
|
||||
|
||||
await dotEnvFile.writeAsString(
|
||||
"\nENABLE_UPDATE_CHECK=0",
|
||||
mode: FileMode.append,
|
||||
);
|
||||
|
||||
final androidManifestFile = File(
|
||||
join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml"));
|
||||
|
||||
final androidManifestXml =
|
||||
XmlDocument.parse(await androidManifestFile.readAsString());
|
||||
|
||||
final deletingElement =
|
||||
androidManifestXml.findAllElements("meta-data").firstWhereOrNull(
|
||||
(el) =>
|
||||
el.getAttribute("android:name") ==
|
||||
"com.google.android.gms.car.application",
|
||||
);
|
||||
|
||||
deletingElement?.parent?.children.remove(deletingElement);
|
||||
|
||||
await androidManifestFile.writeAsString(
|
||||
androidManifestXml.toXmlString(pretty: true),
|
||||
);
|
||||
|
||||
await shell.run(
|
||||
"""
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter build appbundle --flavor ${CliEnv.channel.name}
|
||||
""",
|
||||
);
|
||||
|
||||
final ogApkFile = File(
|
||||
join(
|
||||
"build",
|
||||
"app",
|
||||
"outputs",
|
||||
"flutter-apk",
|
||||
"app-${CliEnv.channel.name}-release.apk",
|
||||
),
|
||||
);
|
||||
|
||||
await ogApkFile.copy(
|
||||
join(cwd.path, "build", "Spotube-android-all-arch.apk"),
|
||||
);
|
||||
|
||||
final ogAppbundleFile = File(
|
||||
join(
|
||||
cwd.path,
|
||||
"build",
|
||||
"app",
|
||||
"outputs",
|
||||
"bundle",
|
||||
"${CliEnv.channel.name}Release",
|
||||
"app-${CliEnv.channel.name}-release.aab",
|
||||
),
|
||||
);
|
||||
|
||||
await ogAppbundleFile.copy(
|
||||
join(cwd.path, "build", "Spotube-playstore-all-arch.aab"),
|
||||
);
|
||||
|
||||
stdout.writeln("✅ Built Android Apk and Appbundle");
|
||||
}
|
||||
}
|
||||
66
cli/commands/build/common.dart
Normal file
66
cli/commands/build/common.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:process_run/shell_run.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
|
||||
mixin BuildCommandCommonSteps on Command {
|
||||
final shell = Shell();
|
||||
Directory get cwd => Directory.current;
|
||||
|
||||
Pubspec? _pubspec;
|
||||
|
||||
Pubspec get pubspec {
|
||||
if (_pubspec != null) {
|
||||
return _pubspec!;
|
||||
}
|
||||
|
||||
final pubspecFile = File(join(cwd.path, "pubspec.yaml"));
|
||||
_pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
|
||||
|
||||
return _pubspec!;
|
||||
}
|
||||
|
||||
String get versionWithoutBuildNumber {
|
||||
return "${pubspec.version!.major}.${pubspec.version!.minor}.${pubspec.version!.patch}";
|
||||
}
|
||||
|
||||
RegExp get versionVarRegExp =>
|
||||
RegExp(r"\%\{\{SPOTUBE_VERSION\}\}\%", multiLine: true);
|
||||
|
||||
File get dotEnvFile => File(join(cwd.path, ".env"));
|
||||
|
||||
Future<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
|
||||
""",
|
||||
);
|
||||
}
|
||||
}
|
||||
29
cli/commands/build/ios.dart
Normal file
29
cli/commands/build/ios.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
|
||||
class IosBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "iOS build command";
|
||||
|
||||
@override
|
||||
String get name => "ios";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
await bootstrap();
|
||||
|
||||
final buildDirPath = join(cwd.path, "build", "ios", "iphoneos");
|
||||
await shell.run(
|
||||
"""
|
||||
flutter build ios --release --no-codesign --flavor ${CliEnv.channel.name}
|
||||
ln -sf $buildDirPath Payload
|
||||
zip -r9 Spotube-iOS.ipa ${join("Payload", "${CliEnv.channel.name}.app")}
|
||||
""",
|
||||
);
|
||||
}
|
||||
}
|
||||
106
cli/commands/build/linux.dart
Normal file
106
cli/commands/build/linux.dart
Normal file
@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:io/io.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
|
||||
class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "Linux build command";
|
||||
|
||||
@override
|
||||
String get name => "linux";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
stdout.writeln("Replacing versions");
|
||||
|
||||
final appDataFile = File(
|
||||
join(cwd.path, "linux", "com.github.KRTirtho.Spotube.appdata.xml"),
|
||||
);
|
||||
|
||||
appDataFile.writeAsStringSync(
|
||||
appDataFile.readAsStringSync().replaceAll(
|
||||
versionVarRegExp,
|
||||
'<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");
|
||||
}
|
||||
}
|
||||
37
cli/commands/build/linux_arm.dart
Normal file
37
cli/commands/build/linux_arm.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
|
||||
class LinuxArmBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "Build Linux Arm";
|
||||
|
||||
@override
|
||||
String get name => "linux_arm";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
await bootstrap();
|
||||
|
||||
await shell.run(
|
||||
"docker buildx build --platform=linux/arm64 "
|
||||
"-f ${join(cwd.path, ".github", "Dockerfile")} ${cwd.path} "
|
||||
"--build-arg FLUTTER_VERSION=${CliEnv.flutterVersion} "
|
||||
"--build-arg BUILD_VERSION=${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber} "
|
||||
"-t krtirtho/spotube_linux_arm:latest "
|
||||
"--load",
|
||||
);
|
||||
|
||||
await shell.run(
|
||||
"""
|
||||
docker images ls
|
||||
docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest
|
||||
docker cp spotube_linux_arm:/app/dist/ dist/
|
||||
""",
|
||||
);
|
||||
}
|
||||
}
|
||||
42
cli/commands/build/macos.dart
Normal file
42
cli/commands/build/macos.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
class MacosBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "Macos Build command";
|
||||
|
||||
@override
|
||||
String get name => "macos";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
await bootstrap();
|
||||
|
||||
await shell.run(
|
||||
"""
|
||||
flutter build macos
|
||||
appdmg appdmg.json ${join(cwd.path, "build", "Spotube-macos-universal.dmg")}
|
||||
flutter_distributor package --platform=macos --targets pkg --skip-clean
|
||||
""",
|
||||
);
|
||||
|
||||
final ogPkg = File(
|
||||
join(
|
||||
cwd.path,
|
||||
"dist",
|
||||
pubspec.version.toString(),
|
||||
"spotube-${pubspec.version}-macos.pkg",
|
||||
),
|
||||
);
|
||||
|
||||
await ogPkg.copy(
|
||||
join(cwd.path, "build", "Spotube-macos-universal.pkg"),
|
||||
);
|
||||
await ogPkg.delete();
|
||||
}
|
||||
}
|
||||
100
cli/commands/build/windows.dart
Normal file
100
cli/commands/build/windows.dart
Normal file
@ -0,0 +1,100 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
114
cli/commands/credits.dart
Normal file
114
cli/commands/credits.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.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 {
|
||||
@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(res) {
|
||||
try {
|
||||
return Pubspec.parse(res.body);
|
||||
} catch (e) {
|
||||
final document = parse(res.body);
|
||||
final pre = document.querySelector('pre');
|
||||
if (pre == null) {
|
||||
stdout.writeln(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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
cli/commands/install-dependencies.dart
Normal file
74
cli/commands/install-dependencies.dart
Normal file
@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
cli/commands/translated.dart
Normal file
39
cli/commands/translated.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class TranslatedCommand extends Command {
|
||||
@override
|
||||
String get description =>
|
||||
"Update translation based on generated translated messages";
|
||||
|
||||
@override
|
||||
String get name => "translated";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
final cwd = Directory.current;
|
||||
final translatedFile = jsonDecode(
|
||||
await File(join(cwd.path, 'tm.json')).readAsString(),
|
||||
) as Map<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');
|
||||
}
|
||||
}
|
||||
}
|
||||
48
cli/commands/untranslated.dart
Normal file
48
cli/commands/untranslated.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class UntranslatedCommand extends Command {
|
||||
@override
|
||||
get name => "untranslated";
|
||||
@override
|
||||
get description =>
|
||||
"Generate Untranslated Messages for ChatGPT based Translation";
|
||||
|
||||
@override
|
||||
run() async {
|
||||
final cwd = Directory.current;
|
||||
final file = jsonDecode(
|
||||
File(join(cwd.path, 'untranslated_messages.json')).readAsStringSync(),
|
||||
) as Map<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),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
cli/core/env.dart
Normal file
24
cli/core/env.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'dart:io';
|
||||
|
||||
enum BuildChannel {
|
||||
stable,
|
||||
nightly;
|
||||
|
||||
factory BuildChannel.fromEnvironment(String name) {
|
||||
final channel = Platform.environment[name]!;
|
||||
if (channel == "stable") {
|
||||
return BuildChannel.stable;
|
||||
} else if (channel == "nightly") {
|
||||
return BuildChannel.nightly;
|
||||
} else {
|
||||
throw Exception("Invalid channel: $channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CliEnv {
|
||||
static final channel = BuildChannel.fromEnvironment("CHANNEL");
|
||||
static final dotenv = Platform.environment["DOTENV"]!;
|
||||
static final ghRunNumber = Platform.environment["GITHUB_RUN_NUMBER"];
|
||||
static final flutterVersion = Platform.environment["FLUTTER_VERSION"]!;
|
||||
}
|
||||
@ -1,8 +1,13 @@
|
||||
import 'package:envied/envied.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
enum ReleaseChannel {
|
||||
nightly,
|
||||
stable,
|
||||
}
|
||||
|
||||
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
||||
abstract class Env {
|
||||
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
||||
@ -25,8 +30,15 @@ abstract class Env {
|
||||
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
|
||||
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
|
||||
|
||||
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
||||
static final String _releaseChannel = _Env._releaseChannel;
|
||||
|
||||
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
||||
? ReleaseChannel.stable
|
||||
: ReleaseChannel.nightly;
|
||||
|
||||
static bool get enableUpdateChecker =>
|
||||
DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1";
|
||||
kIsFlatpak || _enableUpdateChecker == "1";
|
||||
|
||||
static String discordAppId = "1176718791388975124";
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
|
||||
Future<void> registerWindowsScheme(String scheme) async {
|
||||
if (!DesktopTools.platform.isWindows) return;
|
||||
if (!kIsWindows) return;
|
||||
String appPath = Platform.resolvedExecutable;
|
||||
|
||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||
|
||||
@ -81,10 +81,10 @@ abstract class LanguageLocals {
|
||||
// name: "Bashkir",
|
||||
// nativeName: "башҡорт теле",
|
||||
// ),
|
||||
// "eu": const ISOLanguageName(
|
||||
// name: "Basque",
|
||||
// nativeName: "euskara,",
|
||||
// ),
|
||||
"eu": const ISOLanguageName(
|
||||
name: "Basque",
|
||||
nativeName: "euskara",
|
||||
),
|
||||
// "be": const ISOLanguageName(
|
||||
// name: "Belarusian",
|
||||
// nativeName: "Беларуская",
|
||||
@ -197,10 +197,10 @@ abstract class LanguageLocals {
|
||||
// name: "Fijian",
|
||||
// nativeName: "vosa Vakaviti",
|
||||
// ),
|
||||
// "fi": const ISOLanguageName(
|
||||
// name: "Finnish",
|
||||
// nativeName: "suomi",
|
||||
// ),
|
||||
"fi": const ISOLanguageName(
|
||||
name: "Finnish",
|
||||
nativeName: "suomi",
|
||||
),
|
||||
"fr": const ISOLanguageName(
|
||||
name: "French",
|
||||
nativeName: "français",
|
||||
@ -213,10 +213,10 @@ abstract class LanguageLocals {
|
||||
// name: "Galician",
|
||||
// nativeName: "Galego",
|
||||
// ),
|
||||
// "ka": const ISOLanguageName(
|
||||
// name: "Georgian",
|
||||
// nativeName: "ქართული",
|
||||
// ),
|
||||
"ka": const ISOLanguageName(
|
||||
name: "Georgian",
|
||||
nativeName: "ქართული",
|
||||
),
|
||||
"de": const ISOLanguageName(
|
||||
name: "German",
|
||||
nativeName: "Deutsch",
|
||||
@ -265,10 +265,10 @@ abstract class LanguageLocals {
|
||||
// name: "Interlingua",
|
||||
// nativeName: "Interlingua",
|
||||
// ),
|
||||
// "id": const ISOLanguageName(
|
||||
// name: "Indonesian",
|
||||
// nativeName: "Bahasa Indonesia",
|
||||
// ),
|
||||
"id": const ISOLanguageName(
|
||||
name: "Indonesian",
|
||||
nativeName: "Bahasa Indonesia",
|
||||
),
|
||||
// "ie": const ISOLanguageName(
|
||||
// name: "Interlingue",
|
||||
// nativeName: "Occidental",
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||
import 'package:spotube/pages/home/genres/genres.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
||||
import 'package:spotube/pages/library/local_folder.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
||||
@ -113,6 +114,17 @@ final routerProvider = Provider((ref) {
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: "local",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is String);
|
||||
return SpotubePage(
|
||||
child: LocalLibraryPage(state.extra as String,
|
||||
isDownloads: state.uri.queryParameters["downloads"] != null
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: "/lyrics",
|
||||
|
||||
@ -121,4 +121,6 @@ abstract class SpotubeIcons {
|
||||
static const monitor = FeatherIcons.monitor;
|
||||
static const power = FeatherIcons.power;
|
||||
static const bluetooth = FeatherIcons.bluetooth;
|
||||
static const folderAdd = FeatherIcons.folderPlus;
|
||||
static const folderRemove = FeatherIcons.folderMinus;
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ class TokenLoginForm extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
||||
final directCodeController = useTextEditingController();
|
||||
final mounted = useIsMounted();
|
||||
|
||||
final isLoading = useState(false);
|
||||
|
||||
@ -57,7 +56,7 @@ class TokenLoginForm extends HookConsumerWidget {
|
||||
await AuthenticationCredentials.fromCookie(
|
||||
cookieHeader),
|
||||
);
|
||||
if (mounted()) {
|
||||
if (context.mounted) {
|
||||
onDone?.call();
|
||||
}
|
||||
} finally {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/home/sections/friends/friend_item.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
import 'package:spotube/models/spotify_friends.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
class HomePageFriendsSection extends HookConsumerWidget {
|
||||
@ -14,6 +16,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
final friendsQuery = ref.watch(friendsProvider);
|
||||
final friends =
|
||||
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
|
||||
@ -27,32 +30,36 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
xxl: 7,
|
||||
);
|
||||
|
||||
final friendGroup = friends.fold<List<List<SpotifyFriendActivity>>>(
|
||||
[],
|
||||
(previousValue, element) {
|
||||
if (previousValue.isEmpty) {
|
||||
final friendGroup = useMemoized(
|
||||
() => friends.fold<List<List<SpotifyFriendActivity>>>(
|
||||
[],
|
||||
(previousValue, element) {
|
||||
if (previousValue.isEmpty) {
|
||||
return [
|
||||
[element]
|
||||
];
|
||||
}
|
||||
|
||||
final lastGroup = previousValue.last;
|
||||
if (lastGroup.length < groupCount) {
|
||||
return [
|
||||
...previousValue.sublist(0, previousValue.length - 1),
|
||||
[...lastGroup, element]
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...previousValue,
|
||||
[element]
|
||||
];
|
||||
}
|
||||
|
||||
final lastGroup = previousValue.last;
|
||||
if (lastGroup.length < groupCount) {
|
||||
return [
|
||||
...previousValue.sublist(0, previousValue.length - 1),
|
||||
[...lastGroup, element]
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...previousValue,
|
||||
[element]
|
||||
];
|
||||
},
|
||||
},
|
||||
),
|
||||
[friends, groupCount],
|
||||
);
|
||||
|
||||
if (friendsQuery.isLoading ||
|
||||
friendsQuery.asData?.value.friends.isEmpty == true) {
|
||||
friendsQuery.asData?.value.friends.isEmpty == true ||
|
||||
auth == null) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox.shrink(),
|
||||
);
|
||||
|
||||
@ -54,7 +54,7 @@ class HomeGenresSection extends HookConsumerWidget {
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(
|
||||
"Browse All",
|
||||
context.l10n.browse_all,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
|
||||
199
lib/components/library/local_folder/local_folder_item.dart
Normal file
199
lib/components/library/local_folder/local_folder_item.dart
Normal file
@ -0,0 +1,199 @@
|
||||
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/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: () {
|
||||
if (isDownloadFolder) {
|
||||
context.go("/library/local?downloads=1", extra: folder);
|
||||
} else {
|
||||
context.go(
|
||||
"/library/local",
|
||||
extra: folder,
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Color.lerp(
|
||||
colorScheme.surfaceVariant,
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,52 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/components/library/local_folder/local_folder_item.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||
|
||||
const supportedAudioTypes = [
|
||||
"audio/webm",
|
||||
"audio/ogg",
|
||||
"audio/mpeg",
|
||||
"audio/mp4",
|
||||
"audio/opus",
|
||||
"audio/wav",
|
||||
"audio/aac",
|
||||
];
|
||||
|
||||
const imgMimeToExt = {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
"image/gif": ".gif",
|
||||
};
|
||||
|
||||
enum SortBy {
|
||||
none,
|
||||
@ -59,273 +25,77 @@ enum SortBy {
|
||||
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 {
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
final sortBy = useState<SortBy>(SortBy.none);
|
||||
final playlist = ref.watch(proxyPlaylistProvider);
|
||||
final trackSnapshot = ref.watch(localTracksProvider);
|
||||
final isPlaylistPlaying =
|
||||
playlist.containsTracks(trackSnapshot.asData?.value ?? []);
|
||||
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
final searchController = useTextEditingController();
|
||||
useValueListenable(searchController);
|
||||
final searchFocus = useFocusNode();
|
||||
final isFiltering = useState(false);
|
||||
final addLocalLibraryLocation = useCallback(() async {
|
||||
if (kIsMobile || kIsMacOS) {
|
||||
final dirStr = await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: preferences.downloadLocation,
|
||||
);
|
||||
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]);
|
||||
|
||||
final controller = useScrollController();
|
||||
// This is just to pre-load the tracks.
|
||||
// For now, this gets all of them.
|
||||
ref.watch(localTracksProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
onPressed: trackSnapshot.asData?.value != null
|
||||
? () async {
|
||||
if (trackSnapshot.asData?.value.isNotEmpty == true) {
|
||||
if (!isPlaylistPlaying) {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
trackSnapshot.asData!.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(context.l10n.play),
|
||||
Icon(
|
||||
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ExpandableSearchButton(
|
||||
isFiltering: isFiltering.value,
|
||||
onPressed: (value) => isFiltering.value = value,
|
||||
searchFocus: searchFocus,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SortTracksDropdown(
|
||||
value: sortBy.value,
|
||||
onChanged: (value) {
|
||||
sortBy.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
child: const Icon(SpotubeIcons.refresh),
|
||||
onPressed: () {
|
||||
ref.invalidate(localTracksProvider);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
ExpandableSearchField(
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
isFiltering: isFiltering.value,
|
||||
onChangeFiltering: (value) => isFiltering.value = value,
|
||||
),
|
||||
trackSnapshot.when(
|
||||
data: (tracks) {
|
||||
final sortedTracks = useMemoized(() {
|
||||
return ServiceUtils.sortTracks(tracks, sortBy.value);
|
||||
}, [sortBy.value, tracks]);
|
||||
|
||||
final filteredTracks = useMemoized(() {
|
||||
if (searchController.text.isEmpty) {
|
||||
return sortedTracks;
|
||||
}
|
||||
return sortedTracks
|
||||
.map((e) => (
|
||||
weightedRatio(
|
||||
"${e.name} - ${e.artists?.asString() ?? ""}",
|
||||
searchController.text,
|
||||
),
|
||||
e,
|
||||
))
|
||||
.toList()
|
||||
.sorted(
|
||||
(a, b) => b.$1.compareTo(a.$1),
|
||||
)
|
||||
.where((e) => e.$1 > 50)
|
||||
.map((e) => e.$2)
|
||||
.toList()
|
||||
.toList();
|
||||
}, [searchController.text, sortedTracks]);
|
||||
|
||||
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) {
|
||||
return const Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [NotFound()],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(localTracksProvider);
|
||||
},
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
child: Skeletonizer(
|
||||
enabled: trackSnapshot.isLoading,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (trackSnapshot.isLoading) {
|
||||
return TrackTile(
|
||||
playlist: playlist,
|
||||
track: FakeData.track,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
index: index,
|
||||
playlist: playlist,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => Expanded(
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: ListView.builder(
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => TrackTile(
|
||||
track: FakeData.track,
|
||||
index: index,
|
||||
playlist: playlist,
|
||||
),
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(SpotubeIcons.folderAdd),
|
||||
label: Text(context.l10n.add_library_location),
|
||||
onPressed: addLocalLibraryLocation,
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
)
|
||||
],
|
||||
);
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.isXs
|
||||
? 210
|
||||
: constrains.mdAndDown
|
||||
? 280
|
||||
: 250,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: preferences.localLibraryLocation.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
return LocalFolderItem(
|
||||
folder: index == 0
|
||||
? preferences.downloadLocation
|
||||
: preferences.localLibraryLocation[index - 1],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
@ -31,11 +30,17 @@ class VolumeSlider extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
child: SliderTheme(
|
||||
data: const SliderThemeData(
|
||||
showValueIndicator: ShowValueIndicator.always,
|
||||
),
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
label: (value * 100).toStringAsFixed(0),
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -24,6 +23,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/provider/volume_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class BottomPlayer extends HookConsumerWidget {
|
||||
BottomPlayer({super.key});
|
||||
@ -95,19 +95,19 @@ class BottomPlayer extends HookConsumerWidget {
|
||||
tooltip: context.l10n.mini_player,
|
||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||
onPressed: () async {
|
||||
final prevSize =
|
||||
await DesktopTools.window.getSize();
|
||||
await DesktopTools.window.setMinimumSize(
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
final prevSize = await windowManager.getSize();
|
||||
await windowManager.setMinimumSize(
|
||||
const Size(300, 300),
|
||||
);
|
||||
await DesktopTools.window.setAlwaysOnTop(true);
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
if (!kIsLinux) {
|
||||
await DesktopTools.window.setHasShadow(false);
|
||||
await windowManager.setHasShadow(false);
|
||||
}
|
||||
await DesktopTools.window
|
||||
await windowManager
|
||||
.setAlignment(Alignment.topRight);
|
||||
await DesktopTools.window
|
||||
.setSize(const Size(400, 500));
|
||||
await windowManager.setSize(const Size(400, 500));
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() async {
|
||||
|
||||
56
lib/components/root/update_dialog.dart
Normal file
56
lib/components/root/update_dialog.dart
Normal file
@ -0,0 +1,56 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class InterScrollbar extends HookWidget {
|
||||
final Widget child;
|
||||
@ -15,7 +15,7 @@ class InterScrollbar extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (DesktopTools.platform.isDesktop) return child;
|
||||
if (kIsDesktop) return child;
|
||||
|
||||
return DraggableScrollbar.semicircle(
|
||||
controller: controller,
|
||||
|
||||
@ -7,7 +7,8 @@ import 'package:titlebar_buttons/titlebar_buttons.dart';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
||||
implements PreferredSizeWidget {
|
||||
@ -89,7 +90,7 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
||||
final systemTitleBar =
|
||||
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
|
||||
if (kIsDesktop && !systemTitleBar) {
|
||||
DesktopTools.window.startDragging();
|
||||
windowManager.startDragging();
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,11 +108,7 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
||||
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: DesktopTools.platform.isMacOS &&
|
||||
hasFullscreen &&
|
||||
hasLeadingOrCanPop
|
||||
? 65
|
||||
: 0,
|
||||
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
||||
),
|
||||
sliver: SliverAppBar(
|
||||
leading: widget.leading,
|
||||
@ -149,11 +146,7 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
||||
onVerticalDragStart: onDrag,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: DesktopTools.platform.isMacOS &&
|
||||
hasFullscreen &&
|
||||
hasLeadingOrCanPop
|
||||
? 65
|
||||
: 0,
|
||||
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
||||
),
|
||||
child: AppBar(
|
||||
leading: widget.leading,
|
||||
@ -193,12 +186,12 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
||||
const type = ThemeType.auto;
|
||||
|
||||
Future<void> onClose() async {
|
||||
await DesktopTools.window.close();
|
||||
await windowManager.close();
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
if (kIsDesktop) {
|
||||
DesktopTools.window.isMaximized().then((value) {
|
||||
windowManager.isMaximized().then((value) {
|
||||
isMaximized.value = value;
|
||||
});
|
||||
}
|
||||
@ -235,14 +228,14 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MinimizeWindowButton(
|
||||
onPressed: DesktopTools.window.minimize,
|
||||
onPressed: windowManager.minimize,
|
||||
colors: colors,
|
||||
),
|
||||
if (isMaximized.value != true)
|
||||
MaximizeWindowButton(
|
||||
colors: colors,
|
||||
onPressed: () {
|
||||
DesktopTools.window.maximize();
|
||||
windowManager.maximize();
|
||||
isMaximized.value = true;
|
||||
},
|
||||
)
|
||||
@ -250,7 +243,7 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
||||
RestoreWindowButton(
|
||||
colors: colors,
|
||||
onPressed: () {
|
||||
DesktopTools.window.unmaximize();
|
||||
windowManager.unmaximize();
|
||||
isMaximized.value = false;
|
||||
},
|
||||
),
|
||||
@ -270,16 +263,16 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
||||
children: [
|
||||
DecoratedMinimizeButton(
|
||||
type: type,
|
||||
onPressed: DesktopTools.window.minimize,
|
||||
onPressed: windowManager.minimize,
|
||||
),
|
||||
DecoratedMaximizeButton(
|
||||
type: type,
|
||||
onPressed: () async {
|
||||
if (await DesktopTools.window.isMaximized()) {
|
||||
await DesktopTools.window.unmaximize();
|
||||
if (await windowManager.isMaximized()) {
|
||||
await windowManager.unmaximize();
|
||||
isMaximized.value = false;
|
||||
} else {
|
||||
await DesktopTools.window.maximize();
|
||||
await windowManager.maximize();
|
||||
isMaximized.value = true;
|
||||
}
|
||||
},
|
||||
|
||||
@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
@ -23,6 +22,7 @@ import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_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/spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
@ -197,6 +197,8 @@ class TrackOptions extends HookConsumerWidget {
|
||||
return downloadManager.getProgressNotifier(spotubeTrack);
|
||||
});
|
||||
|
||||
final isLocalTrack = track is LocalTrack;
|
||||
|
||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
@ -314,118 +316,120 @@ class TrackOptions extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack() => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (mediaQuery.smAndDown)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.album,
|
||||
leading: const Icon(SpotubeIcons.album),
|
||||
title: Text(context.l10n.go_to_album),
|
||||
subtitle: Text(track.album!.name!),
|
||||
),
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (me.asData?.value != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
leading: favorites.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
favorites.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.startRadio,
|
||||
leading: const Icon(SpotubeIcons.radio),
|
||||
title: Text(context.l10n.start_a_radio),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
],
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.download,
|
||||
enabled: !isInQueue,
|
||||
leading: isInQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
final progress = useListenable(progressNotifier!);
|
||||
return CircularProgressIndicator(
|
||||
value: progress.value,
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
title: Text(context.l10n.download_track),
|
||||
children: [
|
||||
if (isLocalTrack)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
title: Text(context.l10n.delete),
|
||||
),
|
||||
if (mediaQuery.smAndDown)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.album,
|
||||
leading: const Icon(SpotubeIcons.album),
|
||||
title: Text(context.l10n.go_to_album),
|
||||
subtitle: Text(track.album!.name!),
|
||||
),
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.playNext,
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (me.asData?.value != null && !isLocalTrack)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.favorite,
|
||||
leading: favorites.isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
title: Text(
|
||||
favorites.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.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,
|
||||
),
|
||||
),
|
||||
if (auth != null && !isLocalTrack) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.startRadio,
|
||||
leading: const Icon(SpotubeIcons.radio),
|
||||
title: Text(context.l10n.start_a_radio),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
],
|
||||
if (userPlaylist && auth != 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(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.share,
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.songlink,
|
||||
leading: Assets.logos.songlinkTransparent.image(
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
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.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
]
|
||||
},
|
||||
title: Text(context.l10n.song_link),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
//! This is the most ANTI pattern I've ever done, but it works
|
||||
|
||||
@ -195,19 +195,26 @@ class TrackTile extends HookConsumerWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: LinkText(
|
||||
track.name!,
|
||||
"/track/${track.id}",
|
||||
push: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: switch (track) {
|
||||
LocalTrack() => Text(
|
||||
track.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_ => LinkText(
|
||||
track.name!,
|
||||
"/track/${track.id}",
|
||||
push: true,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
},
|
||||
),
|
||||
if (constrains.mdAndUp) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: switch (track.runtimeType) {
|
||||
child: switch (track) {
|
||||
LocalTrack() => Text(
|
||||
track.album!.name!,
|
||||
maxLines: 1,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
@ -12,6 +12,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class TrackViewFlexHeader extends HookConsumerWidget {
|
||||
const TrackViewFlexHeader({super.key});
|
||||
@ -53,7 +54,7 @@ class TrackViewFlexHeader extends HookConsumerWidget {
|
||||
floating: false,
|
||||
pinned: true,
|
||||
expandedHeight: 450,
|
||||
automaticallyImplyLeading: DesktopTools.platform.isMobile,
|
||||
automaticallyImplyLeading: kIsMobile,
|
||||
backgroundColor: palette.color,
|
||||
title: isExpanded ? null : Text(props.title, style: headingStyle),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
@ -8,6 +8,7 @@ import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class TrackView extends HookConsumerWidget {
|
||||
const TrackView({super.key});
|
||||
@ -18,7 +19,7 @@ class TrackView extends HookConsumerWidget {
|
||||
final controller = useScrollController();
|
||||
|
||||
return Scaffold(
|
||||
appBar: DesktopTools.platform.isDesktop
|
||||
appBar: kIsDesktop
|
||||
? const PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
|
||||
@ -20,8 +20,6 @@ class Waypoint extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMounted = useIsMounted();
|
||||
|
||||
useEffect(() {
|
||||
if (isGrid) {
|
||||
return null;
|
||||
@ -32,19 +30,19 @@ class Waypoint extends HookWidget {
|
||||
|
||||
// scrollController fetches the next paginated data when the current
|
||||
// position of the user on the screen has surpassed
|
||||
if (controller.position.pixels >= nextPageTrigger && isMounted()) {
|
||||
if (controller.position.pixels >= nextPageTrigger && context.mounted) {
|
||||
await onTouchEdge?.call();
|
||||
}
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.hasClients && isMounted()) {
|
||||
if (controller.hasClients && context.mounted) {
|
||||
listener();
|
||||
controller.addListener(listener);
|
||||
}
|
||||
});
|
||||
return () => controller.removeListener(listener);
|
||||
}, [controller, onTouchEdge, isMounted]);
|
||||
}, [controller, onTouchEdge]);
|
||||
|
||||
if (isGrid) {
|
||||
return VisibilityDetector(
|
||||
|
||||
@ -1,29 +1,31 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/hooks/configurators/use_window_listener.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final closeNotification = DesktopTools.createNotification(
|
||||
title: 'Spotube',
|
||||
message: 'Running in background. Minimized to System Tray',
|
||||
actions: [
|
||||
LocalNotificationAction(text: 'Close The App'),
|
||||
],
|
||||
)?..onClickAction = (value) {
|
||||
exit(0);
|
||||
};
|
||||
final closeNotification = !kIsDesktop
|
||||
? null
|
||||
: (LocalNotification(
|
||||
title: 'Spotube',
|
||||
body: 'Running in background. Minimized to System Tray',
|
||||
actions: [
|
||||
LocalNotificationAction(text: 'Close The App'),
|
||||
],
|
||||
)..onClickAction = (value) {
|
||||
exit(0);
|
||||
});
|
||||
|
||||
void useCloseBehavior(WidgetRef ref) {
|
||||
useWindowListener(
|
||||
onWindowClose: () async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
if (preferences.closeBehavior == CloseBehavior.minimizeToTray) {
|
||||
await DesktopTools.window.hide();
|
||||
await windowManager.hide();
|
||||
closeNotification?.show();
|
||||
} else {
|
||||
exit(0);
|
||||
|
||||
@ -7,7 +7,7 @@ import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final appLinks = AppLinks();
|
||||
final linkStream = appLinks.allStringLinkStream.asBroadcastStream();
|
||||
@ -53,7 +53,7 @@ void useDeepLinking(WidgetRef ref) {
|
||||
|
||||
StreamSubscription? mediaStream;
|
||||
|
||||
if (DesktopTools.platform.isMobile) {
|
||||
if (kIsMobile) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
||||
|
||||
mediaStream =
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import 'package:disable_battery_optimization/disable_battery_optimization.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:spotube/hooks/utils/use_async_effect.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
void useDisableBatteryOptimizations() {
|
||||
useAsyncEffect(() async {
|
||||
if (!DesktopTools.platform.isAndroid ||
|
||||
KVStoreService.askedForBatteryOptimization) return;
|
||||
if (!kIsAndroid || KVStoreService.askedForBatteryOptimization) return;
|
||||
|
||||
await DisableBatteryOptimization.showDisableBatteryOptimizationSettings();
|
||||
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/hooks/utils/use_async_effect.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
void useGetStoragePermissions(WidgetRef ref) {
|
||||
final isMounted = useIsMounted();
|
||||
final context = useContext();
|
||||
|
||||
useAsyncEffect(
|
||||
() async {
|
||||
if (!DesktopTools.platform.isMobile) return;
|
||||
if (!kIsMobile) return;
|
||||
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
@ -25,11 +26,11 @@ void useGetStoragePermissions(WidgetRef ref) {
|
||||
|
||||
if (hasNoStoragePerm) {
|
||||
await Permission.storage.request();
|
||||
if (isMounted()) ref.invalidate(localTracksProvider);
|
||||
if (context.mounted) ref.invalidate(localTracksProvider);
|
||||
}
|
||||
if (hasNoAudioPerm) {
|
||||
await Permission.audio.request();
|
||||
if (isMounted()) ref.invalidate(localTracksProvider);
|
||||
if (context.mounted) ref.invalidate(localTracksProvider);
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/intents.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
void useInitSysTray(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
final systemTray = useRef<SystemTray?>(null);
|
||||
|
||||
final initializeMenu = useCallback(() async {
|
||||
systemTray.value?.destroy();
|
||||
final playlist = ref.read(proxyPlaylistProvider);
|
||||
final playlistQueue = ref.read(proxyPlaylistProvider.notifier);
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
if (!preferences.showSystemTrayIcon) {
|
||||
await systemTray.value?.destroy();
|
||||
systemTray.value = null;
|
||||
return;
|
||||
}
|
||||
final enabled = !playlist.isFetching;
|
||||
systemTray.value = await DesktopTools.createSystemTrayMenu(
|
||||
title: DesktopTools.platform.isWindows ? "Spotube" : "",
|
||||
iconPath: "assets/spotube-logo.png",
|
||||
windowsIconPath: "assets/spotube-logo.ico",
|
||||
items: [
|
||||
MenuItemLabel(
|
||||
label: "Show/Hide",
|
||||
name: "show-hide",
|
||||
onClicked: (item) async {
|
||||
if (await DesktopTools.window.isVisible()) {
|
||||
await DesktopTools.window.hide();
|
||||
} else {
|
||||
await DesktopTools.window.show();
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuItemLabel(
|
||||
label: "Play/Pause",
|
||||
name: "play-pause",
|
||||
enabled: enabled,
|
||||
onClicked: (_) async {
|
||||
Actions.maybeInvoke<PlayPauseIntent>(
|
||||
context, PlayPauseIntent(ref)) ??
|
||||
PlayPauseAction().invoke(PlayPauseIntent(ref));
|
||||
},
|
||||
),
|
||||
MenuItemLabel(
|
||||
label: "Next",
|
||||
name: "next",
|
||||
enabled: enabled && (playlist.tracks.length) > 1,
|
||||
onClicked: (p0) async {
|
||||
await playlistQueue.next();
|
||||
},
|
||||
),
|
||||
MenuItemLabel(
|
||||
label: "Previous",
|
||||
name: "previous",
|
||||
enabled: enabled && (playlist.tracks.length) > 1,
|
||||
onClicked: (p0) async {
|
||||
await playlistQueue.previous();
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuItemLabel(
|
||||
label: "Quit",
|
||||
name: "quit",
|
||||
onClicked: (item) async {
|
||||
exit(0);
|
||||
},
|
||||
),
|
||||
],
|
||||
onEvent: (event, tray) async {
|
||||
if (DesktopTools.platform.isWindows) {
|
||||
switch (event) {
|
||||
case SystemTrayEvent.click:
|
||||
await DesktopTools.window.show();
|
||||
break;
|
||||
case SystemTrayEvent.rightClick:
|
||||
await tray.popUpContextMenu();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
switch (event) {
|
||||
case SystemTrayEvent.rightClick:
|
||||
await DesktopTools.window.show();
|
||||
break;
|
||||
case SystemTrayEvent.click:
|
||||
await tray.popUpContextMenu();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [ref]);
|
||||
|
||||
useReassemble(initializeMenu);
|
||||
|
||||
ref.listen<ProxyPlaylist?>(
|
||||
proxyPlaylistProvider,
|
||||
(previous, next) {
|
||||
initializeMenu();
|
||||
},
|
||||
);
|
||||
ref.listen(
|
||||
userPreferencesProvider.select((s) => s.showSystemTrayIcon),
|
||||
(previous, next) {
|
||||
initializeMenu();
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
initializeMenu();
|
||||
});
|
||||
return () async {
|
||||
await systemTray.value?.destroy();
|
||||
};
|
||||
}, [initializeMenu]);
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:spotube/collections/env.dart';
|
||||
|
||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||
import 'package:spotube/hooks/controllers/use_package_info.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
void useUpdateChecker(WidgetRef ref) {
|
||||
final isCheckUpdateEnabled =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.checkUpdate));
|
||||
final packageInfo = usePackageInfo(
|
||||
appName: 'Spotube',
|
||||
packageName: 'spotube',
|
||||
);
|
||||
final Future<List<Version?>> Function() checkUpdate = useCallback(
|
||||
() async {
|
||||
final value = await http.get(
|
||||
Uri.parse(
|
||||
"https://api.github.com/repos/KRTirtho/spotube/releases/latest"),
|
||||
);
|
||||
final tagName =
|
||||
(jsonDecode(value.body)["tag_name"] as String).replaceAll("v", "");
|
||||
final currentVersion = packageInfo.version == "Unknown"
|
||||
? null
|
||||
: Version.parse(packageInfo.version);
|
||||
final latestVersion =
|
||||
tagName == "nightly" ? null : Version.parse(tagName);
|
||||
return [currentVersion, latestVersion];
|
||||
},
|
||||
[packageInfo.version],
|
||||
);
|
||||
|
||||
final context = useContext();
|
||||
|
||||
download(String url) => launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (!Env.enableUpdateChecker) return;
|
||||
if (!isCheckUpdateEnabled) return null;
|
||||
checkUpdate().then((value) {
|
||||
final currentVersion = value.first;
|
||||
final latestVersion = value.last;
|
||||
if (currentVersion == null ||
|
||||
latestVersion == null ||
|
||||
(latestVersion.isPreRelease && !currentVersion.isPreRelease) ||
|
||||
(!latestVersion.isPreRelease && currentVersion.isPreRelease)) return;
|
||||
if (latestVersion <= currentVersion) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.black26,
|
||||
builder: (context) {
|
||||
const url =
|
||||
"https://spotube.krtirtho.dev/other-downloads/stable-downloads";
|
||||
return AlertDialog(
|
||||
title: const Text("Spotube has an update"),
|
||||
actions: [
|
||||
FilledButton(
|
||||
child: const Text("Download Now"),
|
||||
onPressed: () => download(url),
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Spotube v${value.last} has been released"),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
return null;
|
||||
}, [packageInfo, isCheckUpdateEnabled]);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class CallbackWindowListener implements WindowListener {
|
||||
final VoidCallback? _onWindowClose;
|
||||
@ -154,6 +156,8 @@ void useWindowListener({
|
||||
VoidCallback? onWindowEvent,
|
||||
}) {
|
||||
useEffect(() {
|
||||
if (!kIsDesktop) return null;
|
||||
|
||||
final listener = CallbackWindowListener(
|
||||
onWindowClose: onWindowClose,
|
||||
onWindowFocus: onWindowFocus,
|
||||
@ -172,9 +176,9 @@ void useWindowListener({
|
||||
onWindowUndocked: onWindowUndocked,
|
||||
onWindowEvent: onWindowEvent,
|
||||
);
|
||||
DesktopTools.window.addListener(listener);
|
||||
windowManager.addListener(listener);
|
||||
return () {
|
||||
DesktopTools.window.removeListener(listener);
|
||||
windowManager.removeListener(listener);
|
||||
};
|
||||
}, [
|
||||
onWindowClose,
|
||||
|
||||
@ -14,7 +14,6 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) {
|
||||
final context = useContext();
|
||||
final theme = Theme.of(context);
|
||||
final paletteColor = ref.watch(_paletteColorState);
|
||||
final mounted = useIsMounted();
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
@ -25,7 +24,7 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) {
|
||||
width: 50,
|
||||
),
|
||||
);
|
||||
if (!mounted()) return;
|
||||
if (!context.mounted) return;
|
||||
final color = theme.brightness == Brightness.light
|
||||
? palette.lightMutedColor ?? palette.lightVibrantColor
|
||||
: palette.darkMutedColor ?? palette.darkVibrantColor;
|
||||
@ -41,7 +40,7 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) {
|
||||
|
||||
PaletteGenerator usePaletteGenerator(String imageUrl) {
|
||||
final palette = useState(PaletteGenerator.fromColors([]));
|
||||
final mounted = useIsMounted();
|
||||
final context = useContext();
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
@ -52,7 +51,7 @@ PaletteGenerator usePaletteGenerator(String imageUrl) {
|
||||
width: 50,
|
||||
),
|
||||
);
|
||||
if (!mounted()) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
palette.value = newPalette;
|
||||
});
|
||||
|
||||
@ -107,6 +107,9 @@
|
||||
"always_on_top": "Always on top",
|
||||
"exit_mini_player": "Exit Mini player",
|
||||
"download_location": "Download location",
|
||||
"local_library": "Local library",
|
||||
"add_library_location": "Add to library",
|
||||
"remove_library_location": "Remove from library",
|
||||
"account": "Account",
|
||||
"login_with_spotify": "Login with your Spotify account",
|
||||
"connect_with_spotify": "Connect with Spotify",
|
||||
@ -295,6 +298,7 @@
|
||||
"delete_playlist": "Delete Playlist",
|
||||
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
|
||||
"local_tracks": "Local Tracks",
|
||||
"local_tab": "Local",
|
||||
"song_link": "Song Link",
|
||||
"skip_this_nonsense": "Skip this nonsense",
|
||||
"freedom_of_music": "“Freedom of Music”",
|
||||
@ -321,4 +325,4 @@
|
||||
"connect_client_alert": "You're being controlled by {client}",
|
||||
"this_device": "This Device",
|
||||
"remote": "Remote"
|
||||
}
|
||||
}
|
||||
|
||||
324
lib/l10n/app_eu.arb
Normal file
324
lib/l10n/app_eu.arb
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"guest": "Gonbidatua",
|
||||
"browse": "Arakatu",
|
||||
"search": "Bilatu",
|
||||
"library": "Liburutegia",
|
||||
"lyrics": "Hitzak",
|
||||
"settings": "Ezarpenak",
|
||||
"genre_categories_filter": "Kategoria edo generoak filtratu...",
|
||||
"genre": "Generoa",
|
||||
"personalized": "Pertsonalizatua",
|
||||
"featured": "Nabarmenduak",
|
||||
"new_releases": "Argitaratze berriak",
|
||||
"songs": "Abestiak",
|
||||
"playing_track": "{track} erreproduzitzen",
|
||||
"queue_clear_alert": "Uneko zerrenda ezabatuko da. {track_length} abesti ezabatuko dira.\nJarraitu nahi duzu?",
|
||||
"load_more": "Gehiago kargatu",
|
||||
"playlists": "Zerrendak",
|
||||
"artists": "Artistak",
|
||||
"albums": "Albumak",
|
||||
"tracks": "Kantak",
|
||||
"downloads": "Deskargak",
|
||||
"filter_playlists": "Zure zerrendak filtratu...",
|
||||
"liked_tracks": "Gustuko Kantak",
|
||||
"liked_tracks_description": "Zure gustuko kanta guztiak",
|
||||
"create_playlist": "Sortu zerrenda",
|
||||
"create_a_playlist": "Sortu zerrenda bat",
|
||||
"update_playlist": "Eguneratu zerrenda",
|
||||
"create": "Sortu",
|
||||
"cancel": "Ezeztatu",
|
||||
"update": "Eguneratu",
|
||||
"playlist_name": "Zerrenda Izena",
|
||||
"name_of_playlist": "Zerrendaren izena",
|
||||
"description": "Deskribapena",
|
||||
"public": "Publikoa",
|
||||
"collaborative": "Kolaboratiboa",
|
||||
"search_local_tracks": "Bilatu kanta lokalak...",
|
||||
"play": "Erreproduzitu",
|
||||
"delete": "Ezabatu",
|
||||
"none": "Batere ez",
|
||||
"sort_a_z": "Ordenatu A-Z",
|
||||
"sort_z_a": "Ordenatu Z-A",
|
||||
"sort_artist": "Ordenatu Artistaren arabera",
|
||||
"sort_album": "Ordenatu Albumaren arabera",
|
||||
"sort_duration": "Ordenar Iraupenaren arabera",
|
||||
"sort_tracks": "Ordenatu Kantak",
|
||||
"currently_downloading": "Oraintxe ({tracks_length}) deskargatzen",
|
||||
"cancel_all": "Ezeztatu dena",
|
||||
"filter_artist": "Filtratu artistak...",
|
||||
"followers": "{followers} Jarraitzaile",
|
||||
"add_artist_to_blacklist": "Gehitu artista zerrenda beltzera",
|
||||
"top_tracks": "Top Kantak",
|
||||
"fans_also_like": "Fan-ek hau ere gustuko dute",
|
||||
"loading": "Kargatzen...",
|
||||
"artist": "Artista",
|
||||
"blacklisted": "Zerrenda beltzean",
|
||||
"following": "Jarraitzen",
|
||||
"follow": "Jarraitu",
|
||||
"artist_url_copied": "Artistaren URL-a arbelera kopiatua",
|
||||
"added_to_queue": "{tracks} kanta zerrendara gehituak",
|
||||
"filter_albums": "Albumak filtratu...",
|
||||
"synced": "Sinkronizatuta",
|
||||
"plain": "Arrunta",
|
||||
"shuffle": "Ausaz",
|
||||
"search_tracks": "Bilatu kantak...",
|
||||
"released": "Argitaratua",
|
||||
"error": "Errorea: {error}",
|
||||
"title": "Izenburua",
|
||||
"time": "Iraupena",
|
||||
"more_actions": "Ekintza gehiago",
|
||||
"download_count": "({count}) deskarga",
|
||||
"add_count_to_playlist": "Gehitu ({count}) zerrendara",
|
||||
"add_count_to_queue": "Gehitu ({count}) ilarara",
|
||||
"play_count_next": "Erreproduzitu hurrengo ({count})-ak",
|
||||
"album": "Albuma",
|
||||
"copied_to_clipboard": "{data} arbelean kopiatua",
|
||||
"add_to_following_playlists": "Gehitu {track} hurrengo erreprodukzio-zerrendetara",
|
||||
"add": "Gehitu",
|
||||
"added_track_to_queue": "{track} zerrendan gehitua",
|
||||
"add_to_queue": "Gehitu zerrendan",
|
||||
"track_will_play_next": "{track} erreproduzituko da ondoren",
|
||||
"play_next": "Hurrengo erreprodukzioa",
|
||||
"removed_track_from_queue": "{track} zerrendatik ezabatua",
|
||||
"remove_from_queue": "Ezabatu ilaratik",
|
||||
"remove_from_favorites": "Ezabatu gogokoetatik",
|
||||
"save_as_favorite": "Gorde gogokoetan",
|
||||
"add_to_playlist": "Gehitu zerrendara",
|
||||
"remove_from_playlist": "Ezabatu zerrendatik",
|
||||
"add_to_blacklist": "Gehitu zerrenda beltzera",
|
||||
"remove_from_blacklist": "Ezabatu zerrenda beltzetik",
|
||||
"share": "Elkarbanatu",
|
||||
"mini_player": "Mini Erreproduzitzailea",
|
||||
"slide_to_seek": "Arrastatu aurrerantz edo atzearantz bilatzeko",
|
||||
"shuffle_playlist": "Erreproduzitu zerrenda ausazko ordenean",
|
||||
"unshuffle_playlist": "Desgaitu ausazko erreprodukzioa",
|
||||
"previous_track": "Aurreko pista",
|
||||
"next_track": "Hurrengo pista",
|
||||
"pause_playback": "Pausatu erreprodukzioa",
|
||||
"resume_playback": "Berrabiarazi erreprodukzioa",
|
||||
"loop_track": "Kanta begiztan",
|
||||
"repeat_playlist": "Errepikatu lista",
|
||||
"queue": "Ilara",
|
||||
"alternative_track_sources": "Kanten iturri alternatiboak",
|
||||
"download_track": "Deskargatu kanta",
|
||||
"tracks_in_queue": "{tracks} kanta zerrendan",
|
||||
"clear_all": "Garbitu dena",
|
||||
"show_hide_ui_on_hover": "Erakutsi/Ezkutatu interfazea kurtsorea pasatzean",
|
||||
"always_on_top": "Beti ikusgai",
|
||||
"exit_mini_player": "Irten mini erreproduzitzailetik",
|
||||
"download_location": "Deskargen kokapena",
|
||||
"account": "Kontua",
|
||||
"login_with_spotify": "Hasi saioa zure Spotify kontuarekin",
|
||||
"connect_with_spotify": "Spotify-rekin konektatu",
|
||||
"logout": "Itxi saioa",
|
||||
"logout_of_this_account": "Itxi kontu honen saioa",
|
||||
"language_region": "Hizkuntza eta Herrialdea",
|
||||
"language": "Hizkuntza",
|
||||
"system_default": "Sisteman lehenetsia",
|
||||
"market_place_region": "Dendaren herrialdea",
|
||||
"recommendation_country": "Gomendio herrialdea",
|
||||
"appearance": "Itxura",
|
||||
"layout_mode": "Diseinu modua",
|
||||
"override_layout_settings": "Responsive diseinu moduaren ezarpenak ezeztatu",
|
||||
"adaptive": "Moldagarria",
|
||||
"compact": "Trinkoa",
|
||||
"extended": "Hedatua",
|
||||
"theme": "Gaia",
|
||||
"dark": "Iluna",
|
||||
"light": "Argia",
|
||||
"system": "Sistema",
|
||||
"accent_color": "Azentu kolorea",
|
||||
"sync_album_color": "Sinkronizatu albumaren kolorea",
|
||||
"sync_album_color_description": "Albumaren artearen kolore nagusia erabili azentu kolore bezala",
|
||||
"playback": "Erreprodukzioa",
|
||||
"audio_quality": "Audioaren kalitatea",
|
||||
"high": "Altua",
|
||||
"low": "Baxua",
|
||||
"pre_download_play": "Aurre-deskargatu eta erreproduzitu",
|
||||
"pre_download_play_description": "Streaming egin beharrean, byte-ak deskargatu eta erreproduzitu (banda-zabalera handia duten erabiltzaileentzat gomendagarria)",
|
||||
"skip_non_music": "Musika ez diren segmentuak baztertu (SponsorBlock)",
|
||||
"blacklist_description": "Zerrenda beltzeko abesti eta artistak",
|
||||
"wait_for_download_to_finish": "Mesedez, itxaron uneko deskarga bukatu arte",
|
||||
"desktop": "Mahaigaina",
|
||||
"close_behavior": "Ixterako Portaera",
|
||||
"close": "Itxi",
|
||||
"minimize_to_tray": "Sistemako erretilura minimizatu",
|
||||
"show_tray_icon": "Erakutsi ikonoa sistemaren erretiluan",
|
||||
"about": "Honi buruz",
|
||||
"u_love_spotube": "Badakigu Spotube maite duzula",
|
||||
"check_for_updates": "Bilatu eguneraketak",
|
||||
"about_spotube": "Spotube-ri buruz",
|
||||
"blacklist": "Zerrenda beltza",
|
||||
"please_sponsor": "Mesedez, babestu/diruz lagundu",
|
||||
"spotube_description": "Spotube, arina, plataforma-anitza eta doakoa den Spotify-ren bezeroa",
|
||||
"version": "Bertsioa",
|
||||
"build_number": "Konpilazio zenbakia",
|
||||
"founder": "Sortzailea",
|
||||
"repository": "Errepositorioa",
|
||||
"bug_issues": "Erroreak eta arazoak",
|
||||
"made_with": "Bangladesh🇧🇩-en ❤️-z egina",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Lizentzia",
|
||||
"add_spotify_credentials": "Gehitu zure Spotify kredentzialak hasi ahal izateko",
|
||||
"credentials_will_not_be_shared_disclaimer": "Ez arduratu, zure kredentzialak ez ditugu bilduko edo inorekin elkarbanatuko",
|
||||
"know_how_to_login": "Ez dakizu nola egin?",
|
||||
"follow_step_by_step_guide": "Jarraitu pausoz-pausoko gida",
|
||||
"spotify_cookie": "Spotify-ren {name} cookiea",
|
||||
"cookie_name_cookie": "{name} cookiea",
|
||||
"fill_in_all_fields": "Mesedez, osatu eremu guztiak",
|
||||
"submit": "Bidali",
|
||||
"exit": "Irten",
|
||||
"previous": "Aurrekoa",
|
||||
"next": "Hurrengoa",
|
||||
"done": "Eginda",
|
||||
"step_1": "1. pausua",
|
||||
"first_go_to": "Hasteko, joan hona",
|
||||
"login_if_not_logged_in": "eta hasi saioa/sortu kontua lehendik ez baduzu eginda",
|
||||
"step_2": "2. pausua",
|
||||
"step_2_steps": "1. Saioa hasita duzularik, sakatu F12 edo saguaren eskuineko botoia klikatu > Ikuskatu nabigatzaileko garapen tresnak irekitzeko.\n2. Joan \"Aplikazio\" (Chrome, Edge, Brave, etab.) edo \"Biltegiratzea\" (Firefox, Palemoon, etab.)\n3. Joan \"Cookieak\" atalera eta gero \"https://accounts.spotify.com\" azpiatalera",
|
||||
"step_3": "3. pausua",
|
||||
"step_3_steps": "Kopiatu \"sp_dc\" cookiearen balioa",
|
||||
"success_emoji": "Eginda! 🥳",
|
||||
"success_message": "Ongi hasi duzu zure Spotify kontua. Lan bikaina, lagun!",
|
||||
"step_4": "4. pausua",
|
||||
"step_4_steps": "Itsatsi \"sp_dc\"-tik kopiatutako balioa",
|
||||
"something_went_wrong": "Zerbaitek huts egin du",
|
||||
"piped_instance": "Piped zerbitzariaren instantzia",
|
||||
"piped_description": "Kanten koizidentzietan erabiltzeko Piped zerbitzariaren instantzia",
|
||||
"piped_warning": "Batzuk agian ez dute ongi funtzionatuko, zure ardurapean erabili",
|
||||
"generate_playlist": "Sortu Zerrenda",
|
||||
"track_exists": "{track} kanta dagoeneko badago",
|
||||
"replace_downloaded_tracks": "Ordezkatu deskargatutako kanta guztiak",
|
||||
"skip_download_tracks": "Deskargatutako kanta guztien deskarga baztertu",
|
||||
"do_you_want_to_replace": "Dagoen kanta ordezkatu nahi duzu??",
|
||||
"replace": "Ordezkatu",
|
||||
"skip": "Baztertu",
|
||||
"select_up_to_count_type": "Aukertu {count} {type}",
|
||||
"select_genres": "Aukeratu Generoak",
|
||||
"add_genres": "Gehitu Generoak",
|
||||
"country": "Herrialdea",
|
||||
"number_of_tracks_generate": "Sortzeko kanta kopurua",
|
||||
"acousticness": "Akustikotasuna",
|
||||
"danceability": "Dantzagarritasuna",
|
||||
"energy": "Energia",
|
||||
"instrumentalness": "Instrumentaltasuna",
|
||||
"liveness": "Zuzenean",
|
||||
"loudness": "Ozentasuna",
|
||||
"speechiness": "Hitzaldia",
|
||||
"valence": "Balentzia",
|
||||
"popularity": "Populartasuna",
|
||||
"key": "Tonua",
|
||||
"duration": "Iraupena (s)",
|
||||
"tempo": "Tenpoa (BPM)",
|
||||
"mode": "Modua",
|
||||
"time_signature": "Konpasa",
|
||||
"short": "Motza",
|
||||
"medium": "Ertaina",
|
||||
"long": "Luzea",
|
||||
"min": "Min.",
|
||||
"max": "Max.",
|
||||
"target": "Helburua",
|
||||
"moderate": "Moderatua",
|
||||
"deselect_all": "Desaukeratu dena",
|
||||
"select_all": "Aukeratu dena",
|
||||
"are_you_sure": "Ziur zaude?",
|
||||
"generating_playlist": "Zure pertsonalizatutako zerrenda sortzen...",
|
||||
"selected_count_tracks": "{count} kanta aukeratuta",
|
||||
"download_warning": "Abesti guztiak aldi berean deskargatuz gero, argi dago musika pirateatzen ari zarela eta musikaren gizarte sortzaileari kalte egiten diozula. Honen jakitun izan eta artisten lan gogorra errespetatu eta babestea espero dut",
|
||||
"download_ip_ban_warning": "Bidenabar, baliteke zure IPa YouTuben blokeatzea deskarga eskera gehiegi egiten badituzu. IPa blokeatzeak esan nahi du ezin izango duzula YouTube erabili (nahiz eta saioa hasia izan) gutxienez 2-3 hilabetez IP helbide horretatik. Eta Spotube ez da erantzule izango hori gertatzen bazaizu",
|
||||
"by_clicking_accept_terms": "'Onartu' klikatzean, ondorengo baldintzak onartzen dituzu:",
|
||||
"download_agreement_1": "Badakit musika pirateatzen ari naizela. Gaiztoa naiz",
|
||||
"download_agreement_2": "Ahal dudanean lagunduko diot artistari baina oraingoz ez dut bere artea erosteko dirurik",
|
||||
"download_agreement_3": "Erabat jakitun naiz YouTubek nire IPa blokea dezakeela eta ez diot Spotube-ri edo bere jabe/laguntzaileei erantzukizunik eskatuko nire oraingo jokaerak ekar ditzakeen arazoengatik",
|
||||
"decline": "Baztertu",
|
||||
"accept": "Onartu",
|
||||
"details": "Xehetasunak",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Kanala",
|
||||
"likes": "Gustukoak",
|
||||
"dislikes": "Ez gustukoak",
|
||||
"views": "Ikuspenak",
|
||||
"streamUrl": "Streaming-aren URLa",
|
||||
"stop": "Gelditu",
|
||||
"sort_newest": "Ordenatu gehitu berrienetik",
|
||||
"sort_oldest": "Ordenatu gehitu zaharrenetik",
|
||||
"sleep_timer": "Itzaltzeko tenporizadorea",
|
||||
"mins": "{minutes} minutu",
|
||||
"hours": "{hours} ordu",
|
||||
"hour": "{hours} ordu",
|
||||
"custom_hours": "Ordu pertsonalizatuak",
|
||||
"logs": "Log-ak",
|
||||
"developers": "Garatzaileak",
|
||||
"not_logged_in": "Ez duzu saioa hasi",
|
||||
"search_mode": "Bilaketa modua",
|
||||
"audio_source": "Audio Iturria",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Errorea zifratzean",
|
||||
"encryption_failed_warning": "Spotube-ek zifratzea darabil datuak modu seguruan biltegiratzeko. Baina huts egin du. Hori dela eta, biltegiratzea ez da segurua izango\nLinux erabiltzen ari bazara, ziurtatu edozein sekretu-zerbitzu (gnome-keyring, kde-wallet, keepassxc etab.) instalatuta duzula",
|
||||
"querying_info": "Informazioa egiaztatzen...",
|
||||
"piped_api_down": "Piped-en APIa ez dago eskuragarri",
|
||||
"piped_down_error_instructions": "Piped-en {pipedInstance} instantzia ez dago martxan une honetan\n\nAldatu instantzia edo aldatu 'API mota' YouTuberen API ofizialera\n\nZiurtatu aplikazioa berrabiarazten duzula aldaketa eta gero",
|
||||
"you_are_offline": "Une honetan konexiorik gabe zaude",
|
||||
"connection_restored": "Internet konexioa berrezarri egin da",
|
||||
"use_system_title_bar": "Erabili sistemako izenburu barra",
|
||||
"crunching_results": "Emaitzak prozesatzen...",
|
||||
"search_to_get_results": "Bilatu emaitzak lortzeko",
|
||||
"use_amoled_mode": "Erabili AMOLED modua",
|
||||
"pitch_dark_theme": "Dart-en gai iluna",
|
||||
"normalize_audio": "Normalizatu audioa",
|
||||
"change_cover": "Aldatu azala",
|
||||
"add_cover": "Gehitu azala",
|
||||
"restore_defaults": "Berrezarri berezko balioak",
|
||||
"download_music_codec": "Deskargatutako musikaren codec-a",
|
||||
"streaming_music_codec": "Streaming musikaren codec-a",
|
||||
"login_with_lastfm": "Hasi saioa Last.fm-n",
|
||||
"connect": "Konektatu",
|
||||
"disconnect_lastfm": "Deskonektatu Last.fm-tik",
|
||||
"disconnect": "Deskonektatu",
|
||||
"username": "Erabiltzaile izena",
|
||||
"password": "Pasahitza",
|
||||
"login": "Hasi saioa",
|
||||
"login_with_your_lastfm": "Hasi saioa Last.fm-ko zure kontuarekin",
|
||||
"scrobble_to_lastfm": "Scrobble Last.fm-ra",
|
||||
"go_to_album": "Albumera joan",
|
||||
"discord_rich_presence": "Discord-en presentzia aberatsa",
|
||||
"browse_all": "Esploratu dena",
|
||||
"genres": "Generoak",
|
||||
"explore_genres": "Esploratu generoak",
|
||||
"friends": "Lagunak",
|
||||
"no_lyrics_available": "Sentitzen dut, ezin dira kanta honen hitzak aurkitu",
|
||||
"start_a_radio": "Hasi Irrati bat",
|
||||
"how_to_start_radio": "Nola hasi nahi duzu irratia?",
|
||||
"replace_queue_question": "Uneko zerrenda ordezkatu nahi duzu edo bertan gehitu?",
|
||||
"endless_playback": "Amaigabeko erreprodukzioa",
|
||||
"delete_playlist": "Ezabatu zerrenda",
|
||||
"delete_playlist_confirmation": "Ziur zaude zerrenda ezabatu nahi duzula?",
|
||||
"local_tracks": "Kanta lokalak",
|
||||
"song_link": "Kantaren lotura",
|
||||
"skip_this_nonsense": "Utzi txorakeria hau",
|
||||
"freedom_of_music": "“Musika Askatasuna”",
|
||||
"freedom_of_music_palm": "“Musika Askatasuna zure eskuetan”",
|
||||
"get_started": "Has gaitezen",
|
||||
"youtube_source_description": "Gomendatua eta hobekien dabilena.",
|
||||
"piped_source_description": "Aske zara? YouTube bezala, baino askeago.",
|
||||
"jiosaavn_source_description": "Asia hegoaldeko herrialdeetarako hoberena.",
|
||||
"highest_quality": "Kalitate Onena: {quality}",
|
||||
"select_audio_source": "Aukeratu Audio Iturria",
|
||||
"endless_playback_description": "Gehitu automatikoki kanta berriak\n ilararen bukaeran",
|
||||
"choose_your_region": "Aukeratu zure herrialdea",
|
||||
"choose_your_region_description": "Honekin Spotube-k zure kokalerakuari dagokion edukia\neskeiniko dizu.",
|
||||
"choose_your_language": "Aukeratu zure hizkuntza",
|
||||
"help_project_grow": "Lagundu proiektu honi hazten",
|
||||
"help_project_grow_description": "Spotube kode irekiko proiektu bat da. Proiektu hau hazten lagundu dezakezu, erroreak jakinaraziz edo ezaugarri berriak proposatuz.",
|
||||
"contribute_on_github": "GitHub-en lagundu",
|
||||
"donate_on_open_collective": "Open Collective-en diruz lagundu",
|
||||
"browse_anonymously": "Nabigatu Anonimoki",
|
||||
"enable_connect": "Gaitu konexioa",
|
||||
"enable_connect_description": "Kontrolatu Spotube beste gailu batzuetatik",
|
||||
"devices": "Gailuak",
|
||||
"select": "Aukeratu",
|
||||
"connect_client_alert": "{client} gailuak kontrolatzen zaitu",
|
||||
"this_device": "Gailu hau",
|
||||
"remote": "Urrunekoa"
|
||||
}
|
||||
324
lib/l10n/app_fi.arb
Normal file
324
lib/l10n/app_fi.arb
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"guest": "Vieras",
|
||||
"browse": "Selaa",
|
||||
"search": "Hae",
|
||||
"library": "Kirjasto",
|
||||
"lyrics": "Lyriikat",
|
||||
"settings": "Asetukset",
|
||||
"genre_categories_filter": "Suodata kategorioita tai genrejä",
|
||||
"genre": "Genre",
|
||||
"personalized": "Personoidut",
|
||||
"featured": "Esittelyssä",
|
||||
"new_releases": "Uusi julkaisu",
|
||||
"songs": "Laulut",
|
||||
"playing_track": "Soitetaan {track}",
|
||||
"queue_clear_alert": "Tämä tulee tyhjentämään jonon. {track_length} Kappaleita poistetaan\nHaluatko jatkaa?",
|
||||
"load_more": "Lataa lisää",
|
||||
"playlists": "Soittolistat",
|
||||
"artists": "Artistit",
|
||||
"albums": "Albumit",
|
||||
"tracks": "Kappaleet",
|
||||
"downloads": "Lataukset",
|
||||
"filter_playlists": "Suodata soittolistasi...",
|
||||
"liked_tracks": "Tykätyt kappaleet",
|
||||
"liked_tracks_description": "Kaikki tykättysi kappaleet",
|
||||
"create_playlist": "Luo soittolista",
|
||||
"create_a_playlist": "Luo soittolista",
|
||||
"update_playlist": "Päivitä soittolista",
|
||||
"create": "Luo",
|
||||
"cancel": "Peruuta",
|
||||
"update": "Päivitä",
|
||||
"playlist_name": "Soittolistan nimi",
|
||||
"name_of_playlist": "Soittolistan nimi",
|
||||
"description": "Kuvaus",
|
||||
"public": "Julkinen",
|
||||
"collaborative": "Collaborative",
|
||||
"search_local_tracks": "Hae paikallisia lauluja...",
|
||||
"play": "Soita",
|
||||
"delete": "Poista",
|
||||
"none": "Ei mitään",
|
||||
"sort_a_z": "Suodata A-Z",
|
||||
"sort_z_a": "Suodata Z-A",
|
||||
"sort_artist": "Suodata Artistilta",
|
||||
"sort_album": "Suodata Albumilta",
|
||||
"sort_duration": "Suodata Pituudelta",
|
||||
"sort_tracks": "Suodata Kappaleet",
|
||||
"currently_downloading": "Ladataan ({tracks_length})",
|
||||
"cancel_all": "Peru kaikki",
|
||||
"filter_artist": "Suodata artistit...",
|
||||
"followers": "{followers} Seuraajaa",
|
||||
"add_artist_to_blacklist": "Lisää artisti mustalle listalle",
|
||||
"top_tracks": "Suosituimmat kappaleet",
|
||||
"fans_also_like": "Fanit myös tykkäsivät",
|
||||
"loading": "Ladataan...",
|
||||
"artist": "Artisti",
|
||||
"blacklisted": "Mustalistattu",
|
||||
"following": "Seurataan",
|
||||
"follow": "Seuraa",
|
||||
"artist_url_copied": "Aristin URL kopioitiin leikepöytään",
|
||||
"added_to_queue": "Lisättiin {tracks} kappaletta jonoon",
|
||||
"filter_albums": "Suodata albumit...",
|
||||
"synced": "Synkronoitu",
|
||||
"plain": "Tavallinen",
|
||||
"shuffle": "Sekoita",
|
||||
"search_tracks": "Hae kappaleita...",
|
||||
"released": "Julkaistu",
|
||||
"error": "Virhe {error}",
|
||||
"title": "Otsikko",
|
||||
"time": "Aika",
|
||||
"more_actions": "Lisää toimintoja",
|
||||
"download_count": "Lataa ({count})",
|
||||
"add_count_to_playlist": "Lisää ({count}) Soittolistaasi",
|
||||
"add_count_to_queue": "Lisää ({count}) Jonoon",
|
||||
"play_count_next": "Soita ({count}) seuraavaksi",
|
||||
"album": "Albumi",
|
||||
"copied_to_clipboard": "Kopioitiin {data} leikepöytään",
|
||||
"add_to_following_playlists": "Lisää {track} seuraaviin soittolistoihin",
|
||||
"add": "Lisää",
|
||||
"added_track_to_queue": "Lisättiin {track} jonoon",
|
||||
"add_to_queue": "Lisää jonoon",
|
||||
"track_will_play_next": "{track} Soitetaan seuraavaksi",
|
||||
"play_next": "Soita seuraavaksi",
|
||||
"removed_track_from_queue": "Poistettiin {track} jonosta",
|
||||
"remove_from_queue": "Poista jonosta",
|
||||
"remove_from_favorites": "Poista suosikeista",
|
||||
"save_as_favorite": "Tallenna soittolistana",
|
||||
"add_to_playlist": "Lisää soittolistaan",
|
||||
"remove_from_playlist": "Poista soittolistasta",
|
||||
"add_to_blacklist": "Lisää mustalle listalle",
|
||||
"remove_from_blacklist": "Poista mustalistalta",
|
||||
"share": "Jaa",
|
||||
"mini_player": "Minisoitin",
|
||||
"slide_to_seek": "Liu'uta mennäkseen eteenpäin tai taaksepäin",
|
||||
"shuffle_playlist": "Sekoita soittolista",
|
||||
"unshuffle_playlist": "Poista sekoitus soittolistasta",
|
||||
"previous_track": "Äskeinen kappale",
|
||||
"next_track": "Seuraava kappale",
|
||||
"pause_playback": "Pysäytä soittolistan toisto",
|
||||
"resume_playback": "Jatka soittolistan toistoa",
|
||||
"loop_track": "Uudelleentoista kappale",
|
||||
"repeat_playlist": "Toista soittolista uudelleen",
|
||||
"queue": "Jono",
|
||||
"alternative_track_sources": "Toinen kappale lähde",
|
||||
"download_track": "Lataa kappale",
|
||||
"tracks_in_queue": "{tracks} kappaletta jonossa",
|
||||
"clear_all": "Tyhjennä kaikki",
|
||||
"show_hide_ui_on_hover": "Näytä/Piilota UI leijumalla",
|
||||
"always_on_top": "Aina päällimmäisenä",
|
||||
"exit_mini_player": "Lähde minisoittimesta",
|
||||
"download_location": "Lataus sijainti",
|
||||
"account": "Käyttäjä",
|
||||
"login_with_spotify": "Kirjaudu Spotify-käyttäjällä",
|
||||
"connect_with_spotify": "Yhdistä Spotify:lla",
|
||||
"logout": "Kirjaudu ulos",
|
||||
"logout_of_this_account": "Kirjaudu ulos tältä käyttäjältä",
|
||||
"language_region": "Kieli ja Maa",
|
||||
"language": "Kieli",
|
||||
"system_default": "Järjestelmän oletus",
|
||||
"market_place_region": "Markkina-alue",
|
||||
"recommendation_country": "Suositeltu maa",
|
||||
"appearance": "Ulkomuto",
|
||||
"layout_mode": "Asettelutila",
|
||||
"override_layout_settings": "Jätä reagoiva asettelutila huomioimatta",
|
||||
"adaptive": "Mukautuva",
|
||||
"compact": "Kompakti",
|
||||
"extended": "Laajennettu",
|
||||
"theme": "Teema",
|
||||
"dark": "Tumma",
|
||||
"light": "Vaalea",
|
||||
"system": "Järjestelmä",
|
||||
"accent_color": "Korostusväri",
|
||||
"sync_album_color": "Synkronoi albumin väri",
|
||||
"sync_album_color_description": "Käyttää albumin kansitaiteen vallitsevaa väirä korostuvärinä",
|
||||
"playback": "Toisto",
|
||||
"audio_quality": "Äänenlaatu",
|
||||
"high": "Korkea",
|
||||
"low": "Matala",
|
||||
"pre_download_play": "Esilataa ja soita",
|
||||
"pre_download_play_description": "Audion suoratoiston sijaan, lataa tavut ja soita ne (Suositeltu korkeamman kaistanleveyden käyttäjille)",
|
||||
"skip_non_music": "Ohita ei-musiikki kohdat (SponsorBlock)",
|
||||
"blacklist_description": "Mustalistat kappaleet aja artistit",
|
||||
"wait_for_download_to_finish": "Odota nykyisen latauksen lopetteluun",
|
||||
"desktop": "Työpöytä",
|
||||
"close_behavior": "Sulkemisen käyttäytyminen",
|
||||
"close": "Sulje",
|
||||
"minimize_to_tray": "Minimisoi tehtäväpalkkiin",
|
||||
"show_tray_icon": "Näytä järjestelmäkuvake",
|
||||
"about": "Tietoa",
|
||||
"u_love_spotube": "Tiedämme että rakastat Spotubea",
|
||||
"check_for_updates": "Tarkista päivitykset",
|
||||
"about_spotube": "Tietoa Spotube:sta",
|
||||
"blacklist": "Mustalista",
|
||||
"please_sponsor": "Sponsoroi/Lahjoita, kiitos",
|
||||
"spotube_description": "Spotube, kevyt, cross-platform, vapaa-kaikille spotify clientti",
|
||||
"version": "Versio",
|
||||
"build_number": "Rakennusnumero",
|
||||
"founder": "Perustaja",
|
||||
"repository": "Arkisto",
|
||||
"bug_issues": "Bugit+Ongelmat",
|
||||
"made_with": "Tehty ❤️ Bangladeshista 🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Lisenssi",
|
||||
"add_spotify_credentials": "Lisää Spotify-tunnuksesi aloittaaksesi",
|
||||
"credentials_will_not_be_shared_disclaimer": "Älä huoli, tunnuksiasi ei talleteta tai jaeta kenenkään kanssa",
|
||||
"know_how_to_login": "Etkö tiedä miten tehdä tämä?",
|
||||
"follow_step_by_step_guide": "Seuraa askel askeleelta opasta",
|
||||
"spotify_cookie": "Spotify {name} Keksi",
|
||||
"cookie_name_cookie": "{name} Keksi",
|
||||
"fill_in_all_fields": "Täytä kaikki kentät",
|
||||
"submit": "Lähetä",
|
||||
"exit": "Poistu",
|
||||
"previous": "Edellinen",
|
||||
"next": "Seuraava",
|
||||
"done": "Tehty",
|
||||
"step_1": "Vaihe 1",
|
||||
"first_go_to": "Ensiksi, mene",
|
||||
"login_if_not_logged_in": "ja Kirjaudu/Tee tili jos et ole kirjautunut sisään",
|
||||
"step_2": "Vaihe 2",
|
||||
"step_2_steps": "1. Kun olet kirjautunut, paina F12 tai oikeaa hiiren näppäintä > Tarkista ja avaa selaimen kehittäjä työkalut.\n2. Mene sitten \"Application\"-välilehteen (Chrome, Edge, Brave jne..) tai \"Storage\"-välilehteen (Firefox, Palemoon jne..)\n3. Mene \"Cookies\"-osastoon, sitten \"https://accounts.spotify.com\" alakohtaan.",
|
||||
"step_3": "Vaihe 3",
|
||||
"step_3_steps": "Kopioi Keksin \"sp_dc\" arvo",
|
||||
"success_emoji": "Onnistuit🥳",
|
||||
"success_message": "Olet nyt kirjautunut sisään Spotify-käyttäjällesi. Hyvää työtä toveri!",
|
||||
"step_4": "Vaihe 4",
|
||||
"step_4_steps": "Liitä kopioitu \"sp_dc\" arvo",
|
||||
"something_went_wrong": "Jotain meni pieleen",
|
||||
"piped_instance": "Johdettu palvelinesiintymä",
|
||||
"piped_description": "Johdettu palvelinesiintymä Kappale täsmäyksiin",
|
||||
"piped_warning": "Jotkut niistä eivät toimi hyvin, käytä siis omalla vastuullasi",
|
||||
"generate_playlist": "Tuota soittolista",
|
||||
"track_exists": "Kappale {track} on jo olemassa!",
|
||||
"replace_downloaded_tracks": "Korvaa kaikki ladatut kappaleet",
|
||||
"skip_download_tracks": "Ohita ladattujen laulujen lataaminen",
|
||||
"do_you_want_to_replace": "Haluatko korvata olemassa olevan kappaleen??",
|
||||
"replace": "Korvaa",
|
||||
"skip": "Ohita",
|
||||
"select_up_to_count_type": "Valitse enintään {count} {type}",
|
||||
"select_genres": "Valitse Genret",
|
||||
"add_genres": "Lisää Genrejä",
|
||||
"country": "Maa",
|
||||
"number_of_tracks_generate": "Numero tuotettavia kappaleita",
|
||||
"acousticness": "Akustisuus",
|
||||
"danceability": "Tanssittavuus",
|
||||
"energy": "Energia",
|
||||
"instrumentalness": "Instrumentaalisuus",
|
||||
"liveness": "Elävyyttä",
|
||||
"loudness": "Äänekkyys",
|
||||
"speechiness": "Puheisuus",
|
||||
"valence": "Valenssi",
|
||||
"popularity": "Suosio",
|
||||
"key": "Sävellaji",
|
||||
"duration": "Pituus (s)",
|
||||
"tempo": "Tempo (BPM)",
|
||||
"mode": "Tila",
|
||||
"time_signature": "Aikamerkki",
|
||||
"short": "Lyhyt",
|
||||
"medium": "Keskikokoinen",
|
||||
"long": "Pitkä",
|
||||
"min": "Minimi",
|
||||
"max": "Maximi",
|
||||
"target": "Kohde",
|
||||
"moderate": "Kohtalainen",
|
||||
"deselect_all": "Poista kaikki valinnat",
|
||||
"select_all": "Valitse kaikki",
|
||||
"are_you_sure": "Oletko varma?",
|
||||
"generating_playlist": "Luodaan mukautettua soittolistoa...",
|
||||
"selected_count_tracks": "Valittu {count} kappaletta",
|
||||
"download_warning": "Jos lataat kaikki laulut kerrällä olet selkeästi Piratoimassa ja aiheuttamassa vahinkoa musiikin luovaan yhteiskuntaan. Toivottavasti olet tietoinen tästä. Yritä aina kunnioittaa ja tukea Artistin kovaa työtä.",
|
||||
"download_ip_ban_warning": "BTW, YouTube voi estää IP-Osoitteesi tavallista liiallisten latauspyyntöjen takia. IP-Osoitteen esto tarkoittaa sitä, ettet voi käyttää YouTubea (vaikka olisit kirjautunut) vähintään 2-3kk aikana kyseiseltä laitteelta. Spotube ei kanna yhtään vastuuta jos se tapahtuu.",
|
||||
"by_clicking_accept_terms": "Painamalla 'hyväksy' hyväksyt seuraaviin ehtoihin:",
|
||||
"download_agreement_1": "Tiedän että Piratoin musiikkia. Olen paha.",
|
||||
"download_agreement_2": "Tuen Artisteja silloin kun pystyn, ja teen tämän vain koska minulla ei ole rahaa ostaa heidän taidetta",
|
||||
"download_agreement_3": "Ymmärrän että minun YouTube voi estää IP-Osoitteeni ja en pidä Spotubea tai omistajiinsa/avustajia vastuullisena mistään omista teoistsani",
|
||||
"decline": "Hylkää",
|
||||
"accept": "Hyväksy",
|
||||
"details": "Yksityiskohdat",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Kanava",
|
||||
"likes": "Tykkäykset",
|
||||
"dislikes": "Epä-tykkäykset",
|
||||
"views": "Näyttökerrat",
|
||||
"streamUrl": "Suoratoiston URL",
|
||||
"stop": "Lopeta",
|
||||
"sort_newest": "Suodata uusimmista",
|
||||
"sort_oldest": "Suodata vanhimmista",
|
||||
"sleep_timer": "Uniajastin",
|
||||
"mins": "{minutes} Minuuttia",
|
||||
"hours": "{hours} Tuntia",
|
||||
"hour": "{hours} Tunti",
|
||||
"custom_hours": "Mukautetut tunnit",
|
||||
"logs": "Lokit",
|
||||
"developers": "Kehittäjät",
|
||||
"not_logged_in": "Et ole kirjautunut sisään.",
|
||||
"search_mode": "Hakutila",
|
||||
"audio_source": "Äänilähde",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Salaaminen epäonnistui",
|
||||
"encryption_failed_warning": "Spotube käyttää salausta tallentaakseen tietosi, mutta epäonnistui, joten se palaa epäturvalliseen tallennukseen\nJos käytät Linuxia, varmista että sinulla on turvallisuuspalvelu (gnome-keyring, kde-wallet, keepassxc jne) asennettu",
|
||||
"querying_info": "Hankitaan tietoa...",
|
||||
"piped_api_down": "Johdettu palvelinesiintymä on alhaalla",
|
||||
"piped_down_error_instructions": "Johdettu palvelinesiintymä {pipedInstance} on alhaalla.\n\nVaihda joko ilmeytymä tia vahda 'API tyyppi' YouTuben viralliseen API\n\nKäynnistä sovellus uudestaan vaihdon jälkeen",
|
||||
"you_are_offline": "Et ole yhdistetty verkkoon",
|
||||
"connection_restored": "Verkkoyhteys palautettu",
|
||||
"use_system_title_bar": "Käytä järjestelmäpalkkia",
|
||||
"crunching_results": "Paloitellaan tuloksia...",
|
||||
"search_to_get_results": "Hae saadakseen tuloksia",
|
||||
"use_amoled_mode": "Pilkkopimeä tumma teema",
|
||||
"pitch_dark_theme": "AMOLED Tila",
|
||||
"normalize_audio": "Normalisoi audio",
|
||||
"change_cover": "Vaihda koveri",
|
||||
"add_cover": "Lisää koveri",
|
||||
"restore_defaults": "Palauta oletukset",
|
||||
"download_music_codec": "Ladatun musiikin codefc",
|
||||
"streaming_music_codec": "Suoratoistetun musiikin codec",
|
||||
"login_with_lastfm": "Kirjaudu sisään Last.fm:llä",
|
||||
"connect": "Yhdistä",
|
||||
"disconnect_lastfm": "Katkaise Last.fm",
|
||||
"disconnect": "Katkaise",
|
||||
"username": "Käyttäjänimi",
|
||||
"password": "Salasana",
|
||||
"login": "Kirjaudu",
|
||||
"login_with_your_lastfm": "Kirjaudu Last.fm käyttäjälläsi",
|
||||
"scrobble_to_lastfm": "Scrobble Last.fm:ään",
|
||||
"go_to_album": "Mene albumiin",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Selaa kaikki",
|
||||
"genres": "Genret",
|
||||
"explore_genres": "Seikkaile genrejä",
|
||||
"friends": "Kaverit",
|
||||
"no_lyrics_available": "Anteeksi, emme löytäneet lyriikoita tälle laululle",
|
||||
"start_a_radio": "Aloita Radio",
|
||||
"how_to_start_radio": "Kuinka haluat aloittaa radion?",
|
||||
"replace_queue_question": "Haluatko korvata nykyisen jonon vai lisätä siihen?",
|
||||
"endless_playback": "Loputon toisto",
|
||||
"delete_playlist": "Poista soittolista",
|
||||
"delete_playlist_confirmation": "Oletko varma että haluat poistaa tämän soittolistan?",
|
||||
"local_tracks": "Paikalliset kappaleet",
|
||||
"song_link": "Laulun linkki",
|
||||
"skip_this_nonsense": "Ohita tämä hölynpöly",
|
||||
"freedom_of_music": "“Musiikin vapaus”",
|
||||
"freedom_of_music_palm": "“Musiikin vapaus käsissäsi”",
|
||||
"get_started": "Aloitetaan",
|
||||
"youtube_source_description": "Suositeltu ja toimii parhaiten.",
|
||||
"piped_source_description": "Tuntuuko vapaalta? Sama kuin YouTube mutta paljon vapautta",
|
||||
"jiosaavn_source_description": "Paras Etelä-Aasian alueelle.",
|
||||
"highest_quality": "Korkein laatu: {quality}",
|
||||
"select_audio_source": "Valitse äänilähde",
|
||||
"endless_playback_description": "Lisää automaattisesti uusia lauluja\njonon perään",
|
||||
"choose_your_region": "Valitse alueesi",
|
||||
"choose_your_region_description": "Tämä auttaa Spotube näyttämään sinulle oikeaa sisältöä\nsijaintiasi varten.",
|
||||
"choose_your_language": "Valitse kielesi",
|
||||
"help_project_grow": "Auta tätä projektia kasvamaan",
|
||||
"help_project_grow_description": "Spotube projekti minkä lähdekoodi on julkisesti saatavilla. Voit autta tätä projektia kasvamaan muutoksilla, ilmoittamalla bugeista, tai ehdottamalla uusia ominaisuuksia.",
|
||||
"contribute_on_github": "Auta GitHub:ssa",
|
||||
"donate_on_open_collective": "Lahjoita avoimessa kollektiivissa",
|
||||
"browse_anonymously": "Selaa anonyyminä",
|
||||
"enable_connect": "Ota käyttöön yhdistäminen",
|
||||
"enable_connect_description": "Ohjaa Spotubea toiselta laitteelta",
|
||||
"devices": "Laitteet",
|
||||
"select": "Valitse",
|
||||
"connect_client_alert": "{client} ohjaa sinua",
|
||||
"this_device": "Tämä laite",
|
||||
"remote": "Etä"
|
||||
}
|
||||
324
lib/l10n/app_id.arb
Normal file
324
lib/l10n/app_id.arb
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"guest": "Tamu",
|
||||
"browse": "Jelajahi",
|
||||
"search": "Cari",
|
||||
"library": "Pustaka",
|
||||
"lyrics": "Lirik",
|
||||
"settings": "Pengaturan",
|
||||
"genre_categories_filter": "Urutkan kategori atau genre...",
|
||||
"genre": "Genre",
|
||||
"personalized": "Dipersonalisasi",
|
||||
"featured": "Unggulan",
|
||||
"new_releases": "Rilis Terbaru",
|
||||
"songs": "Lagu",
|
||||
"playing_track": "Memutar {track}",
|
||||
"queue_clear_alert": "Ini akan menghapus antrian saat ini This will clear the current queue. {track_length} trek akan dihapus\nAnda ingin melanjutkan?",
|
||||
"load_more": "Lebih Banyak",
|
||||
"playlists": "Daftar Putar",
|
||||
"artists": "Artis",
|
||||
"albums": "Album",
|
||||
"tracks": "Trek",
|
||||
"downloads": "Unduhan",
|
||||
"filter_playlists": "Urutkan daftar putar Anda...",
|
||||
"liked_tracks": "Lagu Yang Disukai",
|
||||
"liked_tracks_description": "Semua lagu yang Anda sukai",
|
||||
"create_playlist": "Buat Daftar Putar",
|
||||
"create_a_playlist": "Buat daftar putar",
|
||||
"update_playlist": "Ubah daftar putar",
|
||||
"create": "Buat",
|
||||
"cancel": "Batal",
|
||||
"update": "Ubah",
|
||||
"playlist_name": "Nama Daftar Putar",
|
||||
"name_of_playlist": "Nama daftar putar",
|
||||
"description": "Deskripsi",
|
||||
"public": "Publik",
|
||||
"collaborative": "Kolaboratif",
|
||||
"search_local_tracks": "Cari trek lokal...",
|
||||
"play": "Putar",
|
||||
"delete": "Hapus",
|
||||
"none": "Tidak Ada",
|
||||
"sort_a_z": "Urutkan berdasarkan A-Z",
|
||||
"sort_z_a": "Urutkan berdasarkan Z-A",
|
||||
"sort_artist": "Urutkan berdasarkan Artis",
|
||||
"sort_album": "Urutkan berdasarkan Album",
|
||||
"sort_duration": "Urutkan berdasarkan Durasi",
|
||||
"sort_tracks": "Urutkan trek",
|
||||
"currently_downloading": "Sedang Mengunduh ({tracks_length})",
|
||||
"cancel_all": "Batalkan Semua",
|
||||
"filter_artist": "Urutkan artis...",
|
||||
"followers": "{followers} Pengikut",
|
||||
"add_artist_to_blacklist": "Tambah artis ke daftar hitam",
|
||||
"top_tracks": "Lagu Teratas",
|
||||
"fans_also_like": "Penggemar juga menyukainya",
|
||||
"loading": "Memuat...",
|
||||
"artist": "Artis",
|
||||
"blacklisted": "Masuk Daftar Hitam",
|
||||
"following": "Mengikuti",
|
||||
"follow": "Ikuti",
|
||||
"artist_url_copied": "URL artis telah disalin",
|
||||
"added_to_queue": "Menambah trek {tracks} ke antrean",
|
||||
"filter_albums": "Urutkan album...",
|
||||
"synced": "Disinkronkan",
|
||||
"plain": "Normal",
|
||||
"shuffle": "Acak",
|
||||
"search_tracks": "Cari trek...",
|
||||
"released": "Dirilis",
|
||||
"error": "Kesalahan {error}",
|
||||
"title": "Judul",
|
||||
"time": "Waktu",
|
||||
"more_actions": "Tindakan Lainnya",
|
||||
"download_count": "Unduhan ({count})",
|
||||
"add_count_to_playlist": "Menambah ({count}) ke Daftar Putar",
|
||||
"add_count_to_queue": "Menambah ({count}) ke Antrian",
|
||||
"play_count_next": "Mainkan ({count}) selanjutnya",
|
||||
"album": "Album",
|
||||
"copied_to_clipboard": "{data} telah disalin",
|
||||
"add_to_following_playlists": "Menambah {track} ke Daftar Putar berikut",
|
||||
"add": "Tambah",
|
||||
"added_track_to_queue": "Menambah {track} ke antrian",
|
||||
"add_to_queue": "Tambah ke antrian",
|
||||
"track_will_play_next": "{track} akan diputar berikutnya",
|
||||
"play_next": "Mainkan selanjutnya",
|
||||
"removed_track_from_queue": "Menghapus {track} dari antrian",
|
||||
"remove_from_queue": "Hapus dari antrian",
|
||||
"remove_from_favorites": "Hapus dari favorit",
|
||||
"save_as_favorite": "Simpan sebagai favorit",
|
||||
"add_to_playlist": "Tambah ke daftar putar",
|
||||
"remove_from_playlist": "Hapus dari daftar putar",
|
||||
"add_to_blacklist": "Tambah ke daftar hitam",
|
||||
"remove_from_blacklist": "Hapus dari daftar hitam",
|
||||
"share": "Bagikan",
|
||||
"mini_player": "Pemutar Mini",
|
||||
"slide_to_seek": "Geser untuk maju atau mundur",
|
||||
"shuffle_playlist": "Acak daftar putar",
|
||||
"unshuffle_playlist": "Batalkan pengacakan daftar putar",
|
||||
"previous_track": "Lagu sebelumnya",
|
||||
"next_track": "Lagu berikutnya",
|
||||
"pause_playback": "Jeda Pemutaran",
|
||||
"resume_playback": "Lanjutkan Pemutaran",
|
||||
"loop_track": "Ulangi Pemutaran",
|
||||
"repeat_playlist": "Ulangi daftar putar",
|
||||
"queue": "Antrian",
|
||||
"alternative_track_sources": "Sumber trek alternatif",
|
||||
"download_track": "Unduh lagu",
|
||||
"tracks_in_queue": "{tracks} trek dalam antrian",
|
||||
"clear_all": "Bersihkan semua",
|
||||
"show_hide_ui_on_hover": "Tampil/Sembunyikan UI saat mengarahkan kursor",
|
||||
"always_on_top": "Selalu di atas",
|
||||
"exit_mini_player": "Keluar Pemutar Mini",
|
||||
"download_location": "Lokasi unduhan",
|
||||
"account": "Akun",
|
||||
"login_with_spotify": "Masuk dengan Spotify",
|
||||
"connect_with_spotify": "Hubungkan dengan Spotify",
|
||||
"logout": "Keluar",
|
||||
"logout_of_this_account": "Keluar dari akun",
|
||||
"language_region": "Bahasa & Wilayah",
|
||||
"language": "Bahasa",
|
||||
"system_default": "Bawaan Sistem",
|
||||
"market_place_region": "Wilayah Pasar",
|
||||
"recommendation_country": "Negara Rekomendasi",
|
||||
"appearance": "Tampilan",
|
||||
"layout_mode": "Mode Tata Letak",
|
||||
"override_layout_settings": "Ganti pengaturan mode tata letak responsif",
|
||||
"adaptive": "Adaptif",
|
||||
"compact": "Ringkas",
|
||||
"extended": "Diperluas",
|
||||
"theme": "Tema",
|
||||
"dark": "Gelap",
|
||||
"light": "Terang",
|
||||
"system": "Sistem",
|
||||
"accent_color": "Warna Aksen",
|
||||
"sync_album_color": "Sinkronkan warna album",
|
||||
"sync_album_color_description": "Menggunakan warna dominan sampul album sebagai warna aksen",
|
||||
"playback": "Pemutaran",
|
||||
"audio_quality": "Kualitas Suara",
|
||||
"high": "Tinggi",
|
||||
"low": "Rendah",
|
||||
"pre_download_play": "Unduh dan putar",
|
||||
"pre_download_play_description": "Daripada streaming audio, unduh byte dan mainkan (Direkomendasikan untuk pengguna bandwidth yang lebih tinggi)",
|
||||
"skip_non_music": "Lewati segmen non-musik (SponsorBlock)",
|
||||
"blacklist_description": "Lagu dan artis di daftar hitam",
|
||||
"wait_for_download_to_finish": "Tunggu hingga unduhan saat ini selesai",
|
||||
"desktop": "Desktop",
|
||||
"close_behavior": "Tutup Perilaku",
|
||||
"close": "Tutup",
|
||||
"minimize_to_tray": "Perkecil ke tray",
|
||||
"show_tray_icon": "Tampilkan tray ikon sistem",
|
||||
"about": "Tentang",
|
||||
"u_love_spotube": "Kami tahu Anda menyukai Spotube",
|
||||
"check_for_updates": "Periksa pembaruan",
|
||||
"about_spotube": "Tentang Spotube",
|
||||
"blacklist": "Daftar Hitam",
|
||||
"please_sponsor": "Silakan Sponsor/Menyumbang",
|
||||
"spotube_description": "Spotube, klien Spotify yang ringan, lintas platform, dan gratis untuk semua",
|
||||
"version": "Versi",
|
||||
"build_number": "Nomor Pembuatan",
|
||||
"founder": "Pendiri",
|
||||
"repository": "Repositori",
|
||||
"bug_issues": "Bug+Masalah",
|
||||
"made_with": "Dibuat dengan ❤️ di Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Lisensi",
|
||||
"add_spotify_credentials": "Tambahkan kredensial Spotify Anda untuk memulai",
|
||||
"credentials_will_not_be_shared_disclaimer": "Jangan khawatir, kredensial Anda tidak akan dikumpulkan atau dibagikan kepada siapa pun",
|
||||
"know_how_to_login": "Tidak tahu bagaimana melakukan ini?",
|
||||
"follow_step_by_step_guide": "Ikuti panduan Langkah demi Langkah",
|
||||
"spotify_cookie": "Spotify {name} Cookie",
|
||||
"cookie_name_cookie": "{name} Cookie",
|
||||
"fill_in_all_fields": "Silakan isi semua kolom",
|
||||
"submit": "Kirim",
|
||||
"exit": "Keluar",
|
||||
"previous": "Sebelumnya",
|
||||
"next": "Berikutnya",
|
||||
"done": "Selesai",
|
||||
"step_1": "Langkah 1",
|
||||
"first_go_to": "Pertama, Pergi ke",
|
||||
"login_if_not_logged_in": "dan Masuk/Daftar jika Anda belum masuk",
|
||||
"step_2": "Langkah 2",
|
||||
"step_2_steps": "1. Setelah Anda masuk, tekan F12 atau Klik Kanan Mouse > Buka Browser Devtools.\n2. Lalu buka Tab \"Aplikasi\" (Chrome, Edge, Brave, dll.) atau Tab \"Penyimpanan\" (Firefox, Palemoon, dll.)\n3. Buka bagian \"Cookie\" lalu subbagian \"https://accounts.spotify.com\"",
|
||||
"step_3": "Langkah 3",
|
||||
"step_3_steps": "Salin nilai Cookie \"sp_dc\" ",
|
||||
"success_emoji": "Berhasil🥳",
|
||||
"success_message": "Sekarang Anda telah berhasil Masuk dengan akun Spotify Anda. Kerja bagus, sobat!",
|
||||
"step_4": "Langkah 4",
|
||||
"step_4_steps": "Tempel nilai \"sp_dc\" yang disalin",
|
||||
"something_went_wrong": "Terjadi kesalahan",
|
||||
"piped_instance": "Piped Server Instance",
|
||||
"piped_description": "The Piped server instance untuk digunakan sebagai pencocokan trek",
|
||||
"piped_warning": "Beberapa di antaranya mungkin tidak berfungsi dengan baik. Jadi gunakan dengan risiko Anda sendiri",
|
||||
"generate_playlist": "Hasilkan Daftar Putar",
|
||||
"track_exists": "Lagu {track} sudah ada",
|
||||
"replace_downloaded_tracks": "Ganti semua trek yang diunduh",
|
||||
"skip_download_tracks": "Lewati pengunduhan semua trek yang diunduh",
|
||||
"do_you_want_to_replace": "Apakah Anda ingin mengganti track yang ada?",
|
||||
"replace": "Ganti",
|
||||
"skip": "Lewati",
|
||||
"select_up_to_count_type": "Pilih hingga {count} {type}",
|
||||
"select_genres": "Pilih Genre",
|
||||
"add_genres": "Tambah Genre",
|
||||
"country": "Negara",
|
||||
"number_of_tracks_generate": "Jumlah trek yang akan dihasilkan",
|
||||
"acousticness": "Akustik",
|
||||
"danceability": "Menari",
|
||||
"energy": "Energi",
|
||||
"instrumentalness": "Instrumentalitas",
|
||||
"liveness": "Kehidupan",
|
||||
"loudness": "Kekerasan",
|
||||
"speechiness": "Berbicara",
|
||||
"valence": "Valensi",
|
||||
"popularity": "Popularitas",
|
||||
"key": "Kunci",
|
||||
"duration": "Durasi (s)",
|
||||
"tempo": "Tempo (BPM)",
|
||||
"mode": "Mode",
|
||||
"time_signature": "Tanda Tangan Waktu",
|
||||
"short": "Pendek",
|
||||
"medium": "Sedang",
|
||||
"long": "Panjang",
|
||||
"min": "Minimal",
|
||||
"max": "Maksimal",
|
||||
"target": "Target",
|
||||
"moderate": "Sedang",
|
||||
"deselect_all": "Batalkan Semua",
|
||||
"select_all": "Pilih Semua",
|
||||
"are_you_sure": "Anda yakin?",
|
||||
"generating_playlist": "Menghasilkan daftar putar khusus Anda...",
|
||||
"selected_count_tracks": "{count} lagu yang dipilih",
|
||||
"download_warning": "Jika Anda mengunduh semua Lagu secara massal, Anda jelas membajak Musik & menyebabkan kerusakan pada masyarakat kreatif Musik. Saya harap Anda menyadari hal ini. Selalu berusaha menghormati & mendukung kerja keras Artis",
|
||||
"download_ip_ban_warning": "BTW, IP Anda bisa diblokir di YouTube karena permintaan unduhan yang berlebihan dari biasanya. Blokir IP berarti Anda tidak dapat menggunakan YouTube (meskipun Anda masuk) setidaknya selama 2-3 bulan dari perangkat IP tersebut. Dan Spotube tidak bertanggung jawab jika hal ini terjadi",
|
||||
"by_clicking_accept_terms": "Dengan mengklik 'terima' Anda menyetujui ketentuan berikut:",
|
||||
"download_agreement_1": "Saya tahu saya membajak Musik. Saya buruk",
|
||||
"download_agreement_2": "Saya akan mendukung Artis di mana pun saya bisa dan saya melakukan ini hanya karena saya tidak punya uang untuk membeli karya seni mereka",
|
||||
"download_agreement_3": "Saya sepenuhnya menyadari bahwa IP saya dapat diblokir di YouTube & saya tidak menganggap Spotube atau pemilik/kontributornya bertanggung jawab atas kecelakaan apa pun yang disebabkan oleh tindakan saya saat ini",
|
||||
"decline": "Menolak",
|
||||
"accept": "Setuju",
|
||||
"details": "Detail",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Channel",
|
||||
"likes": "Suka",
|
||||
"dislikes": "Tidak Suka",
|
||||
"views": "Dilihat",
|
||||
"streamUrl": "URL Stream",
|
||||
"stop": "Berhenti",
|
||||
"sort_newest": "Urutkan yang baru ditambah",
|
||||
"sort_oldest": "Urutkan yang paling lama ditambah",
|
||||
"sleep_timer": "Pengatur Waktu Tidur",
|
||||
"mins": "{minutes} Menit",
|
||||
"hours": "{hours} Jam",
|
||||
"hour": "{hours} Jam",
|
||||
"custom_hours": "Jam Kostum",
|
||||
"logs": "Log",
|
||||
"developers": "Pengembang",
|
||||
"not_logged_in": "Anda belum masuk",
|
||||
"search_mode": "Mode Pencarian",
|
||||
"audio_source": "Sumber Suara",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Gagal mengenkripsi",
|
||||
"encryption_failed_warning": "Spotube menggunakan enkripsi untuk menyimpan data Anda dengan aman. Namun gagal melakukannya. Jadi itu akan kembali ke penyimpanan yang tidak aman\nJika Anda menggunakan linux, pastikan Anda telah menginstal layanan rahasia (gnome-keyring, kde-wallet, keepassxc, dll)",
|
||||
"querying_info": "Mencari informasi...",
|
||||
"piped_api_down": "Piped API tidak aktif",
|
||||
"piped_down_error_instructions": "Piped Instance {pipedInstance} saat ini tidak aktif\n\nUbah instance atau ubah 'jenis API' menjadi API YouTube resmi\n\nPastikan untuk memulai ulang aplikasi setelah perubahan",
|
||||
"you_are_offline": "Anda sedang offline",
|
||||
"connection_restored": "Koneksi internet Anda telah pulih",
|
||||
"use_system_title_bar": "Gunakan bilah judul sistem",
|
||||
"crunching_results": "Mengolah hasil...",
|
||||
"search_to_get_results": "Cari untuk mendapatkan hasil",
|
||||
"use_amoled_mode": "Tema gelap gulita",
|
||||
"pitch_dark_theme": "Mode AMOLED",
|
||||
"normalize_audio": "Normalisasi audio",
|
||||
"change_cover": "Ganti sampul",
|
||||
"add_cover": "Tambah sampul",
|
||||
"restore_defaults": "Kembalikan semula",
|
||||
"download_music_codec": "Unduh codec musik",
|
||||
"streaming_music_codec": "Streaming codec musik",
|
||||
"login_with_lastfm": "Masuk dengan Last.fm",
|
||||
"connect": "Hubungkan",
|
||||
"disconnect_lastfm": "Memutuskan Last.fm",
|
||||
"disconnect": "Memutuskan",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"login": "Masuk",
|
||||
"login_with_your_lastfm": "Masuk dengan Last.fm Anda",
|
||||
"scrobble_to_lastfm": "Scrobble ke Last.fm",
|
||||
"go_to_album": "Pergi ke Album",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Lihat Semua",
|
||||
"genres": "Genre",
|
||||
"explore_genres": "Jelajahi Genre",
|
||||
"friends": "Daftar Teman",
|
||||
"no_lyrics_available": "Maaf, tidak dapat menemukan lirik untuk lagu ini",
|
||||
"start_a_radio": "Putar Radio",
|
||||
"how_to_start_radio": "Bagaimana Anda ingin memutar radio?",
|
||||
"replace_queue_question": "Apakah Anda ingin mengganti antrean saat ini atau menambahkannya?",
|
||||
"endless_playback": "Pemutaran Tanpa Akhir",
|
||||
"delete_playlist": "Hapus Daftar Putar",
|
||||
"delete_playlist_confirmation": "Anda yakin ingin menghapus daftar putar ini?",
|
||||
"local_tracks": "Trek Lokal",
|
||||
"song_link": "Tautan Lagu",
|
||||
"skip_this_nonsense": "Lewati omong kosong ini",
|
||||
"freedom_of_music": "“Kebebasan Musik”",
|
||||
"freedom_of_music_palm": "“Kebebasan Musik di telapak tangan Anda”",
|
||||
"get_started": "Mari kita mulai",
|
||||
"youtube_source_description": "Direkomendasikan dan berfungsi paling baik.",
|
||||
"piped_source_description": "Merasa bebas? Sama seperti YouTube tetapi banyak yang gratis.",
|
||||
"jiosaavn_source_description": "Terbaik untuk wilayah Asia Selatan.",
|
||||
"highest_quality": "Kualitas Terbaik: {quality}",
|
||||
"select_audio_source": "Pilih Sumber Suara",
|
||||
"endless_playback_description": "Tambahkan lagu baru secara otomatis\nke akhir antrean",
|
||||
"choose_your_region": "Pilih wilayah Anda",
|
||||
"choose_your_region_description": "Ini akan membantu Spotube menampilkan konten yang tepat\nuntuk lokasi Anda.",
|
||||
"choose_your_language": "Pilih bahasa Anda",
|
||||
"help_project_grow": "Bantu proyek ini berkembang",
|
||||
"help_project_grow_description": "Spotube adalah proyek sumber terbuka. Anda dapat membantu proyek ini berkembang dengan berkontribusi pada proyek, melaporkan bug, atau menyarankan fitur baru.",
|
||||
"contribute_on_github": "Berkontribusi di GitHub",
|
||||
"donate_on_open_collective": "Donasi di Open Collective",
|
||||
"browse_anonymously": "Jelajahi Secara Anonim",
|
||||
"enable_connect": "Aktifkan Hubungkan",
|
||||
"enable_connect_description": "Kontrol Spotube dari perangkat lain",
|
||||
"devices": "Perangkat",
|
||||
"select": "Pilih",
|
||||
"connect_client_alert": "Anda dikendalikan oleh {client}",
|
||||
"this_device": "Perangkat Ini",
|
||||
"remote": "Remot"
|
||||
}
|
||||
324
lib/l10n/app_ka.arb
Normal file
324
lib/l10n/app_ka.arb
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"guest": "სტუმარი",
|
||||
"browse": "ნახვა",
|
||||
"search": "ძებნა",
|
||||
"library": "ბიბლიოთეკა",
|
||||
"lyrics": "ტექსტები",
|
||||
"settings": "კონფიგურაციები",
|
||||
"genre_categories_filter": "კატეგორიების ან ჟანრების ფილტრი...",
|
||||
"genre": "ჟანრი",
|
||||
"personalized": "პეერსონალიზებული",
|
||||
"featured": "გამორჩეული",
|
||||
"new_releases": "ახალი გამოცემები",
|
||||
"songs": "სიმღერები",
|
||||
"playing_track": "უკრავს {track}",
|
||||
"queue_clear_alert": "ეს გაასუფთავებს მიმდინარე რიგს. {track_length} ტრეკი წაიშლება\nᲒინდა გააგრძელო?",
|
||||
"load_more": "მეტის ჩატვირთვა",
|
||||
"playlists": "ფლეილისტები",
|
||||
"artists": "არტისტები",
|
||||
"albums": "ალბომები",
|
||||
"tracks": "ტრეკები",
|
||||
"downloads": "ჩამოტვირთვები",
|
||||
"filter_playlists": "ფლეილისტების გაფილტვრა...",
|
||||
"liked_tracks": "მოწონებული ტრეკები",
|
||||
"liked_tracks_description": "ყველა შენი მოწონებული ტრეკი",
|
||||
"create_playlist": "ფლეილისტის შექმნა",
|
||||
"create_a_playlist": "ფლეილისტის შექმნა",
|
||||
"update_playlist": "ფლეილისტის განახლება",
|
||||
"create": "შექმნა",
|
||||
"cancel": "გაუქმება",
|
||||
"update": "განახლება",
|
||||
"playlist_name": "ფლეილისტის სახელი",
|
||||
"name_of_playlist": "ფლეილისტის სახელი",
|
||||
"description": "აღწერა",
|
||||
"public": "საჯარო",
|
||||
"collaborative": "კოლაბორაციული",
|
||||
"search_local_tracks": "ლოცალური ტრეკების ძებნა...",
|
||||
"play": "დაკვრა",
|
||||
"delete": "წაშლა",
|
||||
"none": "არცერთი",
|
||||
"sort_a_z": "დალაგება A-Z-ს მიხედვით",
|
||||
"sort_z_a": "დალაგება Z-A-ს მიხედვით",
|
||||
"sort_artist": "დალაგება არტისტის მიხედვით",
|
||||
"sort_album": "დალაგება ალბომის მიხედვით",
|
||||
"sort_duration": "დალაგება ხანგრძლივობის მიხედვით",
|
||||
"sort_tracks": "ტრეკების დალაგება",
|
||||
"currently_downloading": "მიმდინარეობს ჩამოტვირთვა ({tracks_length})",
|
||||
"cancel_all": "ყველას გაუქმება",
|
||||
"filter_artist": "არტისტების ფილტრი...",
|
||||
"followers": "{followers} ფოლოვერები",
|
||||
"add_artist_to_blacklist": "არტისტის შავ სიაში დამატება",
|
||||
"top_tracks": "ტოპ ტრეკები",
|
||||
"fans_also_like": "ფანებს ასევე მოსწონთ",
|
||||
"loading": "იტვირთება...",
|
||||
"artist": "არტისტი",
|
||||
"blacklisted": "შავ სიაში მყოფი",
|
||||
"following": "ფოლოვინგი",
|
||||
"follow": "დაფოლოვება",
|
||||
"artist_url_copied": "არტისტის ლინკი დაკოპირებულია",
|
||||
"added_to_queue": "{tracks} ტრეკი დაემატა რიგში",
|
||||
"filter_albums": "ალბომების გაფილტვრა...",
|
||||
"synced": "სინქრონიზებული",
|
||||
"plain": "Plain",
|
||||
"shuffle": "რიგის არევა",
|
||||
"search_tracks": "ტრეკების ძებნა...",
|
||||
"released": "გამოშვებული",
|
||||
"error": "შეცდომა {error}",
|
||||
"title": "სათაური",
|
||||
"time": "დრო",
|
||||
"more_actions": "მეტი მოქმედებები",
|
||||
"download_count": "გადმოწერა ({count})",
|
||||
"add_count_to_playlist": "ფლეილისტში ({count})-ის დამატება",
|
||||
"add_count_to_queue": "რიგში ({count})-ის დამატება",
|
||||
"play_count_next": "შემდეგი ({count})-ის დაკვრა",
|
||||
"album": "ალბომი",
|
||||
"copied_to_clipboard": "{data} დაკოპირებულია",
|
||||
"add_to_following_playlists": "დაამატე {track} ამ ფლეილისტებში",
|
||||
"add": "დამატება",
|
||||
"added_track_to_queue": "რიგში დაემატა {track}",
|
||||
"add_to_queue": "რიგში დამატება",
|
||||
"track_will_play_next": "{track} დაუკრავს შემდეგს",
|
||||
"play_next": "შემდეგის დაკვრა",
|
||||
"removed_track_from_queue": "რიგიდან წაიშალა {track}",
|
||||
"remove_from_queue": "რიგიდან წაშლა",
|
||||
"remove_from_favorites": "ფავორიტებიდან წაშლა",
|
||||
"save_as_favorite": "ფავორიტებში დამატება",
|
||||
"add_to_playlist": "ფლეილისტში დამატება",
|
||||
"remove_from_playlist": "ფლეილისტიდან წაშლა",
|
||||
"add_to_blacklist": "შავ სიაში დამატება",
|
||||
"remove_from_blacklist": "შავი სიიდან წაშლა",
|
||||
"share": "გაზიარება",
|
||||
"mini_player": "მინი დამკვრელი",
|
||||
"slide_to_seek": "გადახვევისთვის გაასრიალეთ წინ ან უკან",
|
||||
"shuffle_playlist": "ფლეილისტის არევა",
|
||||
"unshuffle_playlist": "ფლეილისტის დალაგება",
|
||||
"previous_track": "წინა ტრეკი",
|
||||
"next_track": "შემდეგი ტრეკი",
|
||||
"pause_playback": "დაკვრის გაჩერება",
|
||||
"resume_playback": "დაკვრის გაგრძელება",
|
||||
"loop_track": "ტრეკის ლუპზე დაკვრა",
|
||||
"repeat_playlist": "ფლეილისტის გამეორება",
|
||||
"queue": "რიგი",
|
||||
"alternative_track_sources": "ალტერნატიული ტრეკების წყაროები",
|
||||
"download_track": "გადმოწერე ტრეკი",
|
||||
"tracks_in_queue": "{tracks} ტრეკი რიგში",
|
||||
"clear_all": "ყველას წაშლა",
|
||||
"show_hide_ui_on_hover": "UI-ის ჩვენება/დამალვა ჰოვერზე",
|
||||
"always_on_top": "ტოველთვის ზემოდან",
|
||||
"exit_mini_player": "მინი დამკვრელიდან გამოსვლა",
|
||||
"download_location": "ჩამოტვირთვის მდებარეობა",
|
||||
"account": "ანგარიში",
|
||||
"login_with_spotify": "შედით თქვენი Spotify ანგარიშით",
|
||||
"connect_with_spotify": "დაუკავშირდით Spotify-ს",
|
||||
"logout": "გასვლა",
|
||||
"logout_of_this_account": "ანგარიშიდან გასვლა",
|
||||
"language_region": "ენა და რეგიონი",
|
||||
"language": "ენა",
|
||||
"system_default": "სისტემის ნაგულისხმევი",
|
||||
"market_place_region": "მარკეტფლეისის რეგიონი",
|
||||
"recommendation_country": "რეკომენდირებული ქვეყანა",
|
||||
"appearance": "გარეგნობა",
|
||||
"layout_mode": "განლაგების რეჟიმი",
|
||||
"override_layout_settings": "რესფონსივ განლაგების რეჟიმის კონფიგურაციაზე გადაწერა",
|
||||
"adaptive": "ადაპტირებული",
|
||||
"compact": "კომპაქტური",
|
||||
"extended": "გაფართოებული",
|
||||
"theme": "თემა",
|
||||
"dark": "ბნელი",
|
||||
"light": "ღია",
|
||||
"system": "სისტემის",
|
||||
"accent_color": "აქცენტის ფერი",
|
||||
"sync_album_color": "ალბომის ფერის სინქრონიზაცია",
|
||||
"sync_album_color_description": "დომინანტური ალბომის ფერის აქცენტის ფერად გამოყენება",
|
||||
"playback": "დაკვრა",
|
||||
"audio_quality": "აუდიოს ხარისხი",
|
||||
"high": "მაღალი",
|
||||
"low": "დაბალი",
|
||||
"pre_download_play": "წინასწარ ჩამოტვირთვა და დაკვრა",
|
||||
"pre_download_play_description": "აუდიოს სტრიმინგის ნაცვლად, ბაიტების ჩამოტვირთვა და დაკვრა (რეკომენდებულია უფრო მაღალი გამტარუნარიანობის მომხმარებლებისთვის)",
|
||||
"skip_non_music": "არა მუსიკალური ნაწილის გამოტოვება (სპონსორის ბლოკი)",
|
||||
"blacklist_description": "შავ სიაში მყოფი არტისტები და ტრეკები",
|
||||
"wait_for_download_to_finish": "გთხოვთ, დაელოდოთ მიმდინარე ჩამოტვირთვის დასრულებას",
|
||||
"desktop": "დესკტოპი",
|
||||
"close_behavior": "დახურვის ქცევა",
|
||||
"close": "დახურვა",
|
||||
"minimize_to_tray": "მინიმიზაცია",
|
||||
"show_tray_icon": "სისტემის აიკონის ჩვენება",
|
||||
"about": "ჩვენს შესახებ",
|
||||
"u_love_spotube": "We know you love Spotube",
|
||||
"check_for_updates": "განახლებების შემოწმება",
|
||||
"about_spotube": "Spotube-ს შესახებ",
|
||||
"blacklist": "შავი სია",
|
||||
"please_sponsor": "გთხოვთ დაგვასპონსოროთ",
|
||||
"spotube_description": "Spotube, a lightweight, cross-platform, free-for-all spotify client",
|
||||
"version": "ვერსია",
|
||||
"build_number": "Build Number",
|
||||
"founder": "დამფუძნებელი",
|
||||
"repository": "რეპოზიტორია",
|
||||
"bug_issues": "Bug+Issues",
|
||||
"made_with": "Made with ❤️ in Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "ლიცენზია",
|
||||
"add_spotify_credentials": "დასაწყებად დაამატეთ თქვენი Spotify მონაცემები",
|
||||
"credentials_will_not_be_shared_disclaimer": "არ ინერვიულოთ, თქვენი მონაცემები არ იქნება შეგროვებული ან გაზიარებული ვინმესთან",
|
||||
"know_how_to_login": "არ იცით როგორ გააკეთოთ ეს?",
|
||||
"follow_step_by_step_guide": "მიჰყევით ნაბიჯ-ნაბიჯ სახელმძღვანელოს",
|
||||
"spotify_cookie": "Spotify {name} ქუქი",
|
||||
"cookie_name_cookie": "{name} ქუქი",
|
||||
"fill_in_all_fields": "გთხოვთ შეავსოთ ყველა ველი",
|
||||
"submit": "გაგზავნა",
|
||||
"exit": "გამოსვლა",
|
||||
"previous": "წინა",
|
||||
"next": "შემდეგი",
|
||||
"done": "მზადაა",
|
||||
"step_1": "ნაბიჯი 1",
|
||||
"first_go_to": "პირველი, გადადით",
|
||||
"login_if_not_logged_in": "და შესვლა/რეგისტრაცია, თუ არ ხართ შესული",
|
||||
"step_2": "ნაბიჯი 2",
|
||||
"step_2_steps": "1. როცა შეხვალთ, დააჭირეთ F12-ს ან მაუსის მარჯვენა ღილაკს > Inspect to Open the Browser devtools.\n2. შემდეგ გახსენით \"Application\" განყოფილება (Chrome, Edge, Brave etc..) ან \"Storage\" განყოფილება (Firefox, Palemoon etc..)\n3. შედით \"Cookies\" სექციაში და შემდეგ \"https://accounts.spotify.com\" სუბსექციაში",
|
||||
"step_3": "ნაბიჯი 3",
|
||||
"step_3_steps": "დააკოპირეთ \"sp_dc\" ქუქი-ფაილის მნიშვნელობა",
|
||||
"success_emoji": "წარმატება🥳",
|
||||
"success_message": "თქვენ წარმატებით შეხვედით თქვენი Spotify ანგარიშით.",
|
||||
"step_4": "ნაბიჯი 4",
|
||||
"step_4_steps": "ჩასვით კოპირებული \"sp_dc\" მნიშვნელობა",
|
||||
"something_went_wrong": "Რაღაც არასწორად წავიდა",
|
||||
"piped_instance": "Piped Server Instance",
|
||||
"piped_description": "The Piped server instance to use for track matching",
|
||||
"piped_warning": "ზოგიერთი მათგანმა შეიძლება კარგად არ იმუშაოს. ",
|
||||
"generate_playlist": "ფლეილისტის დაგენერირება",
|
||||
"track_exists": "ტრეკი {track} უკვე არსებობს",
|
||||
"replace_downloaded_tracks": "ყველა ჩამოტვირთული ტრეკის შეცვლა",
|
||||
"skip_download_tracks": "ყველა ჩამოტვირთული ტრეკის გამოტოვება",
|
||||
"do_you_want_to_replace": "გსურთ შეცვალოთ არსებული ტრეკი??",
|
||||
"replace": "შეცვლა",
|
||||
"skip": "გამოტოვება",
|
||||
"select_up_to_count_type": "აირჩიე {count}-მდე {type}",
|
||||
"select_genres": "ჟანრების არჩევა",
|
||||
"add_genres": "ჟანრების დამატება",
|
||||
"country": "ქვეყანა",
|
||||
"number_of_tracks_generate": "დასაგენერირებელი ტრეკების რაოდენობა",
|
||||
"acousticness": "Acousticness",
|
||||
"danceability": "Danceability",
|
||||
"energy": "Energy",
|
||||
"instrumentalness": "Instrumentalness",
|
||||
"liveness": "Liveness",
|
||||
"loudness": "Loudness",
|
||||
"speechiness": "Speechiness",
|
||||
"valence": "Valence",
|
||||
"popularity": "Popularity",
|
||||
"key": "Key",
|
||||
"duration": "Duration (s)",
|
||||
"tempo": "Tempo (BPM)",
|
||||
"mode": "Mode",
|
||||
"time_signature": "Time Signature",
|
||||
"short": "Short",
|
||||
"medium": "საშუალო",
|
||||
"long": "გრძელი",
|
||||
"min": "მინიმალური",
|
||||
"max": "მაქსიმალური",
|
||||
"target": "სამიზნე",
|
||||
"moderate": "საშუალო",
|
||||
"deselect_all": "ყველა მონიშვნის გაუქმება",
|
||||
"select_all": "ყველას მონიშვნა",
|
||||
"are_you_sure": "Დარწმუნებული ხართ?",
|
||||
"generating_playlist": "მიმდინარეობს თქვენი მორგებული ფლეილისტის გენერირება...",
|
||||
"selected_count_tracks": "არჩეულია {count} ტრეკი",
|
||||
"download_warning": "If you download all Tracks at bulk you're clearly pirating Music & causing damage to the creative society of Music. I hope you are aware of this. Always, try respecting & supporting Artist's hard work",
|
||||
"download_ip_ban_warning": "BTW, your IP can get blocked on YouTube due excessive download requests than usual. IP block means you can't use YouTube (even if you're logged in) for at least 2-3 months from that IP device. And Spotube doesn't hold any responsibility if this ever happens",
|
||||
"by_clicking_accept_terms": "By clicking 'accept' you agree to following terms:",
|
||||
"download_agreement_1": "I know I'm pirating Music. I'm bad",
|
||||
"download_agreement_2": "I'll support the Artist wherever I can and I'm only doing this because I don't have money to buy their art",
|
||||
"download_agreement_3": "I'm completely aware that my IP can get blocked on YouTube & I don't hold Spotube or his owners/contributors responsible for any accidents caused by my current action",
|
||||
"decline": "უარყოფა",
|
||||
"accept": "დათანხმება",
|
||||
"details": "დეტალები",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Channel",
|
||||
"likes": "მოწონებები",
|
||||
"dislikes": "არ მოწონებები",
|
||||
"views": "ნახვები",
|
||||
"streamUrl": "სტრიმის ლინკი",
|
||||
"stop": "გაჩერება",
|
||||
"sort_newest": "ფალაგება სიახლის მიხედიტ",
|
||||
"sort_oldest": "დალაგება სიძველის მიხედვით",
|
||||
"sleep_timer": "ძილის ტაიმერი",
|
||||
"mins": "{minutes} წუთი",
|
||||
"hours": "{hours} საათი",
|
||||
"hour": "{hours} საათი",
|
||||
"custom_hours": "მორგებული საათები",
|
||||
"logs": "ლოგები",
|
||||
"developers": "დეველოპერები",
|
||||
"not_logged_in": "არ ხარ დალოგინებული",
|
||||
"search_mode": "ძებნის რეჟიმი",
|
||||
"audio_source": "აუდიოს წყარო",
|
||||
"ok": "ოკ",
|
||||
"failed_to_encrypt": "დაშიფვრა ვერ მოხერხდა",
|
||||
"encryption_failed_warning": "Spotube uses encryption to securely store your data. But failed to do so. So it'll fallback to insecure storage\nIf you're using linux, please make sure you've any secret-service (gnome-keyring, kde-wallet, keepassxc etc) installed",
|
||||
"querying_info": "Querying info...",
|
||||
"piped_api_down": "Piped API is down",
|
||||
"piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change",
|
||||
"you_are_offline": "ამჟამად ხაზგარეშე ხართ",
|
||||
"connection_restored": "თქვენი ინტერნეტ კავშირი აღდგა",
|
||||
"use_system_title_bar": "სისტემის სათაურის ზოლის გამოყენება",
|
||||
"crunching_results": "იტვირთება შედეგები...",
|
||||
"search_to_get_results": "მოძებნეთ შედეგების მისაღებად",
|
||||
"use_amoled_mode": "Pitch black dark theme",
|
||||
"pitch_dark_theme": "AMOLED Mode",
|
||||
"normalize_audio": "აუდიოს ნორმალიზება",
|
||||
"change_cover": "Ქავერის შეცვლა",
|
||||
"add_cover": "Ქავერის ფოტოს დამატება",
|
||||
"restore_defaults": "ნაგულისხმევი პარამეტრების აღდგენა",
|
||||
"download_music_codec": "მუსიკის კოდეკის გადმოწერა",
|
||||
"streaming_music_codec": "სტრიმინგ მუსიკის კოდეკი",
|
||||
"login_with_lastfm": "Last.fm-ით შესვლა",
|
||||
"connect": "დაკავშირება",
|
||||
"disconnect_lastfm": "Last.fm-იდან გამოსვლა",
|
||||
"disconnect": "გამოსვლა",
|
||||
"username": "მომხმარებელი",
|
||||
"password": "პაროლი",
|
||||
"login": "შესვლა",
|
||||
"login_with_your_lastfm": "Last.fm ანგარიშით შესვლა",
|
||||
"scrobble_to_lastfm": "Scrobble to Last.fm",
|
||||
"go_to_album": "ალბომზე გადასვლა",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "ყველას ნახვა",
|
||||
"genres": "ჟანრები",
|
||||
"explore_genres": "შეისწავლეთ ჟანრები",
|
||||
"friends": "მეგობრები",
|
||||
"no_lyrics_available": "უკაცრავად, ამ ტრეკისთვის ტექსტის პოვნა შეუძლებელია",
|
||||
"start_a_radio": "რადიოს ჩართვა",
|
||||
"how_to_start_radio": "როგორ გნებავთ რადიოს ჩართვა?",
|
||||
"replace_queue_question": "გნებავთ ჩაანაცვლოთ არსებული რიგი თუ დაამატოთ მასზე?",
|
||||
"endless_playback": "დაუსრულებელი დაკვრა",
|
||||
"delete_playlist": "ფლეილისტის წაშლა",
|
||||
"delete_playlist_confirmation": "დარწმუნებული ხართ რომ გნებავთ ფლეილისტის წაშლა?",
|
||||
"local_tracks": "ლოკალური ტრეკები",
|
||||
"song_link": "ტრეკის ლინკი",
|
||||
"skip_this_nonsense": "ამ სისულელის გამოტოვება",
|
||||
"freedom_of_music": "“მუსიკის თავისუფლება”",
|
||||
"freedom_of_music_palm": "“მუსიკის თავისუფლება შენს ხელის გულზე”",
|
||||
"get_started": "დავიწყოთ",
|
||||
"youtube_source_description": "რეკომენდებულია და მუშაობს საუკეთესოდ.",
|
||||
"piped_source_description": "თავისუფლად გრძნობთ თავს? იგივეა, რაც YouTube, მაგრამ ბევრი თავისუფალი.",
|
||||
"jiosaavn_source_description": "საუკეთესოა სამხრეთ აზიის რეგიონისთვის.",
|
||||
"highest_quality": "საუკეთესო ხარისხი: {quality}",
|
||||
"select_audio_source": "აუდიოს წყაროს არჩევა",
|
||||
"endless_playback_description": "ახალი სიმთერების ავტომატურად რიგის ბოლოში დამატება",
|
||||
"choose_your_region": "აირჩიე შენი რეგიონი",
|
||||
"choose_your_region_description": "This will help Spotube show you the right content\nfor your location.",
|
||||
"choose_your_language": "აირჩიე ენა",
|
||||
"help_project_grow": "დაეხმარეთ ამ პროექტს განვითარებაში",
|
||||
"help_project_grow_description": "Spotube is an open-source project. You can help this project grow by contributing to the project, reporting bugs, or suggesting new features.",
|
||||
"contribute_on_github": "GitHub-ზე კონტრიბუცია",
|
||||
"donate_on_open_collective": "Open Collective-ზე დონაცია",
|
||||
"browse_anonymously": "ანონიმურად ნახვა",
|
||||
"enable_connect": "დაკავშირების ჩართვა",
|
||||
"enable_connect_description": "აკონტროლე Spotube სხვა მოწყობილობებიდან",
|
||||
"devices": "მოწყობილობები",
|
||||
"select": "არჩევა",
|
||||
"connect_client_alert": "თქვენ კონტროლირებული ხართ {client} მოწყობილობით",
|
||||
"this_device": "ეს მოწყობილობა",
|
||||
"remote": "დისტანციური"
|
||||
}
|
||||
@ -3,13 +3,13 @@
|
||||
"browse": "Göz at",
|
||||
"search": "Ara",
|
||||
"library": "Kütüphane",
|
||||
"lyrics": "Şarkı Sözleri",
|
||||
"lyrics": "Şarkı sözleri",
|
||||
"settings": "Ayarlar",
|
||||
"genre_categories_filter": "Kategorileri veya türleri filtrele...",
|
||||
"genre_categories_filter": "Kategorileri veya türleri filtreleyin...",
|
||||
"genre": "Tür",
|
||||
"personalized": "Kişiselleştirilmiş",
|
||||
"featured": "Öne Çıkanlar",
|
||||
"new_releases": "Yeni Çıkanlar",
|
||||
"featured": "Öne çıkanlar",
|
||||
"new_releases": "Yeni çıkanlar",
|
||||
"songs": "Şarkılar",
|
||||
"playing_track": "{track} oynatılıyor",
|
||||
"queue_clear_alert": "Bu, mevcut kuyruğu temizleyecektir. {track_length} parça kaldırılacak\nDevam etmek istiyor musunuz?",
|
||||
@ -20,15 +20,15 @@
|
||||
"tracks": "Parçalar",
|
||||
"downloads": "İndirilenler",
|
||||
"filter_playlists": "Oynatma listelerinizi filtreleyin...",
|
||||
"liked_tracks": "Beğenilen Parçalar",
|
||||
"liked_tracks": "Beğenilen parçalar",
|
||||
"liked_tracks_description": "Beğendiğiniz tüm parçalar",
|
||||
"create_playlist": "Oynatma Listesi Oluştur",
|
||||
"create_a_playlist": "Bir oynatma listesi oluşturun",
|
||||
"create_playlist": "Oynatma listesi oluştur",
|
||||
"create_a_playlist": "Bir oynatma listesi oluştur",
|
||||
"update_playlist": "Oynatma listesini güncelle",
|
||||
"create": "Oluştur",
|
||||
"cancel": "İptal",
|
||||
"update": "Güncelle",
|
||||
"playlist_name": "Oynatma Listesi Adı",
|
||||
"playlist_name": "Oynatma listesi adı",
|
||||
"name_of_playlist": "Oynatma listesinin adı",
|
||||
"description": "Açıklama",
|
||||
"public": "Halka açık",
|
||||
@ -39,16 +39,16 @@
|
||||
"none": "Yok",
|
||||
"sort_a_z": "A - Z'ye göre sırala",
|
||||
"sort_z_a": "Z - A'ya göre sırala",
|
||||
"sort_artist": "Sanatçıya Göre Sırala",
|
||||
"sort_album": "Albüme Göre Sırala",
|
||||
"sort_duration": "Süreye Göre Sırala",
|
||||
"sort_tracks": "Parçaları Sırala",
|
||||
"currently_downloading": "Şu An İndirilenler ({tracks_length})",
|
||||
"cancel_all": "Tümünü İptal Et",
|
||||
"filter_artist": "Sanatçıları filtrele...",
|
||||
"sort_artist": "Sanatçıya göre sırala",
|
||||
"sort_album": "Albüme göre sırala",
|
||||
"sort_duration": "Süreye göre sırala",
|
||||
"sort_tracks": "Parçaları sırala",
|
||||
"currently_downloading": "Şu anda indirilenler ({tracks_length})",
|
||||
"cancel_all": "Tümünü iptal et",
|
||||
"filter_artist": "Sanatçıları filtreleyin...",
|
||||
"followers": "{followers} Takipçiler",
|
||||
"add_artist_to_blacklist": "Sanatçıyı kara listeye ekle",
|
||||
"top_tracks": "En İyi Parçalar",
|
||||
"top_tracks": "En iyi parçalar",
|
||||
"fans_also_like": "Hayranlar ayrıca şunları da beğendi",
|
||||
"loading": "Yükleniyor...",
|
||||
"artist": "Sanatçı",
|
||||
@ -57,7 +57,7 @@
|
||||
"follow": "Takip et",
|
||||
"artist_url_copied": "Sanatçı bağlantısı panoya kopyalandı",
|
||||
"added_to_queue": "Kuyruğa {tracks} parçası eklendi",
|
||||
"filter_albums": "Albümleri filtrele...",
|
||||
"filter_albums": "Albümleri filtreleyin...",
|
||||
"synced": "Senkronize edildi",
|
||||
"plain": "Sade",
|
||||
"shuffle": "Karıştır",
|
||||
@ -68,19 +68,19 @@
|
||||
"time": "Zaman",
|
||||
"more_actions": "Daha fazla eylem",
|
||||
"download_count": "İndir ({count})",
|
||||
"add_count_to_playlist": "Oynatma Listesine ({count}) ekle",
|
||||
"add_count_to_queue": "Kuyruğa ({count}) ekle",
|
||||
"play_count_next": "({count}) sonrakini oynat",
|
||||
"add_count_to_playlist": "Oynatma Listesine ekle ({count})",
|
||||
"add_count_to_queue": "Kuyruğa ekle ({count})",
|
||||
"play_count_next": "Sonrakini oynat ({count})",
|
||||
"album": "Albüm",
|
||||
"copied_to_clipboard": "{data} panoya kopyalandı",
|
||||
"add_to_following_playlists": "{track} parçasını aşağıdaki Oynatma Listelerine ekle",
|
||||
"add_to_following_playlists": "{track} parçasını aşağıdaki oynatma listelerine ekle",
|
||||
"add": "Ekle",
|
||||
"added_track_to_queue": "{track} kuyruğa eklendi",
|
||||
"add_to_queue": "Kuyruğa ekle",
|
||||
"track_will_play_next": "{track} bir sonraki çalacak",
|
||||
"play_next": "Sonrakini oynat",
|
||||
"removed_track_from_queue": "{track} sıradan kaldırıldı",
|
||||
"remove_from_queue": "Sıradan kaldır",
|
||||
"removed_track_from_queue": "{track} kuyruktan kaldırıldı",
|
||||
"remove_from_queue": "Kuyruktan kaldır",
|
||||
"remove_from_favorites": "Favorilerden kaldır",
|
||||
"save_as_favorite": "Favori olarak kaydet",
|
||||
"add_to_playlist": "Oynatma listesine ekle",
|
||||
@ -88,7 +88,7 @@
|
||||
"add_to_blacklist": "Kara listeye ekle",
|
||||
"remove_from_blacklist": "Kara listeden kaldır",
|
||||
"share": "Paylaş",
|
||||
"mini_player": "Mini Oynatıcı",
|
||||
"mini_player": "Mini oynatıcı",
|
||||
"slide_to_seek": "İleri veya geri arama yapmak için kaydırın",
|
||||
"shuffle_playlist": "Oynatma listesini karıştır",
|
||||
"unshuffle_playlist": "Oynatma listesinin karışıklığını kaldır",
|
||||
@ -98,27 +98,27 @@
|
||||
"resume_playback": "Oynatmayı sürdür",
|
||||
"loop_track": "Döngü parçası",
|
||||
"repeat_playlist": "Oynatma listesini tekrarla",
|
||||
"queue": "Sıra",
|
||||
"alternative_track_sources": "Alternatif yol kaynakları",
|
||||
"queue": "Kuyruk",
|
||||
"alternative_track_sources": "Alternatif parça kaynakları",
|
||||
"download_track": "Parçayı indir",
|
||||
"tracks_in_queue": "{tracks} parça sırada",
|
||||
"tracks_in_queue": "{tracks} parça kuyrukta",
|
||||
"clear_all": "Tümünü temizle",
|
||||
"show_hide_ui_on_hover": "Fareyle üzerine gelindiğinde kullanıcı arayüzünü göster/gizle",
|
||||
"always_on_top": "Her zaman üstte",
|
||||
"exit_mini_player": "Mini oynatıcıdan çık",
|
||||
"download_location": "İndirme konumu",
|
||||
"account": "Hesap",
|
||||
"login_with_spotify": "Spotify hesabınızla giriş yapın",
|
||||
"login_with_spotify": "Spotify hesabı ile giriş yap",
|
||||
"connect_with_spotify": "Spotify ile bağlan",
|
||||
"logout": "Çıkış Yap",
|
||||
"logout_of_this_account": "Bu hesaptan çıkış yap",
|
||||
"language_region": "Dil ve Bölge",
|
||||
"language": "Dil",
|
||||
"system_default": "Sistem Varsayılanı",
|
||||
"market_place_region": "Pazaryeri Bölgesi",
|
||||
"recommendation_country": "Tavsiye Edilen Ülke",
|
||||
"logout": "Çıkış yap",
|
||||
"logout_of_this_account": "Hesaptan çıkış yap",
|
||||
"language_region": "Dil ve bölge",
|
||||
"language": "Tercih edilen dil",
|
||||
"system_default": "Sistem varsayılanı",
|
||||
"market_place_region": "Tercih edilen bölge",
|
||||
"recommendation_country": "Tavsiye edilen ülke",
|
||||
"appearance": "Görünüm",
|
||||
"layout_mode": "Düzen Modu",
|
||||
"layout_mode": "Düzen modu",
|
||||
"override_layout_settings": "Duyarlı düzen modu ayarlarını geçersiz kıl",
|
||||
"adaptive": "Uyarlanabilir",
|
||||
"compact": "Sıkıştırılmış",
|
||||
@ -127,35 +127,35 @@
|
||||
"dark": "Koyu",
|
||||
"light": "Açık",
|
||||
"system": "Sistem",
|
||||
"accent_color": "Vurgu Rengi",
|
||||
"accent_color": "Vurgu rengi",
|
||||
"sync_album_color": "Albüm rengini senkronize et",
|
||||
"sync_album_color_description": "Vurgu rengi olarak albüm resminin baskın rengini kullanır",
|
||||
"playback": "Oynatma",
|
||||
"audio_quality": "Ses Kalitesi",
|
||||
"audio_quality": "Ses kalitesi",
|
||||
"high": "Yüksek",
|
||||
"low": "Düşük",
|
||||
"pre_download_play": "Ön yükleme ve oynatma",
|
||||
"pre_download_play_description": "Ses akışı yerine baytları indirin ve oynatın (Daha yüksek bant genişliğine sahip kullanıcılar için önerilir)",
|
||||
"skip_non_music": "Müzik olmayan bölümleri atla (SponsorBlock)",
|
||||
"pre_download_play": "Önceden indir ve oynat",
|
||||
"pre_download_play_description": "Ses akışı yerine baytları indir ve oynat (Daha yüksek bant genişliğine sahip kullanıcılar için önerilir)",
|
||||
"skip_non_music": "Müzik olmayan bölümleri atlat (SponsorBlock)",
|
||||
"blacklist_description": "Kara listeye alınan parçalar ve sanatçılar",
|
||||
"wait_for_download_to_finish": "Lütfen mevcut indirme işleminin tamamlanmasını bekleyin",
|
||||
"desktop": "Masaüstü",
|
||||
"close_behavior": "Kapatma Davranışı",
|
||||
"close_behavior": "Kapatma davranışı",
|
||||
"close": "Kapat",
|
||||
"minimize_to_tray": "Tepsiye küçült",
|
||||
"show_tray_icon": "Sistem tepsisi simgesini göster",
|
||||
"about": "Hakkında",
|
||||
"u_love_spotube": "Spotube'u sevdiğinizi biliyoruz",
|
||||
"check_for_updates": "Güncellemeleri kontrol et",
|
||||
"about_spotube": "Spotube Hakkında",
|
||||
"about_spotube": "Spotube hakkında",
|
||||
"blacklist": "Kara liste",
|
||||
"please_sponsor": "Sponsor Ol/Bağış Yap",
|
||||
"spotube_description": "Spotube, hafif, platformlar arası, herkes için ücretsiz bir spotify istemcisidir",
|
||||
"spotube_description": "Spotube, hafif, platformlar arası uyumlu ve herkes için ücretsiz bir Spotify istemcisidir.",
|
||||
"version": "Sürüm",
|
||||
"build_number": "Derleme Numarası",
|
||||
"founder": "Kurucu",
|
||||
"build_number": "Derleme numarası",
|
||||
"founder": "Geliştirici",
|
||||
"repository": "Depo",
|
||||
"bug_issues": "Hata+Sorunlar",
|
||||
"bug_issues": "Hata + Sorunlar",
|
||||
"made_with": "❤️ ile Bangladeş'te yapıldı",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
@ -163,31 +163,31 @@
|
||||
"add_spotify_credentials": "Başlamak için spotify kimlik bilgilerinizi ekleyin",
|
||||
"credentials_will_not_be_shared_disclaimer": "Endişelenmeyin, kimlik bilgilerinizden hiçbiri toplanmayacak veya kimseyle paylaşılmayacak",
|
||||
"know_how_to_login": "Bunu nasıl yapacağınızı bilmiyor musunuz?",
|
||||
"follow_step_by_step_guide": "Adım Adım kılavuzu takip edin",
|
||||
"spotify_cookie": "Spotify {name} Çerezi",
|
||||
"cookie_name_cookie": "{name} Çerezi",
|
||||
"follow_step_by_step_guide": "Adım adım kılavuzu takip edin",
|
||||
"spotify_cookie": "Spotify {name} çerezi",
|
||||
"cookie_name_cookie": "{name} çerezi",
|
||||
"fill_in_all_fields": "Lütfen tüm alanları doldurun",
|
||||
"submit": "Gönder",
|
||||
"submit": "Başvur",
|
||||
"exit": "Çık",
|
||||
"previous": "Önceki",
|
||||
"next": "Sonraki",
|
||||
"done": "Bitti",
|
||||
"step_1": "1. Adım",
|
||||
"first_go_to": "İlk olarak şuraya gidin:",
|
||||
"login_if_not_logged_in": "ve oturum açmadıysanız Oturum Açın/Kaydolun",
|
||||
"login_if_not_logged_in": "ve oturum açmadıysanız Oturum açın/Kaydolun",
|
||||
"step_2": "2. Adım",
|
||||
"step_2_steps": "1. Giriş yaptıktan sonra, Tarayıcı geliştirme araçlarını açmak için F12 veya Fareye Sağ Tıklayın > İncele'ye basın.\n2. Ardından \"Uygulama\" Sekmesine (Chrome, Edge, Brave vb.) veya \"Depolama\" Sekmesine (Firefox, Palemoon vb.) gidin.\n3. \"Çerezler\" bölümüne ve ardından \"https://accounts.spotify.com\" alt bölümüne gidin",
|
||||
"step_2_steps": "1. Oturum açtıktan sonra, tarayıcı geliştirme araçlarını açmak için F12'ye veya fareye sağ tıklayın > İncele'ye basın.\n2. Daha sonra \"Uygulama\" sekmesine (Chrome, Edge, Brave vb..) veya \"Depolama\" sekmesine (Firefox, Palemoon vb..) gidin\n3. \"Çerezler\" bölümüne, ardından \"https://accounts.spotify.com\" alt bölümüne gidin",
|
||||
"step_3": "3. Adım",
|
||||
"step_3_steps": "\"sp_dc\" Çerezinin değerini kopyalayın",
|
||||
"success_emoji": "Başarılı🥳",
|
||||
"success_message": "Artık Spotify hesabınızla başarıyla giriş yaptınız. Aferin, dostum!",
|
||||
"success_message": "Artık Spotify hesabınızla başarıyla giriş yaptınız. Tebrik ederim!",
|
||||
"step_4": "4. Adım",
|
||||
"step_4_steps": "Kopyalanan \"sp_dc\" değerini yapıştırın",
|
||||
"something_went_wrong": "Bir hata oluştu",
|
||||
"piped_instance": "Piped Sunucu Örneği",
|
||||
"piped_instance": "Piped sunucu örneği",
|
||||
"piped_description": "Parça eşleştirme için kullanılacak Piped sunucu örneği",
|
||||
"piped_warning": "Bazıları iyi çalışmayabilir. Yani riski size ait olmak üzere kullanın",
|
||||
"generate_playlist": "Oynatma Listesi Oluştur",
|
||||
"generate_playlist": "Oynatma listesi oluştur",
|
||||
"track_exists": "{track} parçası zaten var",
|
||||
"replace_downloaded_tracks": "İndirilen tüm parçaları değiştir",
|
||||
"skip_download_tracks": "İndirilen tüm parçaları indirmeyi atla",
|
||||
@ -195,8 +195,8 @@
|
||||
"replace": "Değiştir",
|
||||
"skip": "Atla",
|
||||
"select_up_to_count_type": "En fazla {count} {type} seçin",
|
||||
"select_genres": "Türleri Seç",
|
||||
"add_genres": "Tür Ekle",
|
||||
"select_genres": "Türleri seç",
|
||||
"add_genres": "Tür ekle",
|
||||
"country": "Ülke",
|
||||
"number_of_tracks_generate": "Oluşturulacak parça sayısı",
|
||||
"acousticness": "Akustiklik",
|
||||
@ -212,7 +212,7 @@
|
||||
"duration": "Süre (sn)",
|
||||
"tempo": "Tempo (BPM)",
|
||||
"mode": "Mod",
|
||||
"time_signature": "Zaman İmzası",
|
||||
"time_signature": "Zaman imzası",
|
||||
"short": "Kısa",
|
||||
"medium": "Orta",
|
||||
"long": "Uzun",
|
||||
@ -220,29 +220,29 @@
|
||||
"max": "Maks",
|
||||
"target": "Hedef",
|
||||
"moderate": "Orta",
|
||||
"deselect_all": "Tüm Seçimleri Kaldır",
|
||||
"select_all": "Tümünü Seç",
|
||||
"deselect_all": "Tüm seçimleri kaldır",
|
||||
"select_all": "Tümünü seç",
|
||||
"are_you_sure": "Emin misiniz?",
|
||||
"generating_playlist": "Özel oynatma listeniz oluşturuluyor...",
|
||||
"selected_count_tracks": "{count} parça seçildi",
|
||||
"download_warning": "Tüm Parçaları toplu olarak indirirseniz, açıkça Müzik korsanlığı yapıyor ve Müziğin yaratıcı toplumuna zarar veriyorsunuz demektir. Umarım bunun farkındasınızdır. Her zaman, Sanatçının sıkı çalışmasına saygı duymaya ve desteklemeye çalışın",
|
||||
"download_ip_ban_warning": "Bu arada, IP'niz normalden daha fazla indirme isteği nedeniyle YouTube'da engellenebilir. IP engelleme, YouTube'u (oturum açmış olsanız bile) o IP cihazından en az 2 -3 ay kullanamayacağınız anlamına gelir. Ve eğer böyle bir şey olursa Spotube'un hiçbir sorumluluğu yok",
|
||||
"download_warning": "Tüm şarkıları toplu olarak indiriyorsanız, açıkça müzik korsanlığı yapıyorsunuz ve müzik dünyasının yaratıcı topluluğuna zarar veriyorsunuz demektir. Umuyorum bunun farkındasınızdır. Her zaman, sanatçıların emeğine saygı göstermeyi ve desteklemeyi deneyin.",
|
||||
"download_ip_ban_warning": "Ayrıca, normalden fazla indirme istekleri nedeniyle YouTube'da IP'niz engellenebilir. IP engeli, en az 2-3 ay boyunca YouTube'u (hatta oturum açmış olsanız bile) o IP cihazından kullanamayacağınız anlamına gelir. Ve eğer böyle bir durum yaşanırsa, Spotube bundan hiçbir sorumluluk kabul etmez.",
|
||||
"by_clicking_accept_terms": "\"Kabul et\" e tıklayarak aşağıdaki şartları kabul etmiş olursunuz:",
|
||||
"download_agreement_1": "Müzik korsanlığı yaptığımı biliyorum. Ben kötüyüm",
|
||||
"download_agreement_1": "Müzik korsanlığı yaptığımı biliyorum. Ben fakir biriyim.",
|
||||
"download_agreement_2": "Sanatçıyı elimden geldiğince destekleyeceğim ve bunu sadece sanatını satın alacak param olmadığı için yapıyorum",
|
||||
"download_agreement_3": "IP adresimin YouTube'da engellenebileceğinin tamamen farkındayım ve mevcut işlemimden kaynaklanan herhangi bir kazadan Spotube'u veya sahiplerini/katkıda bulunanlarını sorumlu tutmuyorum",
|
||||
"download_agreement_3": "YouTube'da IP'min engellenebileceğinin tamamen farkındayım ve mevcut eylemlerimden kaynaklanan herhangi bir kaza için Spotube'u veya sahiplerini/katkıda bulunanları sorumlu tutmuyorum.",
|
||||
"decline": "Reddet",
|
||||
"accept": "Kabul et",
|
||||
"details": "Detaylar",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Kanal",
|
||||
"likes": "Beğeniler",
|
||||
"likes": "Beğenenler",
|
||||
"dislikes": "Beğenmeyenler",
|
||||
"views": "İzlenmeler",
|
||||
"streamUrl": "Akış bağlantısı",
|
||||
"stop": "Durdur",
|
||||
"sort_newest": "En yeniye göre sırala",
|
||||
"sort_oldest": "Eklenen en eskiye göre sırala",
|
||||
"sort_newest": "En yeni eklenene göre sırala.",
|
||||
"sort_oldest": "En eski eklenene göre sırala",
|
||||
"sleep_timer": "Uyku Zamanlayıcısı",
|
||||
"mins": "{minutes} Dakika",
|
||||
"hours": "{hours} Saatler",
|
||||
@ -251,11 +251,11 @@
|
||||
"logs": "Günlükler",
|
||||
"developers": "Geliştiriciler",
|
||||
"not_logged_in": "Giriş yapmadınız",
|
||||
"search_mode": "Arama Modu",
|
||||
"audio_source": "Ses Kaynağı",
|
||||
"search_mode": "Arama modu",
|
||||
"audio_source": "Ses kaynağı",
|
||||
"ok": "Tamam",
|
||||
"failed_to_encrypt": "Şifreleme başarısız oldu",
|
||||
"encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde saklamak için şifreleme kullanır. Ama başaramadı. Bu yüzden güvensiz depolamaya geri dönecek\nLinux kullanıyorsanız, lütfen herhangi bir gizli servisin (gnome - anahtarlık, kde - cüzdan, keepassxc vb.) yüklü olduğundan emin olun",
|
||||
"encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde depolamak için şifreleme kullanır. Ancak bunu başaramadı. Bu nedenle, güvensiz depolamaya geri dönecektir\nLinux kullanıyorsanız, lütfen gnome-keyring, kde-wallet, keepassxc vb. herhangi bir gizli servisin yüklü olduğundan emin olun.",
|
||||
"querying_info": "Bilgi sorgulanıyor...",
|
||||
"piped_api_down": "Piped API kapalı",
|
||||
"piped_down_error_instructions": "Piped örneği {pipedInstance} şu anda kapalı\n\nÖrneği değiştirin veya 'API türünü' resmi YouTube API'si olarak değiştirin\n\nDeğişiklikten sonra uygulamayı yeniden başlattığınızdan emin olun",
|
||||
@ -263,8 +263,8 @@
|
||||
"connection_restored": "İnternet bağlantınız geri yüklendi",
|
||||
"use_system_title_bar": "Sistem başlık çubuğunu kullan",
|
||||
"crunching_results": "Sonuçlar...",
|
||||
"search_to_get_results": "Sonuç almak için ara",
|
||||
"use_amoled_mode": "AMOLED Modunu Kullan",
|
||||
"search_to_get_results": "Sonuç almak için arayın",
|
||||
"use_amoled_mode": "AMOLED modu kullan",
|
||||
"pitch_dark_theme": "Zifiri karanlık koyu tema",
|
||||
"normalize_audio": "Sesi normalleştir",
|
||||
"change_cover": "Kapağı değiştir",
|
||||
@ -277,48 +277,48 @@
|
||||
"disconnect_lastfm": "Last.fm bağlantısını kes",
|
||||
"disconnect": "Bağlantıyı kes",
|
||||
"username": "Kullanıcı adı",
|
||||
"password": "Parola",
|
||||
"login": "Giriş",
|
||||
"password": "Şifre",
|
||||
"login": "Giriş yap",
|
||||
"login_with_your_lastfm": "Last.fm hesabınızla giriş yapın",
|
||||
"scrobble_to_lastfm": "Last.fm için Scrobble",
|
||||
"go_to_album": "Albüme Git",
|
||||
"discord_rich_presence": "Discord Zengin Varlığı",
|
||||
"browse_all": "Tümüne Göz At",
|
||||
"genres": "Müzik Türleri",
|
||||
"explore_genres": "Türleri Keşfet",
|
||||
"go_to_album": "Albüme git",
|
||||
"discord_rich_presence": "Discord zengin varlığı",
|
||||
"browse_all": "Tümüne göz at",
|
||||
"genres": "Müzik türleri",
|
||||
"explore_genres": "Türleri keşfet",
|
||||
"friends": "Arkadaşlar",
|
||||
"no_lyrics_available": "Üzgünüz, bu parçanın sözleri bulunamıyor",
|
||||
"start_a_radio": "Radyo Başlat",
|
||||
"start_a_radio": "Radyo başlat",
|
||||
"how_to_start_radio": "Radyoyu nasıl başlatmak istersiniz?",
|
||||
"replace_queue_question": "Mevcut kuyruğu değiştirmek mi yoksa eklemek mi istersiniz?",
|
||||
"endless_playback": "Sonsuz Olarak Oynat",
|
||||
"delete_playlist": "Oynatma Listesini Sil",
|
||||
"endless_playback": "Sonsuz olarak oynat",
|
||||
"delete_playlist": "Oynatma listesini sil",
|
||||
"delete_playlist_confirmation": "Bu oynatma listesini silmek istediğinizden emin misiniz?",
|
||||
"local_tracks": "Yerel Parçalar",
|
||||
"song_link": "Şarkı Bağlantısı",
|
||||
"local_tracks": "Yerel parçalar",
|
||||
"song_link": "Şarkı bağlantısı",
|
||||
"skip_this_nonsense": "Bu saçmalığı atla",
|
||||
"freedom_of_music": "“Müzik Özgürlüğü”",
|
||||
"freedom_of_music_palm": "“Müzik Özgürlüğü avucunuzun içinde”",
|
||||
"freedom_of_music": "“Müzik özgürlüğü”",
|
||||
"freedom_of_music_palm": "“Müzik özgürlüğü avucunuzun içinde”",
|
||||
"get_started": "Haydi başlayalım",
|
||||
"youtube_source_description": "Tavsiye edilir ve en iyi şekilde çalışır.",
|
||||
"piped_source_description": "Özgür hissediyor musunuz? YouTube ile aynı ama çok daha fazla ücretsiz.",
|
||||
"piped_source_description": "Özgür hissediyor musunuz? YouTube ile aynı, ama çok daha özgür.",
|
||||
"jiosaavn_source_description": "Güney Asya bölgesi için en iyisi.",
|
||||
"highest_quality": "En Yüksek Kalite: {quality}",
|
||||
"select_audio_source": "Ses Kaynağını Seç",
|
||||
"endless_playback_description": "Yeni şarkıları otomatik olarak \nkuyruğun sonuna ekle",
|
||||
"highest_quality": "En yüksek kalite: {quality}",
|
||||
"select_audio_source": "Ses kaynağını seçin",
|
||||
"endless_playback_description": "Yeni şarkıları otomatik olarak\nkuyruğun sonuna ekle",
|
||||
"choose_your_region": "Bölgenizi seçin",
|
||||
"choose_your_region_description": "Bu, Spotube'un size doğru içeriği göstermesine yardımcı olacaktır\nkonumunuz için.",
|
||||
"choose_your_region_description": "Bu, Spotube'un konumunuza uygun içerikleri göstermesine yardımcı olacaktır.",
|
||||
"choose_your_language": "Dilinizi seçin",
|
||||
"help_project_grow": "Bu projenin büyümesine yardımcı ol",
|
||||
"help_project_grow": "Bu projenin büyümesine yardımcı olun",
|
||||
"help_project_grow_description": "Spotube açık kaynaklı bir projedir. Projeye katkıda bulunarak, hataları bildirerek veya yeni özellikler önererek bu projenin büyümesine yardımcı olabilirsiniz.",
|
||||
"contribute_on_github": "GitHub'a katkıda bulunun",
|
||||
"donate_on_open_collective": "Open Collective'e bağış yap",
|
||||
"browse_anonymously": "Anonim Olarak Göz at",
|
||||
"enable_connect": "Bağlantıyı Etkinleştir",
|
||||
"contribute_on_github": "GitHub'da katkıda bulun",
|
||||
"donate_on_open_collective": "Open Collective'de bağış yap",
|
||||
"browse_anonymously": "Anonim olarak giriş yap",
|
||||
"enable_connect": "Bağlanmayı etkinleştir",
|
||||
"enable_connect_description": "Spotube'u diğer cihazlardan kontrol edin",
|
||||
"devices": "Cihazlar",
|
||||
"select": "Seç",
|
||||
"connect_client_alert": "{client} tarafından kontrol ediliyorsun.",
|
||||
"this_device": "Bu Cihaz",
|
||||
"this_device": "Bu cihaz",
|
||||
"remote": "Yönet"
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
/// TexturedPolak@github => Polish
|
||||
/// yuri-val@github => Ukrainian
|
||||
/// energywave@github, ncvescera@github, OpenCode@github => Italian
|
||||
/// mdksec@github, mikropsoft@github => Turkish
|
||||
/// mikropsoft@github => Turkish
|
||||
/// Stephan-P@github, SecularSteve@github => Dutch
|
||||
/// doannc2212@github => Vietnamese
|
||||
/// sappho192@github => Korean
|
||||
@ -28,11 +28,14 @@ class L10n {
|
||||
const Locale('de', 'GE'),
|
||||
const Locale('es', 'ES'),
|
||||
const Locale('fa', 'IR'),
|
||||
const Locale('fi', 'FI'),
|
||||
const Locale('fr', 'FR'),
|
||||
const Locale('ne', 'NP'),
|
||||
const Locale('hi', 'IN'),
|
||||
const Locale('id', 'ID'),
|
||||
const Locale('it', 'IT'),
|
||||
const Locale('ja', 'JP'),
|
||||
const Locale('ka', 'GE'),
|
||||
const Locale('ko', 'KR'),
|
||||
const Locale('nl', 'NL'),
|
||||
const Locale('pl', 'PL'),
|
||||
@ -43,5 +46,6 @@ class L10n {
|
||||
const Locale('tr', 'TR'),
|
||||
const Locale('zh', 'CN'),
|
||||
const Locale('vi', 'VN'),
|
||||
const Locale('eu', 'ES'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@ import 'package:device_preview/device_preview.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -19,6 +19,7 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart';
|
||||
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
||||
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
||||
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
||||
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
||||
import 'package:spotube/l10n/l10n.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/models/skip_segment.dart';
|
||||
@ -31,15 +32,17 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/cli/cli.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||
import 'package:spotube/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/hooks/configurators/use_init_sys_tray.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
Future<void> main(List<String> rawArgs) async {
|
||||
final arguments = await startCLI(rawArgs);
|
||||
@ -55,12 +58,12 @@ Future<void> main(List<String> rawArgs) async {
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// force High Refresh Rate on some Android devices (like One Plus)
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
if (kIsAndroid) {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
}
|
||||
|
||||
if (DesktopTools.platform.isDesktop) {
|
||||
await DesktopTools.window.setPreventClose(true);
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setPreventClose(true);
|
||||
}
|
||||
|
||||
await SystemTheme.accentColor.load();
|
||||
@ -69,7 +72,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
MetadataGod.initialize();
|
||||
}
|
||||
|
||||
if (DesktopTools.platform.isWindows || DesktopTools.platform.isLinux) {
|
||||
if (kIsWindows || kIsLinux) {
|
||||
DiscordRPC.initialize();
|
||||
}
|
||||
|
||||
@ -101,14 +104,10 @@ Future<void> main(List<String> rawArgs) async {
|
||||
path: hiveCacheDir,
|
||||
);
|
||||
|
||||
await DesktopTools.ensureInitialized(
|
||||
DesktopWindowOptions(
|
||||
hideTitleBar: true,
|
||||
title: "Spotube",
|
||||
backgroundColor: Colors.transparent,
|
||||
minimumSize: const Size(300, 700),
|
||||
),
|
||||
);
|
||||
if (kIsDesktop) {
|
||||
await localNotifier.setup(appName: "Spotube");
|
||||
await WindowManagerTools.initialize();
|
||||
}
|
||||
|
||||
Catcher2(
|
||||
enableLogger: arguments["verbose"],
|
||||
@ -189,9 +188,9 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
ref.listen(playbackServerProvider, (_, __) {});
|
||||
ref.listen(connectServerProvider, (_, __) {});
|
||||
ref.listen(connectClientsProvider, (_, __) {});
|
||||
ref.listen(trayManagerProvider, (_, __) {});
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useInitSysTray(ref);
|
||||
useDeepLinking(ref);
|
||||
useCloseBehavior(ref);
|
||||
useGetStoragePermissions(ref);
|
||||
@ -233,9 +232,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
builder: (context, child) {
|
||||
return DevicePreview.appBuilder(
|
||||
context,
|
||||
DesktopTools.platform.isDesktop && !DesktopTools.platform.isMacOS
|
||||
? DragToResizeArea(child: child!)
|
||||
: child,
|
||||
kIsDesktop && !kIsMacOS ? DragToResizeArea(child: child!) : child,
|
||||
);
|
||||
},
|
||||
themeMode: themeMode,
|
||||
|
||||
@ -12,7 +12,7 @@ part of 'connect.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
|
||||
@ -27,7 +27,7 @@ Future<File> getLogsPath() async {
|
||||
}
|
||||
final file = File(path.join(dir, ".spotube_logs"));
|
||||
if (!await file.exists()) {
|
||||
await file.create();
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ part of 'home_feed.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SpotifySectionPlaylist _$SpotifySectionPlaylistFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
|
||||
@ -12,7 +12,7 @@ part of 'recommendation_seeds.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$GeneratePlaylistProviderInput {
|
||||
|
||||
@ -12,7 +12,7 @@ import 'package:spotube/components/shared/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
final Category category;
|
||||
@ -27,7 +27,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
final scrollController = useScrollController();
|
||||
|
||||
return Scaffold(
|
||||
appBar: DesktopTools.platform.isDesktop
|
||||
appBar: kIsDesktop
|
||||
? const PageWindowTitleBar(
|
||||
leading: BackButton(color: Colors.white),
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -53,12 +53,12 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: DesktopTools.platform.isMobile,
|
||||
automaticallyImplyLeading: kIsMobile,
|
||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||
title: const Text(""),
|
||||
backgroundColor: Colors.transparent,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: DesktopTools.platform.isDesktop,
|
||||
centerTitle: kIsDesktop,
|
||||
title: Text(
|
||||
category.name!,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
@ -41,9 +42,14 @@ class HomePage extends HookConsumerWidget {
|
||||
const ConnectDeviceButton(),
|
||||
const Gap(10),
|
||||
Consumer(builder: (context, ref, _) {
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
final me = ref.watch(meProvider);
|
||||
final meData = me.asData?.value;
|
||||
|
||||
if (auth == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: CircleAvatar(
|
||||
backgroundImage: UniversalImage.imageProvider(
|
||||
|
||||
@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [
|
||||
Tab(text: " ${context.l10n.playlists} "),
|
||||
Tab(text: " ${context.l10n.local_tracks} "),
|
||||
Tab(text: " ${context.l10n.local_tab} "),
|
||||
Tab(
|
||||
child: Badge(
|
||||
isLabelVisible: downloadingCount > 0,
|
||||
|
||||
238
lib/pages/library/local_folder.dart
Normal file
238
lib/pages/library/local_folder.dart
Normal file
@ -0,0 +1,238 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.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/models/local_track.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class LocalLibraryPage extends HookConsumerWidget {
|
||||
final String location;
|
||||
final bool isDownloads;
|
||||
const LocalLibraryPage(this.location, {super.key, this.isDownloads = false});
|
||||
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
final sortBy = useState<SortBy>(SortBy.none);
|
||||
final playlist = ref.watch(proxyPlaylistProvider);
|
||||
final trackSnapshot = ref.watch(localTracksProvider);
|
||||
final isPlaylistPlaying = playlist.containsTracks(
|
||||
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
||||
|
||||
final searchController = useTextEditingController();
|
||||
useValueListenable(searchController);
|
||||
final searchFocus = useFocusNode();
|
||||
final isFiltering = useState(false);
|
||||
|
||||
final controller = useScrollController();
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
leading: const BackButton(),
|
||||
centerTitle: true,
|
||||
title: Text(isDownloads ? context.l10n.downloads : location),
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
onPressed: trackSnapshot.asData?.value != null
|
||||
? () async {
|
||||
if (trackSnapshot.asData?.value.isNotEmpty ==
|
||||
true) {
|
||||
if (!isPlaylistPlaying) {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
trackSnapshot.asData!.value[location] ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(context.l10n.play),
|
||||
Icon(
|
||||
isPlaylistPlaying
|
||||
? SpotubeIcons.stop
|
||||
: SpotubeIcons.play,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ExpandableSearchButton(
|
||||
isFiltering: isFiltering.value,
|
||||
onPressed: (value) => isFiltering.value = value,
|
||||
searchFocus: searchFocus,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SortTracksDropdown(
|
||||
value: sortBy.value,
|
||||
onChanged: (value) {
|
||||
sortBy.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
child: const Icon(SpotubeIcons.refresh),
|
||||
onPressed: () {
|
||||
ref.invalidate(localTracksProvider);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
ExpandableSearchField(
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
isFiltering: isFiltering.value,
|
||||
onChangeFiltering: (value) => isFiltering.value = value,
|
||||
),
|
||||
trackSnapshot.when(
|
||||
data: (tracks) {
|
||||
final sortedTracks = useMemoized(() {
|
||||
return ServiceUtils.sortTracks(
|
||||
tracks[location] ?? <LocalTrack>[], 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()),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -18,6 +17,7 @@ import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class MiniLyricsPage extends HookConsumerWidget {
|
||||
final Size prevSize;
|
||||
@ -36,9 +36,11 @@ class MiniLyricsPage extends HookConsumerWidget {
|
||||
final showLyrics = useState(true);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
wasMaximized.value = await DesktopTools.window.isMaximized();
|
||||
});
|
||||
if (kIsDesktop) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
wasMaximized.value = await windowManager.isMaximized();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
@ -112,11 +114,13 @@ class MiniLyricsPage extends HookConsumerWidget {
|
||||
areaActive.value = true;
|
||||
hoverMode.value = false;
|
||||
|
||||
await DesktopTools.window.setSize(
|
||||
showLyrics.value
|
||||
? const Size(400, 500)
|
||||
: const Size(400, 150),
|
||||
);
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setSize(
|
||||
showLyrics.value
|
||||
? const Size(400, 500)
|
||||
: const Size(400, 150),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@ -135,33 +139,34 @@ class MiniLyricsPage extends HookConsumerWidget {
|
||||
hoverMode.value = !hoverMode.value;
|
||||
},
|
||||
),
|
||||
FutureBuilder(
|
||||
future: DesktopTools.window.isAlwaysOnTop(),
|
||||
builder: (context, snapshot) {
|
||||
return IconButton(
|
||||
tooltip: context.l10n.always_on_top,
|
||||
icon: Icon(
|
||||
snapshot.data == true
|
||||
? SpotubeIcons.pinOn
|
||||
: SpotubeIcons.pinOff,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: snapshot.data == true
|
||||
? MaterialStateProperty.all(
|
||||
theme.colorScheme.primary)
|
||||
: null,
|
||||
),
|
||||
onPressed: snapshot.data == null
|
||||
? null
|
||||
: () async {
|
||||
await DesktopTools.window.setAlwaysOnTop(
|
||||
snapshot.data == true ? false : true,
|
||||
);
|
||||
update();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (kIsDesktop)
|
||||
FutureBuilder(
|
||||
future: windowManager.isAlwaysOnTop(),
|
||||
builder: (context, snapshot) {
|
||||
return IconButton(
|
||||
tooltip: context.l10n.always_on_top,
|
||||
icon: Icon(
|
||||
snapshot.data == true
|
||||
? SpotubeIcons.pinOn
|
||||
: SpotubeIcons.pinOff,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
foregroundColor: snapshot.data == true
|
||||
? MaterialStateProperty.all(
|
||||
theme.colorScheme.primary)
|
||||
: null,
|
||||
),
|
||||
onPressed: snapshot.data == null
|
||||
? null
|
||||
: () async {
|
||||
await windowManager.setAlwaysOnTop(
|
||||
snapshot.data == true ? false : true,
|
||||
);
|
||||
update();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -243,19 +248,20 @@ class MiniLyricsPage extends HookConsumerWidget {
|
||||
tooltip: context.l10n.exit_mini_player,
|
||||
icon: const Icon(SpotubeIcons.maximize),
|
||||
onPressed: () async {
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
try {
|
||||
await DesktopTools.window
|
||||
await windowManager
|
||||
.setMinimumSize(const Size(300, 700));
|
||||
await DesktopTools.window.setAlwaysOnTop(false);
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
if (wasMaximized.value) {
|
||||
await DesktopTools.window.maximize();
|
||||
await windowManager.maximize();
|
||||
} else {
|
||||
await DesktopTools.window.setSize(prevSize);
|
||||
await windowManager.setSize(prevSize);
|
||||
}
|
||||
await DesktopTools.window
|
||||
.setAlignment(Alignment.center);
|
||||
await windowManager.setAlignment(Alignment.center);
|
||||
if (!kIsLinux) {
|
||||
await DesktopTools.window.setHasShadow(true);
|
||||
await windowManager.setHasShadow(true);
|
||||
}
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 200));
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -15,12 +14,13 @@ import 'package:spotube/components/root/sidebar.dart';
|
||||
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
||||
import 'package:spotube/provider/connect/server.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
const rootPaths = {
|
||||
"/": 0,
|
||||
@ -38,7 +38,6 @@ class RootApp extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final isMounted = useIsMounted();
|
||||
final showingDialogCompleter = useRef(Completer()..complete());
|
||||
final downloader = ref.watch(downloadManagerProvider);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
@ -47,6 +46,8 @@ class RootApp extends HookConsumerWidget {
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ServiceUtils.checkForUpdates(context, ref);
|
||||
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
||||
@ -129,7 +130,7 @@ class RootApp extends HookConsumerWidget {
|
||||
|
||||
useEffect(() {
|
||||
downloader.onFileExists = (track) async {
|
||||
if (!isMounted()) return false;
|
||||
if (!context.mounted) return false;
|
||||
|
||||
if (!showingDialogCompleter.value.isCompleted) {
|
||||
await showingDialogCompleter.value.future;
|
||||
@ -161,7 +162,6 @@ class RootApp extends HookConsumerWidget {
|
||||
}, [downloader]);
|
||||
|
||||
// checks for latest version of the application
|
||||
useUpdateChecker(ref);
|
||||
|
||||
useEndlessPlayback(ref);
|
||||
|
||||
@ -207,7 +207,7 @@ class RootApp extends HookConsumerWidget {
|
||||
),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: DesktopTools.platform.isDesktop
|
||||
endDrawer: kIsDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@ -113,7 +113,7 @@ class SearchTracksSection extends HookConsumerWidget {
|
||||
child: TextButton(
|
||||
onPressed: searchTrack.isLoadingNextPage
|
||||
? null
|
||||
: () => searchTrackNotifier.fetchMore,
|
||||
: searchTrackNotifier.fetchMore,
|
||||
child: searchTrack.isLoadingNextPage
|
||||
? const CircularProgressIndicator()
|
||||
: Text(context.l10n.load_more),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/links/hyper_link.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
@ -72,6 +73,13 @@ class AboutSpotube extends HookConsumerWidget {
|
||||
Text("v${packageInfo.version}")
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(context.l10n.channel),
|
||||
colon,
|
||||
Text(Env.releaseChannel.name)
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(context.l10n.build_number),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
@ -8,6 +7,7 @@ import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class SettingsDesktopSection extends HookConsumerWidget {
|
||||
const SettingsDesktopSection({super.key});
|
||||
@ -53,7 +53,7 @@ class SettingsDesktopSection extends HookConsumerWidget {
|
||||
value: preferences.systemTitleBar,
|
||||
onChanged: preferencesNotifier.setSystemTitleBar,
|
||||
),
|
||||
if (!DesktopTools.platform.isMacOS)
|
||||
if (!kIsMacOS)
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.discord),
|
||||
title: Text(context.l10n.discord_rich_presence),
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class SettingsDownloadsSection extends HookConsumerWidget {
|
||||
const SettingsDownloadsSection({super.key});
|
||||
@ -18,7 +19,7 @@ class SettingsDownloadsSection extends HookConsumerWidget {
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
final pickDownloadLocation = useCallback(() async {
|
||||
if (DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS) {
|
||||
if (kIsMobile || kIsMacOS) {
|
||||
final dirStr = await FilePicker.platform.getDirectoryPath(
|
||||
initialDirectory: preferences.downloadLocation,
|
||||
);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
@ -14,6 +13,7 @@ import 'package:spotube/pages/settings/sections/downloads.dart';
|
||||
import 'package:spotube/pages/settings/sections/language_region.dart';
|
||||
import 'package:spotube/pages/settings/sections/playback.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
@ -45,8 +45,7 @@ class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsAppearanceSection(),
|
||||
const SettingsPlaybackSection(),
|
||||
const SettingsDownloadsSection(),
|
||||
if (DesktopTools.platform.isDesktop)
|
||||
const SettingsDesktopSection(),
|
||||
if (kIsDesktop) const SettingsDesktopSection(),
|
||||
if (!kIsWeb) const SettingsDevelopersSection(),
|
||||
const SettingsAboutSection(),
|
||||
Center(
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart'
|
||||
hide X509Certificate;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -18,6 +20,18 @@ class AuthenticationCredentials {
|
||||
|
||||
bool get isExpired => DateTime.now().isAfter(expiration);
|
||||
|
||||
static final Dio dio = () {
|
||||
final dio = Dio();
|
||||
|
||||
(dio.httpClientAdapter as IOHttpClientAdapter)
|
||||
.createHttpClient = () => HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||
return host.endsWith("spotify.com") && port == 443;
|
||||
};
|
||||
|
||||
return dio;
|
||||
}();
|
||||
|
||||
AuthenticationCredentials({
|
||||
required this.cookie,
|
||||
required this.accessToken,
|
||||
@ -30,21 +44,23 @@ class AuthenticationCredentials {
|
||||
.split("; ")
|
||||
.firstWhereOrNull((c) => c.trim().startsWith("sp_dc="))
|
||||
?.trim();
|
||||
final res = await get(
|
||||
final res = await dio.getUri(
|
||||
Uri.parse(
|
||||
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player",
|
||||
),
|
||||
headers: {
|
||||
"Cookie": spDc ?? "",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
|
||||
},
|
||||
options: Options(
|
||||
headers: {
|
||||
"Cookie": spDc ?? "",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
|
||||
},
|
||||
),
|
||||
);
|
||||
final body = jsonDecode(res.body);
|
||||
final body = res.data;
|
||||
|
||||
if (res.statusCode >= 400) {
|
||||
if ((res.statusCode ?? 500) >= 400) {
|
||||
throw Exception(
|
||||
"Failed to get access token: ${body['error'] ?? res.reasonPhrase}",
|
||||
"Failed to get access token: ${body['error'] ?? res.statusMessage}",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
import 'package:dart_discord_rpc/dart_discord_rpc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class Discord extends ChangeNotifier {
|
||||
final DiscordRPC? discordRPC;
|
||||
final bool isEnabled;
|
||||
|
||||
Discord(this.isEnabled)
|
||||
: discordRPC = (DesktopTools.platform.isWindows ||
|
||||
DesktopTools.platform.isLinux) &&
|
||||
isEnabled
|
||||
: discordRPC = (kIsWindows || kIsLinux) && isEnabled
|
||||
? DiscordRPC(applicationId: Env.discordAppId)
|
||||
: null {
|
||||
discordRPC?.start(autoRegister: true);
|
||||
|
||||
125
lib/provider/local_tracks/local_tracks_provider.dart
Normal file
125
lib/provider/local_tracks/local_tracks_provider.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/foundation.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:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||
|
||||
const supportedAudioTypes = [
|
||||
"audio/webm",
|
||||
"audio/ogg",
|
||||
"audio/mpeg",
|
||||
"audio/mp4",
|
||||
"audio/opus",
|
||||
"audio/wav",
|
||||
"audio/aac",
|
||||
];
|
||||
|
||||
const imgMimeToExt = {
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/webp": ".webp",
|
||||
"image/gif": ".gif",
|
||||
};
|
||||
|
||||
final localTracksProvider =
|
||||
FutureProvider<Map<String, List<LocalTrack>>>((ref) async {
|
||||
try {
|
||||
if (kIsWeb) return {};
|
||||
final Map<String, List<LocalTrack>> tracks = {};
|
||||
|
||||
final downloadLocation = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.downloadLocation),
|
||||
);
|
||||
final downloadDir = Directory(downloadLocation);
|
||||
if (!await downloadDir.exists()) {
|
||||
await downloadDir.create(recursive: true);
|
||||
}
|
||||
final localLibraryLocations = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.localLibraryLocation),
|
||||
);
|
||||
|
||||
for (var location in [downloadLocation, ...localLibraryLocations]) {
|
||||
if (location.isEmpty) continue;
|
||||
final entities = <FileSystemEntity>[];
|
||||
if (await Directory(location).exists()) {
|
||||
try {
|
||||
entities.addAll(Directory(location).listSync(recursive: true));
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// ignore: no_leading_underscores_for_local_identifiers
|
||||
final _tracks = filesWithMetadata
|
||||
.map(
|
||||
(fileWithMetadata) => LocalTrack.fromTrack(
|
||||
track: Track().fromFile(
|
||||
fileWithMetadata["file"],
|
||||
metadata: fileWithMetadata["metadata"],
|
||||
art: fileWithMetadata["art"],
|
||||
),
|
||||
path: fileWithMetadata["file"].path,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
tracks[location] = _tracks;
|
||||
}
|
||||
return tracks;
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
@ -45,7 +45,14 @@ class ProxyPlaylist {
|
||||
}
|
||||
|
||||
bool containsTrack(TrackSimple track) {
|
||||
return tracks.firstWhereOrNull((element) => element.id == track.id) != null;
|
||||
return tracks.firstWhereOrNull((element) {
|
||||
if (element is LocalTrack && track is LocalTrack) {
|
||||
return element.path == track.path;
|
||||
}
|
||||
|
||||
return element.id == track.id;
|
||||
}) !=
|
||||
null;
|
||||
}
|
||||
|
||||
bool containsTracks(Iterable<TrackSimple> tracks) {
|
||||
@ -64,9 +71,11 @@ class ProxyPlaylist {
|
||||
/// To make sure proper instance method is used for JSON serialization
|
||||
/// Otherwise default super.toJson() is used
|
||||
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
|
||||
return switch (track.runtimeType) {
|
||||
LocalTrack() => track.toJson(),
|
||||
SourcedTrack() => track.toJson(),
|
||||
return switch (track) {
|
||||
// ignore: unnecessary_cast
|
||||
LocalTrack() => (track as LocalTrack).toJson(),
|
||||
// ignore: unnecessary_cast
|
||||
SourcedTrack() => (track as SourcedTrack).toJson(),
|
||||
_ => track.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
||||
final token = await spotify.getCredentials();
|
||||
SubtitleSimple lyrics = await getSpotifyLyrics(token.accessToken);
|
||||
|
||||
if (lyrics.lyrics.isEmpty) {
|
||||
if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) {
|
||||
lyrics = await getLRCLibLyrics();
|
||||
}
|
||||
|
||||
|
||||
79
lib/provider/tray_manager/tray_manager.dart
Normal file
79
lib/provider/tray_manager/tray_manager.dart
Normal file
@ -0,0 +1,79 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/tray_manager/tray_menu.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class SystemTrayManager with TrayListener {
|
||||
final Ref ref;
|
||||
final bool enabled;
|
||||
|
||||
SystemTrayManager(
|
||||
this.ref, {
|
||||
required this.enabled,
|
||||
}) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
if (enabled) {
|
||||
await trayManager.setIcon(
|
||||
kIsWindows
|
||||
? 'assets/spotube-logo.ico'
|
||||
: kIsFlatpak
|
||||
? 'com.github.KRTirtho.Spotube.png'
|
||||
: 'assets/spotube-logo.png',
|
||||
);
|
||||
trayManager.addListener(this);
|
||||
} else {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
trayManager.removeListener(this);
|
||||
}
|
||||
|
||||
@override
|
||||
onTrayIconMouseDown() {
|
||||
if (kIsWindows) {
|
||||
windowManager.show();
|
||||
} else {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
onTrayIconRightMouseDown() {
|
||||
if (!kIsWindows) {
|
||||
windowManager.show();
|
||||
} else {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final trayManagerProvider = Provider(
|
||||
(ref) {
|
||||
final enabled = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.showSystemTrayIcon),
|
||||
);
|
||||
|
||||
ref.listen(trayMenuProvider, (_, menu) {
|
||||
if (!enabled || !kIsDesktop) return;
|
||||
trayManager.setContextMenu(menu);
|
||||
});
|
||||
|
||||
final manager = SystemTrayManager(
|
||||
ref,
|
||||
enabled: enabled,
|
||||
);
|
||||
|
||||
ref.onDispose(manager.dispose);
|
||||
|
||||
return manager;
|
||||
},
|
||||
);
|
||||
108
lib/provider/tray_manager/tray_menu.dart
Normal file
108
lib/provider/tray_manager/tray_menu.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/loop_mode.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final audioPlayerLoopMode = StreamProvider<PlaybackLoopMode>((ref) {
|
||||
return audioPlayer.loopModeStream;
|
||||
});
|
||||
|
||||
final audioPlayerShuffleMode = StreamProvider<bool>((ref) {
|
||||
return audioPlayer.shuffledStream;
|
||||
});
|
||||
final audioPlayerPlaying = StreamProvider<bool>((ref) {
|
||||
return audioPlayer.playingStream;
|
||||
});
|
||||
|
||||
final trayMenuProvider = Provider((ref) {
|
||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||
final isPlaybackPlaying =
|
||||
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
|
||||
final isLoopOne =
|
||||
ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one;
|
||||
final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false;
|
||||
final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false;
|
||||
|
||||
return Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
label: "Show/Hide Window",
|
||||
onClick: (menuItem) async {
|
||||
if (await windowManager.isVisible()) {
|
||||
await windowManager.hide();
|
||||
} else {
|
||||
await windowManager.focus();
|
||||
await windowManager.show();
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: isPlaying ? "Pause" : "Play",
|
||||
disabled: !isPlaybackPlaying,
|
||||
onClick: (menuItem) async {
|
||||
if (audioPlayer.isPlaying) {
|
||||
await audioPlayer.pause();
|
||||
} else {
|
||||
await audioPlayer.resume();
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: "Next",
|
||||
disabled: !isPlaybackPlaying,
|
||||
onClick: (menuItem) {
|
||||
playlistNotifier.next();
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: "Previous",
|
||||
disabled: !isPlaybackPlaying,
|
||||
onClick: (menuItem) {
|
||||
playlistNotifier.previous();
|
||||
},
|
||||
),
|
||||
MenuItem.submenu(
|
||||
label: "Playback",
|
||||
submenu: Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
label: "Repeat",
|
||||
checked: isLoopOne,
|
||||
onClick: (menuItem) {
|
||||
audioPlayer.setLoopMode(
|
||||
isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one,
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: "Shuffle",
|
||||
checked: isShuffled,
|
||||
onClick: (menuItem) {
|
||||
audioPlayer.setShuffle(!isShuffled);
|
||||
},
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: "Stop",
|
||||
onClick: (menuItem) {
|
||||
playlistNotifier.stop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: "Quit",
|
||||
onClick: (menuItem) {
|
||||
exit(0);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -15,6 +14,7 @@ import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
final Ref ref;
|
||||
@ -69,6 +69,11 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
state = state.copyWith(downloadLocation: downloadDir);
|
||||
}
|
||||
|
||||
void setLocalLibraryLocation(List<String> localLibraryDirs) {
|
||||
//if (localLibraryDir.isEmpty) return;
|
||||
state = state.copyWith(localLibraryLocation: localLibraryDirs);
|
||||
}
|
||||
|
||||
void setLayoutMode(LayoutMode mode) {
|
||||
state = state.copyWith(layoutMode: mode);
|
||||
}
|
||||
@ -103,8 +108,8 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
|
||||
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||
state = state.copyWith(systemTitleBar: isSystemTitleBar);
|
||||
if (DesktopTools.platform.isDesktop) {
|
||||
DesktopTools.window.setTitleBarStyle(
|
||||
if (kIsDesktop) {
|
||||
windowManager.setTitleBarStyle(
|
||||
isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
|
||||
);
|
||||
}
|
||||
@ -151,8 +156,8 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
);
|
||||
}
|
||||
|
||||
if (DesktopTools.platform.isDesktop) {
|
||||
await DesktopTools.window.setTitleBarStyle(
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,10 +62,10 @@ class UserPreferences with _$UserPreferences {
|
||||
@Default(false) bool amoledDarkTheme,
|
||||
@Default(true) bool checkUpdate,
|
||||
@Default(false) bool normalizeAudio,
|
||||
@Default(true) bool showSystemTrayIcon,
|
||||
@Default(false) bool showSystemTrayIcon,
|
||||
@Default(false) bool skipNonMusic,
|
||||
@Default(false) bool systemTitleBar,
|
||||
@Default(CloseBehavior.minimizeToTray) CloseBehavior closeBehavior,
|
||||
@Default(CloseBehavior.close) CloseBehavior closeBehavior,
|
||||
@Default(SpotubeColor(0xFF2196F3, name: "Blue"))
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
@ -84,6 +84,7 @@ class UserPreferences with _$UserPreferences {
|
||||
@Default(Market.US) Market recommendationMarket,
|
||||
@Default(SearchMode.youtube) SearchMode searchMode,
|
||||
@Default("") String downloadLocation,
|
||||
@Default([]) List<String> localLibraryLocation,
|
||||
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
|
||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||
@Default(AudioSource.youtube) AudioSource audioSource,
|
||||
|
||||
@ -12,7 +12,7 @@ part of 'user_preferences_state.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) {
|
||||
return _UserPreferences.fromJson(json);
|
||||
@ -43,6 +43,7 @@ mixin _$UserPreferences {
|
||||
Market get recommendationMarket => throw _privateConstructorUsedError;
|
||||
SearchMode get searchMode => throw _privateConstructorUsedError;
|
||||
String get downloadLocation => throw _privateConstructorUsedError;
|
||||
List<String> get localLibraryLocation => throw _privateConstructorUsedError;
|
||||
String get pipedInstance => throw _privateConstructorUsedError;
|
||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||
AudioSource get audioSource => throw _privateConstructorUsedError;
|
||||
@ -88,6 +89,7 @@ abstract class $UserPreferencesCopyWith<$Res> {
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
List<String> localLibraryLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
@ -126,6 +128,7 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? localLibraryLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
@ -196,6 +199,10 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
localLibraryLocation: null == localLibraryLocation
|
||||
? _value.localLibraryLocation
|
||||
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
@ -264,6 +271,7 @@ abstract class _$$UserPreferencesImplCopyWith<$Res>
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
List<String> localLibraryLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
@ -300,6 +308,7 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? localLibraryLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
@ -370,6 +379,10 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
localLibraryLocation: null == localLibraryLocation
|
||||
? _value._localLibraryLocation
|
||||
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
@ -415,10 +428,10 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
this.amoledDarkTheme = false,
|
||||
this.checkUpdate = true,
|
||||
this.normalizeAudio = false,
|
||||
this.showSystemTrayIcon = true,
|
||||
this.showSystemTrayIcon = false,
|
||||
this.skipNonMusic = false,
|
||||
this.systemTitleBar = false,
|
||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||
this.closeBehavior = CloseBehavior.close,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
@ -433,6 +446,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
this.recommendationMarket = Market.US,
|
||||
this.searchMode = SearchMode.youtube,
|
||||
this.downloadLocation = "",
|
||||
final List<String> localLibraryLocation = const [],
|
||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.audioSource = AudioSource.youtube,
|
||||
@ -440,7 +454,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
this.downloadMusicCodec = SourceCodecs.m4a,
|
||||
this.discordPresence = true,
|
||||
this.endlessPlayback = true,
|
||||
this.enableConnect = false});
|
||||
this.enableConnect = false})
|
||||
: _localLibraryLocation = localLibraryLocation;
|
||||
|
||||
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$UserPreferencesImplFromJson(json);
|
||||
@ -496,6 +511,16 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
@override
|
||||
@JsonKey()
|
||||
final String downloadLocation;
|
||||
final List<String> _localLibraryLocation;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<String> get localLibraryLocation {
|
||||
if (_localLibraryLocation is EqualUnmodifiableListView)
|
||||
return _localLibraryLocation;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_localLibraryLocation);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final String pipedInstance;
|
||||
@ -523,7 +548,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)';
|
||||
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -560,6 +585,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
other.searchMode == searchMode) &&
|
||||
(identical(other.downloadLocation, downloadLocation) ||
|
||||
other.downloadLocation == downloadLocation) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._localLibraryLocation, _localLibraryLocation) &&
|
||||
(identical(other.pipedInstance, pipedInstance) ||
|
||||
other.pipedInstance == pipedInstance) &&
|
||||
(identical(other.themeMode, themeMode) ||
|
||||
@ -597,6 +624,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
||||
recommendationMarket,
|
||||
searchMode,
|
||||
downloadLocation,
|
||||
const DeepCollectionEquality().hash(_localLibraryLocation),
|
||||
pipedInstance,
|
||||
themeMode,
|
||||
audioSource,
|
||||
@ -647,6 +675,7 @@ abstract class _UserPreferences implements UserPreferences {
|
||||
final Market recommendationMarket,
|
||||
final SearchMode searchMode,
|
||||
final String downloadLocation,
|
||||
final List<String> localLibraryLocation,
|
||||
final String pipedInstance,
|
||||
final ThemeMode themeMode,
|
||||
final AudioSource audioSource,
|
||||
@ -698,6 +727,8 @@ abstract class _UserPreferences implements UserPreferences {
|
||||
@override
|
||||
String get downloadLocation;
|
||||
@override
|
||||
List<String> get localLibraryLocation;
|
||||
@override
|
||||
String get pipedInstance;
|
||||
@override
|
||||
ThemeMode get themeMode;
|
||||
|
||||
@ -16,12 +16,12 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson(
|
||||
amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false,
|
||||
checkUpdate: json['checkUpdate'] as bool? ?? true,
|
||||
normalizeAudio: json['normalizeAudio'] as bool? ?? false,
|
||||
showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true,
|
||||
showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false,
|
||||
skipNonMusic: json['skipNonMusic'] as bool? ?? false,
|
||||
systemTitleBar: json['systemTitleBar'] as bool? ?? false,
|
||||
closeBehavior:
|
||||
$enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ??
|
||||
CloseBehavior.minimizeToTray,
|
||||
CloseBehavior.close,
|
||||
accentColorScheme: UserPreferences._accentColorSchemeReadValue(
|
||||
json, 'accentColorScheme') ==
|
||||
null
|
||||
@ -44,6 +44,10 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson(
|
||||
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
|
||||
SearchMode.youtube,
|
||||
downloadLocation: json['downloadLocation'] as String? ?? "",
|
||||
localLibraryLocation: (json['localLibraryLocation'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
pipedInstance:
|
||||
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
|
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
@ -81,6 +85,7 @@ Map<String, dynamic> _$$UserPreferencesImplToJson(
|
||||
'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!,
|
||||
'searchMode': _$SearchModeEnumMap[instance.searchMode]!,
|
||||
'downloadLocation': instance.downloadLocation,
|
||||
'localLibraryLocation': instance.localLibraryLocation,
|
||||
'pipedInstance': instance.pipedInstance,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'audioSource': _$AudioSourceEnumMap[instance.audioSource]!,
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:media_kit/media_kit.dart' as mk;
|
||||
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
|
||||
part 'audio_players_streams_mixin.dart';
|
||||
part 'audio_player_impl.dart';
|
||||
@ -30,12 +31,18 @@ class SpotubeMedia extends mk.Media {
|
||||
: "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}",
|
||||
extras: {
|
||||
...?extras,
|
||||
"track": track.toJson(),
|
||||
"track": switch (track) {
|
||||
LocalTrack() => track.toJson(),
|
||||
SourcedTrack() => track.toJson(),
|
||||
_ => track.toJson(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
factory SpotubeMedia.fromMedia(mk.Media media) {
|
||||
final track = Track.fromJson(media.extras?["track"]);
|
||||
final track = media.uri.startsWith("http")
|
||||
? Track.fromJson(media.extras?["track"])
|
||||
: LocalTrack.fromJson(media.extras?["track"]);
|
||||
return SpotubeMedia(track);
|
||||
}
|
||||
}
|
||||
@ -101,7 +108,7 @@ abstract class AudioPlayerInterface {
|
||||
return _mkPlayer.state.completed;
|
||||
}
|
||||
|
||||
Future<bool> get isShuffled async {
|
||||
bool get isShuffled {
|
||||
return _mkPlayer.shuffled;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
||||
@ -7,6 +6,7 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
/// MediaKit [Player] by default doesn't have a state stream.
|
||||
/// This class adds a state stream to the [Player] class.
|
||||
@ -54,7 +54,7 @@ class CustomPlayer extends Player {
|
||||
PackageInfo.fromPlatform().then((packageInfo) {
|
||||
_packageName = packageInfo.packageName;
|
||||
});
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
if (kIsAndroid) {
|
||||
_androidAudioManager = AndroidAudioManager();
|
||||
AudioSession.instance.then((s) async {
|
||||
_androidAudioSessionId =
|
||||
@ -71,7 +71,7 @@ class CustomPlayer extends Player {
|
||||
}
|
||||
|
||||
Future<void> notifyAudioSessionUpdate(bool active) async {
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
if (kIsAndroid) {
|
||||
sendBroadcast(
|
||||
BroadcastMessage(
|
||||
name: active
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
@ -8,6 +7,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
||||
import 'package:spotube/services/audio_services/windows_audio_service.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class AudioServices {
|
||||
final MobileAudioService? mobile;
|
||||
@ -19,9 +19,7 @@ class AudioServices {
|
||||
Ref ref,
|
||||
ProxyPlaylistNotifier playback,
|
||||
) async {
|
||||
final mobile = DesktopTools.platform.isMobile ||
|
||||
DesktopTools.platform.isMacOS ||
|
||||
DesktopTools.platform.isLinux
|
||||
final mobile = kIsMobile || kIsMacOS || kIsLinux
|
||||
? await AudioService.init(
|
||||
builder: () => MobileAudioService(playback),
|
||||
config: const AudioServiceConfig(
|
||||
@ -31,9 +29,7 @@ class AudioServices {
|
||||
),
|
||||
)
|
||||
: null;
|
||||
final smtc = DesktopTools.platform.isWindows
|
||||
? WindowsAudioService(ref, playback)
|
||||
: null;
|
||||
final smtc = kIsWindows ? WindowsAudioService(ref, playback) : null;
|
||||
|
||||
return AudioServices(
|
||||
mobile,
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||
|
||||
abstract class KVStoreService {
|
||||
static SharedPreferences? _sharedPreferences;
|
||||
@ -23,4 +26,21 @@ abstract class KVStoreService {
|
||||
|
||||
static Future<void> setRecentSearches(List<String> value) async =>
|
||||
await sharedPreferences.setStringList('recentSearches', value);
|
||||
|
||||
static WindowSize? get windowSize {
|
||||
final raw = sharedPreferences.getString('windowSize');
|
||||
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
return WindowSize.fromJson(jsonDecode(raw));
|
||||
}
|
||||
|
||||
static Future<void> setWindowSize(WindowSize value) async =>
|
||||
await sharedPreferences.setString(
|
||||
'windowSize',
|
||||
jsonEncode(
|
||||
value.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ part of 'song_link.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SongLink _$SongLinkFromJson(Map<String, dynamic> json) {
|
||||
return _SongLink.fromJson(json);
|
||||
|
||||
@ -163,7 +163,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||
query,
|
||||
preference.searchMode == SearchMode.youtube
|
||||
? PipedFilter.videos
|
||||
? PipedFilter.video
|
||||
: PipedFilter.musicSongs,
|
||||
);
|
||||
|
||||
|
||||
88
lib/services/wm_tools/wm_tools.dart
Normal file
88
lib/services/wm_tools/wm_tools.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowSize {
|
||||
final double height;
|
||||
final double width;
|
||||
final bool maximized;
|
||||
|
||||
WindowSize({
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.maximized,
|
||||
});
|
||||
|
||||
factory WindowSize.fromJson(Map<String, dynamic> json) => WindowSize(
|
||||
height: json["height"],
|
||||
width: json["width"],
|
||||
maximized: json["maximized"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"height": height,
|
||||
"width": width,
|
||||
"maximized": maximized,
|
||||
};
|
||||
}
|
||||
|
||||
class WindowManagerTools with WidgetsBindingObserver {
|
||||
static WindowManagerTools? _instance;
|
||||
static WindowManagerTools get instance => _instance!;
|
||||
|
||||
WindowManagerTools._();
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await windowManager.ensureInitialized();
|
||||
_instance = WindowManagerTools._();
|
||||
WidgetsBinding.instance.addObserver(instance);
|
||||
|
||||
await windowManager.waitUntilReadyToShow(
|
||||
const WindowOptions(
|
||||
title: "Spotube",
|
||||
backgroundColor: Colors.transparent,
|
||||
minimumSize: Size(300, 700),
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
),
|
||||
() async {
|
||||
final savedSize = KVStoreService.windowSize;
|
||||
await windowManager.setResizable(true);
|
||||
if (savedSize?.maximized == true &&
|
||||
!(await windowManager.isMaximized())) {
|
||||
await windowManager.maximize();
|
||||
} else if (savedSize != null) {
|
||||
await windowManager.setSize(Size(savedSize.width, savedSize.height));
|
||||
}
|
||||
|
||||
await windowManager.focus();
|
||||
await windowManager.show();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Size? _prevSize;
|
||||
|
||||
@override
|
||||
void didChangeMetrics() async {
|
||||
super.didChangeMetrics();
|
||||
if (kIsMobile) return;
|
||||
final size = await windowManager.getSize();
|
||||
final windowSameDimension =
|
||||
_prevSize?.width == size.width && _prevSize?.height == size.height;
|
||||
|
||||
if (windowSameDimension || _prevSize == null) {
|
||||
_prevSize = size;
|
||||
return;
|
||||
}
|
||||
final isMaximized = await windowManager.isMaximized();
|
||||
await KVStoreService.setWindowSize(
|
||||
WindowSize(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
maximized: isMaximized,
|
||||
),
|
||||
);
|
||||
_prevSize = size;
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/widgets.dart' hide Element;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/dom.dart' hide Text;
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/root/update_dialog.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
@ -14,6 +14,16 @@ import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html/parser.dart' as parser;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart' hide Element;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
abstract class ServiceUtils {
|
||||
static final logger = getLogger("ServiceUtils");
|
||||
|
||||
@ -318,4 +328,66 @@ abstract class ServiceUtils {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> checkForUpdates(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
if (!Env.enableUpdateChecker) return;
|
||||
if (!ref.read(userPreferencesProvider.select((s) => s.checkUpdate))) return;
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (Env.releaseChannel == ReleaseChannel.nightly) {
|
||||
final value = await http.get(
|
||||
Uri.parse(
|
||||
"https://api.github.com/repos/KRTirtho/spotube/actions/workflows/spotube-release-binary.yml/runs?status=success&per_page=1",
|
||||
),
|
||||
);
|
||||
|
||||
final buildNum =
|
||||
jsonDecode(value.body)["workflow_runs"][0]["run_number"] as int;
|
||||
|
||||
if (buildNum <= int.parse(packageInfo.buildNumber) || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.black26,
|
||||
builder: (context) {
|
||||
return RootAppUpdateDialog.nightly(nightlyBuildNum: buildNum);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final value = await http.get(
|
||||
Uri.parse(
|
||||
"https://api.github.com/repos/KRTirtho/spotube/releases/latest",
|
||||
),
|
||||
);
|
||||
final tagName =
|
||||
(jsonDecode(value.body)["tag_name"] as String).replaceAll("v", "");
|
||||
final currentVersion = packageInfo.version == "Unknown"
|
||||
? null
|
||||
: Version.parse(packageInfo.version);
|
||||
final latestVersion =
|
||||
tagName == "nightly" ? null : Version.parse(tagName);
|
||||
|
||||
if (currentVersion == null ||
|
||||
latestVersion == null ||
|
||||
(latestVersion.isPreRelease && !currentVersion.isPreRelease) ||
|
||||
(!latestVersion.isPreRelease && currentVersion.isPreRelease)) return;
|
||||
|
||||
if (latestVersion <= currentVersion || !context.mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.black26,
|
||||
builder: (context) {
|
||||
return RootAppUpdateDialog(version: latestVersion);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <system_theme/system_theme_plugin.h>
|
||||
#include <system_tray/system_tray_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
#include <window_size/window_size_plugin.h>
|
||||
@ -47,6 +48,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) system_tray_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin");
|
||||
system_tray_plugin_register_with_registrar(system_tray_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user