mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'dev' into website
This commit is contained in:
commit
cab61631ef
@ -1,177 +0,0 @@
|
|||||||
version: 2.1
|
|
||||||
|
|
||||||
orbs:
|
|
||||||
gh: circleci/github-cli@2.2.0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
flutter_linux_arm:
|
|
||||||
machine:
|
|
||||||
image: ubuntu-2204:current
|
|
||||||
resource_class: arm.medium
|
|
||||||
parameters:
|
|
||||||
version:
|
|
||||||
type: string
|
|
||||||
default: 3.1.1
|
|
||||||
channel:
|
|
||||||
type: enum
|
|
||||||
enum:
|
|
||||||
- release
|
|
||||||
- nightly
|
|
||||||
default: release
|
|
||||||
github_run_number:
|
|
||||||
type: string
|
|
||||||
default: "0"
|
|
||||||
dry_run:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- gh/setup
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Get current date
|
|
||||||
command: |
|
|
||||||
echo "export CURRENT_DATE=$(date +%Y-%m-%d)" >> $BASH_ENV
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Install dependencies
|
|
||||||
command: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev zip rpm
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Install Flutter
|
|
||||||
command: |
|
|
||||||
git clone https://github.com/flutter/flutter.git
|
|
||||||
cd flutter && git checkout stable && cd ..
|
|
||||||
export PATH="$PATH:`pwd`/flutter/bin"
|
|
||||||
flutter precache
|
|
||||||
flutter doctor -v
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Install AppImageTool
|
|
||||||
command: |
|
|
||||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage"
|
|
||||||
chmod +x appimagetool
|
|
||||||
mv appimagetool flutter/bin
|
|
||||||
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: flutter
|
|
||||||
paths:
|
|
||||||
- .
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
equal: [<< parameters.channel >>, nightly]
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
|
||||||
command: |
|
|
||||||
curl -sS https://webi.sh/yq | sh
|
|
||||||
yq -i '.version |= sub("\+\d+", "+<< parameters.channel >>.")' pubspec.yaml
|
|
||||||
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
|
|
||||||
echo 'export BUILD_VERSION="<< parameters.version >>+<< parameters.channel >>.<< parameters.github_run_number >>"' >> $BASH_ENV
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
equal: [<< parameters.channel >>, release]
|
|
||||||
steps:
|
|
||||||
- run: echo 'export BUILD_VERSION="<< parameters.version >>"' >> $BASH_ENV
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Generate .env file
|
|
||||||
command: |
|
|
||||||
echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Replace Version in files
|
|
||||||
command: |
|
|
||||||
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${BUILD_VERSION}" date="${CURRENT_DATE}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
|
|
||||||
echo "build_arch: aarch64" >> linux/packaging/rpm/make_config.yaml
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Build secrets
|
|
||||||
command: |
|
|
||||||
export PATH="$PATH:`pwd`/flutter/bin"
|
|
||||||
flutter config --enable-linux-desktop
|
|
||||||
flutter pub get
|
|
||||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Build Flutter app
|
|
||||||
command: |
|
|
||||||
export PATH="$PATH:`pwd`/flutter/bin"
|
|
||||||
export PATH="$PATH":"$HOME/.pub-cache/bin"
|
|
||||||
dart pub global activate flutter_distributor
|
|
||||||
alias dpkg-deb="dpkg-deb --Zxz"
|
|
||||||
flutter_distributor package --platform=linux --targets=deb
|
|
||||||
flutter_distributor package --platform=linux --targets=appimage
|
|
||||||
flutter_distributor package --platform=linux --targets=rpm
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
equal: [<< parameters.channel >>, nightly]
|
|
||||||
steps:
|
|
||||||
- run: make tar VERSION=nightly ARCH=arm64 PKG_ARCH=aarch64
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
equal: [<< parameters.channel >>, release]
|
|
||||||
steps:
|
|
||||||
- run: make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Move artifacts
|
|
||||||
command: |
|
|
||||||
mkdir bundle
|
|
||||||
mv build/spotube-linux-*-aarch64.tar.xz bundle/
|
|
||||||
mv dist/**/spotube-*-linux.deb bundle/Spotube-linux-aarch64.deb
|
|
||||||
mv dist/**/spotube-*-linux.rpm bundle/Spotube-linux-aarch64.rpm
|
|
||||||
mv dist/**/spotube-*-linux.AppImage bundle/Spotube-linux-aarch64.AppImage
|
|
||||||
zip -r Spotube-linux-aarch64.zip bundle
|
|
||||||
|
|
||||||
- store_artifacts:
|
|
||||||
path: Spotube-linux-aarch64.zip
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
and:
|
|
||||||
- equal: [<< parameters.dry_run >>, false]
|
|
||||||
- equal: [<< parameters.channel >>, release]
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
name: Upload to release (release)
|
|
||||||
command: gh release upload v<< parameters.version >> bundle/* --clobber
|
|
||||||
|
|
||||||
- when:
|
|
||||||
condition:
|
|
||||||
and:
|
|
||||||
- equal: [<< parameters.dry_run >>, false]
|
|
||||||
- equal: [<< parameters.channel >>, nightly]
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
name: Upload to release (nightly)
|
|
||||||
command: gh release upload nightly bundle/* --clobber
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
GHA_Actor:
|
|
||||||
type: string
|
|
||||||
default: ""
|
|
||||||
GHA_Action:
|
|
||||||
type: string
|
|
||||||
default: ""
|
|
||||||
GHA_Event:
|
|
||||||
type: string
|
|
||||||
default: ""
|
|
||||||
GHA_Meta:
|
|
||||||
type: string
|
|
||||||
default: ""
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
build_flutter_for_arm_workflow:
|
|
||||||
when: << pipeline.parameters.GHA_Action >>
|
|
||||||
jobs:
|
|
||||||
- flutter_linux_arm:
|
|
||||||
context:
|
|
||||||
- org-global
|
|
||||||
- GITHUB_CREDS
|
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
build
|
||||||
|
dist
|
||||||
|
.dart_tool
|
||||||
|
.idea
|
||||||
|
.github
|
||||||
|
.git
|
@ -9,3 +9,8 @@ ENABLE_UPDATE_CHECK=
|
|||||||
|
|
||||||
LASTFM_API_KEY=
|
LASTFM_API_KEY=
|
||||||
LASTFM_API_SECRET=
|
LASTFM_API_SECRET=
|
||||||
|
|
||||||
|
# Release channel. Can be: nightly, stable
|
||||||
|
RELEASE_CHANNEL=
|
||||||
|
|
||||||
|
HIDE_DONATIONS=
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.19.1",
|
"flutterSdkVersion": "3.24.3"
|
||||||
"flavors": {}
|
|
||||||
}
|
}
|
25
.github/Dockerfile
vendored
Normal file
25
.github/Dockerfile
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
ARG FLUTTER_VERSION
|
||||||
|
|
||||||
|
FROM --platform=linux/arm64 krtirtho/flutter_distributor:${FLUTTER_VERSION}
|
||||||
|
|
||||||
|
ARG BUILD_VERSION
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN chown -R $(whoami) /app
|
||||||
|
|
||||||
|
RUN rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
RUN flutter pub get
|
||||||
|
|
||||||
|
RUN alias dpkg-deb="dpkg-deb --Zxz" &&\
|
||||||
|
flutter_distributor package --platform=linux --targets=deb --skip-clean
|
||||||
|
|
||||||
|
RUN make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64
|
||||||
|
|
||||||
|
RUN mv build/spotube-linux-*-aarch64.tar.xz dist/ &&\
|
||||||
|
mv dist/**/spotube-*-linux.deb dist/Spotube-linux-aarch64.deb
|
||||||
|
|
||||||
|
CMD [ "sleep", "5000000" ]
|
32
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
32
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -7,8 +7,12 @@ labels:
|
|||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this? (Please read the description)
|
||||||
description: Make sure to check if this issue is a duplicate.
|
description: |
|
||||||
|
PLEASE! Make sure to check if this issue is a duplicate.
|
||||||
|
Don't waste our time, we are working hard to make Spotube better for you.
|
||||||
|
|
||||||
|
Try with multiple similar keywords, and check the closed issues too.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing issues
|
||||||
required: true
|
required: true
|
||||||
@ -16,23 +20,41 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Current Behavior
|
label: Current Behavior
|
||||||
description: Write what you are experiencing currently.
|
description: Write what you are experiencing currently.
|
||||||
|
placeholder: |
|
||||||
|
The app isn't working as expected. It crashes when I do this...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Expected Behavior
|
label: Expected Behavior
|
||||||
description: Write what you expected to happen.
|
description: Write what you expected to happen.
|
||||||
|
placeholder: |
|
||||||
|
The app should do this when I do that...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Steps to reproduce
|
||||||
description: Steps to reproduce the issue. A not well written description might delay the resolve of it.
|
description: Steps to reproduce the issue. A not well written description might lead to the delay in fixing the issue.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. I opened the app
|
1. I opened the app
|
||||||
2. I did this
|
2. I did this
|
||||||
3. And that
|
3. And that
|
||||||
4. Then this happened
|
4. Then this happened
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: |
|
||||||
|
If you have any logs, paste them here. Make sure to remove any sensitive information.
|
||||||
|
You can find the logs in the app's Settings > Developers > Logs page.
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Logs</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
<Replace this line by pasting your logs here>
|
||||||
|
```
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@ -53,7 +75,7 @@ body:
|
|||||||
description: Where did you install Spotube from?
|
description: Where did you install Spotube from?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- "Website (spotube.netlify.app) or (spotube.krtirtho.dev)"
|
- "Website (spotube.krtirtho.dev)"
|
||||||
- "GitHub Releases (Binary)"
|
- "GitHub Releases (Binary)"
|
||||||
- "GitHub Actions (Nightly Binary)"
|
- "GitHub Actions (Nightly Binary)"
|
||||||
- "Play Store (Android)"
|
- "Play Store (Android)"
|
||||||
@ -74,7 +96,7 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Self grab
|
label: Self grab
|
||||||
description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions!
|
description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. Any contributions are welcome!
|
||||||
options:
|
options:
|
||||||
- label: I'm ready to work on this issue!
|
- label: I'm ready to work on this issue!
|
||||||
required: false
|
required: false
|
4
.github/workflows/pr-lint.yml
vendored
4
.github/workflows/pr-lint.yml
vendored
@ -4,13 +4,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: '3.16.0'
|
FLUTTER_VERSION: 3.22.2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
43
.github/workflows/spotube-publish-binary.yml
vendored
43
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version to publish (x.x.x)
|
description: Version to publish (x.x.x)
|
||||||
default: 3.1.0
|
default: 3.8.3
|
||||||
required: true
|
required: true
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
description: Dry run
|
||||||
@ -12,10 +12,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
jobs:
|
jobs:
|
||||||
description: Jobs to run (flathub,aur,winget,chocolatey)
|
description: Jobs to run (flathub,aur,winget,chocolatey,playstore)
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: "flathub,aur,winget,chocolatey"
|
default: "flathub,aur,winget,chocolatey,playstore"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
flathub:
|
flathub:
|
||||||
@ -66,7 +66,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Release to AUR
|
- name: Release to AUR
|
||||||
if: ${{ !inputs.dry_run }}
|
if: ${{ !inputs.dry_run }}
|
||||||
uses: KSXGitHub/github-actions-deploy-aur@v2.7.0
|
uses: KSXGitHub/github-actions-deploy-aur@v2.7.2
|
||||||
with:
|
with:
|
||||||
pkgname: spotube-bin
|
pkgname: spotube-bin
|
||||||
pkgbuild: aur-struct/PKGBUILD
|
pkgbuild: aur-struct/PKGBUILD
|
||||||
@ -76,12 +76,12 @@ jobs:
|
|||||||
commit_message: Updated to v${{ inputs.version }}
|
commit_message: Updated to v${{ inputs.version }}
|
||||||
|
|
||||||
winget:
|
winget:
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
if: contains(inputs.jobs, 'winget')
|
if: contains(inputs.jobs, 'winget')
|
||||||
steps:
|
steps:
|
||||||
- name: Release winget package
|
- name: Release winget package
|
||||||
if: ${{ !inputs.dry_run }}
|
if: ${{ !inputs.dry_run }}
|
||||||
uses: vedantmgoyal2009/winget-releaser@v2
|
uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version }}
|
version: ${{ inputs.version }}
|
||||||
release-tag: v${{ inputs.version }}
|
release-tag: v${{ inputs.version }}
|
||||||
@ -104,3 +104,34 @@ jobs:
|
|||||||
- name: Publish to Chocolatey Repository
|
- name: Publish to Chocolatey Repository
|
||||||
if: ${{ !inputs.dry_run }}
|
if: ${{ !inputs.dry_run }}
|
||||||
run: choco push Spotube-windows-x86_64.nupkg --source https://push.chocolatey.org/
|
run: choco push Spotube-windows-x86_64.nupkg --source https://push.chocolatey.org/
|
||||||
|
|
||||||
|
playstore:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(inputs.jobs, 'playstore')
|
||||||
|
steps:
|
||||||
|
- name: Tagname (workflow dispatch)
|
||||||
|
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: robinraju/release-downloader@main
|
||||||
|
with:
|
||||||
|
repository: KRTirtho/spotube
|
||||||
|
tag: v${{ env.TAG_NAME }}
|
||||||
|
tarBall: false
|
||||||
|
zipBall: false
|
||||||
|
out-file-path: dist
|
||||||
|
fileName: "Spotube-playstore-all-arch.aab"
|
||||||
|
|
||||||
|
- name: Create service-account.json
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
|
||||||
|
|
||||||
|
- name: Upload Android Release to Play Store
|
||||||
|
if: ${{!inputs.dry_run}}
|
||||||
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
with:
|
||||||
|
serviceAccountJson: ./service-account.json
|
||||||
|
releaseFiles: ./dist/Spotube-playstore-all-arch.aab
|
||||||
|
packageName: oss.krtirtho.spotube
|
||||||
|
track: production
|
||||||
|
status: draft
|
||||||
|
releaseName: ${{ env.TAG_NAME }}
|
||||||
|
457
.github/workflows/spotube-release-binary.yml
vendored
457
.github/workflows/spotube-release-binary.yml
vendored
@ -2,399 +2,126 @@ name: Spotube Release Binary
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
|
||||||
description: Version to release (x.x.x)
|
|
||||||
default: 3.4.1
|
|
||||||
required: true
|
|
||||||
channel:
|
channel:
|
||||||
type: choice
|
type: choice
|
||||||
description: Release Channel
|
|
||||||
required: true
|
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- nightly
|
- nightly
|
||||||
default: nightly
|
default: nightly
|
||||||
|
description: The release channel
|
||||||
debug:
|
debug:
|
||||||
description: Debug on failed when channel is nightly
|
|
||||||
required: true
|
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
description: Debug with SSH toggle
|
||||||
|
required: false
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
|
||||||
required: true
|
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: false
|
||||||
|
description: Dry run without uploading to release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: '3.19.1'
|
FLUTTER_VERSION: 3.24.3
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
windows:
|
build_platform:
|
||||||
runs-on: windows-latest
|
strategy:
|
||||||
steps:
|
matrix:
|
||||||
- uses: actions/checkout@v4
|
include:
|
||||||
- uses: subosito/flutter-action@v2.12.0
|
- os: ubuntu-latest
|
||||||
with:
|
platform: linux
|
||||||
cache: true
|
files: |
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
dist/Spotube-linux-x86_64.deb
|
||||||
|
dist/Spotube-linux-x86_64.rpm
|
||||||
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
dist/spotube-linux-*-x86_64.tar.xz
|
||||||
if: ${{ inputs.channel == 'nightly' }}
|
- os: ubuntu-latest
|
||||||
run: |
|
platform: linux_arm
|
||||||
choco install sed make yq -y
|
files: |
|
||||||
yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
|
dist/Spotube-linux-aarch64.deb
|
||||||
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
|
dist/spotube-linux-*-aarch64.tar.xz
|
||||||
"BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $env:GITHUB_ENV
|
- os: ubuntu-latest
|
||||||
|
platform: android
|
||||||
- name: BUILD_VERSION Env (stable)
|
files: |
|
||||||
if: ${{ inputs.channel == 'stable' }}
|
build/Spotube-android-all-arch.apk
|
||||||
run: |
|
build/Spotube-playstore-all-arch.aab
|
||||||
"BUILD_VERSION=${{ inputs.version }}" >> $env:GITHUB_ENV
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
- name: Replace version in files
|
files: |
|
||||||
run: |
|
|
||||||
choco install sed make -y
|
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" windows/runner/Runner.rc
|
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/tools/VERIFICATION.txt
|
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ env.BUILD_VERSION }}/" choco-struct/spotube.nuspec
|
|
||||||
|
|
||||||
- name: Create Stable .env
|
|
||||||
if: ${{ inputs.channel == 'stable' }}
|
|
||||||
run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
|
|
||||||
|
|
||||||
- name: Create Nightly .env
|
|
||||||
if: ${{ inputs.channel == 'nightly' }}
|
|
||||||
run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
|
|
||||||
|
|
||||||
- name: Generating Secrets
|
|
||||||
run: |
|
|
||||||
flutter config --enable-windows-desktop
|
|
||||||
flutter pub get
|
|
||||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
|
||||||
|
|
||||||
- name: Build Windows Executable
|
|
||||||
run: |
|
|
||||||
dart pub global activate flutter_distributor
|
|
||||||
make innoinstall
|
|
||||||
flutter_distributor package --platform=windows --targets=exe --skip-clean
|
|
||||||
mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
|
|
||||||
|
|
||||||
- name: Create Chocolatey Package and set hash
|
|
||||||
if: ${{ inputs.channel == 'stable' }}
|
|
||||||
run: |
|
|
||||||
Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash
|
|
||||||
sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt
|
|
||||||
make choco
|
|
||||||
mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg
|
|
||||||
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
if-no-files-found: error
|
|
||||||
name: Spotube-Release-Binaries
|
|
||||||
path: |
|
|
||||||
dist/Spotube-windows-x86_64.nupkg
|
dist/Spotube-windows-x86_64.nupkg
|
||||||
dist/Spotube-windows-x86_64-setup.exe
|
dist/Spotube-windows-x86_64-setup.exe
|
||||||
|
- os: macos-latest
|
||||||
- name: Debug With SSH When fails
|
platform: ios
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
files: |
|
||||||
uses: mxschmitt/action-tmate@v3
|
Spotube-iOS.ipa
|
||||||
with:
|
- os: macos-14
|
||||||
limit-access-to-actor: true
|
platform: macos
|
||||||
|
files: |
|
||||||
linux:
|
build/Spotube-macos-universal.dmg
|
||||||
runs-on: ubuntu-latest
|
build/Spotube-macos-universal.pkg
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: subosito/flutter-action@v2.12.0
|
- uses: subosito/flutter-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
|
cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Setup Java
|
||||||
- name: Get current date
|
if: ${{matrix.platform == 'android'}}
|
||||||
id: date
|
uses: actions/setup-java@v4
|
||||||
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 == 'release' }}
|
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
distribution: 'zulu'
|
||||||
name: Spotube-Release-Binaries
|
java-version: '17'
|
||||||
path: |
|
cache: 'gradle'
|
||||||
dist/Spotube-linux-x86_64.deb
|
check-latest: true
|
||||||
dist/Spotube-linux-x86_64.rpm
|
- name: Set up QEMU
|
||||||
dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
|
if: ${{matrix.platform == 'linux_arm'}}
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Set up Docker Buildx
|
||||||
if: ${{ inputs.channel == 'nightly' }}
|
if: ${{matrix.platform == 'linux_arm'}}
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
if: ${{matrix.platform != 'linux_arm'}}
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
toolchain: stable
|
||||||
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
|
- name: Install ${{matrix.platform}} dependencies
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
with:
|
|
||||||
limit-access-to-actor: true
|
|
||||||
|
|
||||||
|
|
||||||
android:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: subosito/flutter-action@v2.12.0
|
|
||||||
with:
|
|
||||||
cache: true
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse xmlstarlet
|
|
||||||
|
|
||||||
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
|
|
||||||
if: ${{ inputs.channel == 'nightly' }}
|
|
||||||
run: |
|
|
||||||
curl -sS https://webi.sh/yq | sh
|
|
||||||
yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml
|
|
||||||
yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
|
|
||||||
echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: BUILD_VERSION Env (stable)
|
|
||||||
if: ${{ inputs.channel == 'stable' }}
|
|
||||||
run: |
|
|
||||||
echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Stable .env
|
|
||||||
if: ${{ inputs.channel == 'stable' }}
|
|
||||||
run: echo '${{ secrets.DOTENV_RELEASE }}' > .env
|
|
||||||
|
|
||||||
- name: Create Nightly .env
|
|
||||||
if: ${{ inputs.channel == 'nightly' }}
|
|
||||||
run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
|
|
||||||
|
|
||||||
- name: Generate Secrets
|
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
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
|
- name: Sign Apk
|
||||||
|
if: ${{matrix.platform == 'android'}}
|
||||||
run: |
|
run: |
|
||||||
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
|
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
|
||||||
echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
|
echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
|
||||||
|
|
||||||
- name: Build Apk
|
- name: Unessary hosted tools
|
||||||
run: |
|
if: ${{matrix.platform == 'linux_arm'}}
|
||||||
flutter build apk --flavor ${{ inputs.channel }}
|
uses: jlumbroso/free-disk-space@main
|
||||||
mv build/app/outputs/flutter-apk/app-${{ inputs.channel }}-release.apk build/Spotube-android-all-arch.apk
|
with:
|
||||||
|
tool-cache: false
|
||||||
- name: Build Playstore AppBundle
|
swap-storage: false
|
||||||
run: |
|
android: true
|
||||||
echo 'ENABLE_UPDATE_CHECK=0' >> .env
|
dotnet: true
|
||||||
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
|
haskell: true
|
||||||
export MANIFEST=android/app/src/main/AndroidManifest.xml
|
large-packages: true
|
||||||
xmlstarlet ed -d '//meta-data[@android:name="com.google.android.gms.car.application"]' $MANIFEST > $MANIFEST.tmp
|
docker-images: true
|
||||||
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
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: Spotube-Release-Binaries
|
name: Spotube-Release-Binaries
|
||||||
path: |
|
path: ${{matrix.files}}
|
||||||
build/Spotube-android-all-arch.apk
|
|
||||||
build/Spotube-playstore-all-arch.aab
|
|
||||||
|
|
||||||
- name: Debug With SSH When fails
|
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
with:
|
|
||||||
limit-access-to-actor: true
|
|
||||||
|
|
||||||
macos:
|
|
||||||
|
|
||||||
runs-on: macos-12
|
|
||||||
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: |
|
|
||||||
python3 -m pip install 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-latest
|
|
||||||
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
|
- name: Debug With SSH When fails
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||||
@ -404,14 +131,10 @@ jobs:
|
|||||||
|
|
||||||
upload:
|
upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
needs:
|
needs:
|
||||||
- windows
|
- build_platform
|
||||||
- linux
|
|
||||||
- android
|
|
||||||
- macos
|
|
||||||
- iOS
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Spotube-Release-Binaries
|
name: Spotube-Release-Binaries
|
||||||
@ -427,6 +150,10 @@ jobs:
|
|||||||
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
||||||
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
||||||
|
|
||||||
|
- name: Extract pubspec version
|
||||||
|
run: |
|
||||||
|
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@ -435,17 +162,12 @@ jobs:
|
|||||||
RELEASE.md5sum
|
RELEASE.md5sum
|
||||||
RELEASE.sha256sum
|
RELEASE.sha256sum
|
||||||
|
|
||||||
- name: Debug With SSH
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
with:
|
|
||||||
limit-access-to-actor: true
|
|
||||||
|
|
||||||
- name: Upload Release Binaries (stable)
|
- name: Upload Release Binaries (stable)
|
||||||
if: ${{ !inputs.dry_run && inputs.channel == 'stable' }}
|
if: ${{ !inputs.dry_run && inputs.channel == 'stable' }}
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: v${{ inputs.version }} # mind the "v" prefix
|
tag: v${{ env.PUBSPEC_VERSION }} # mind the "v" prefix
|
||||||
omitBodyDuringUpdate: true
|
omitBodyDuringUpdate: true
|
||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
@ -463,3 +185,8 @@ jobs:
|
|||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||||
|
body: |
|
||||||
|
Build Number: ${{github.run_number}}
|
||||||
|
|
||||||
|
Nightly release includes newest features but may contain bugs
|
||||||
|
It is preferred to use the stable version unless you know what you're doing
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -73,6 +73,10 @@ dist
|
|||||||
appimage-build
|
appimage-build
|
||||||
|
|
||||||
android/key.properties
|
android/key.properties
|
||||||
.fvm/flutter_sdk
|
|
||||||
|
|
||||||
**/pb_data
|
**/pb_data
|
||||||
|
|
||||||
|
tm.json
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
16
.metadata
16
.metadata
@ -1,11 +1,11 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
revision: "300451adae589accbece3490f4396f10bdf15e6e"
|
||||||
channel: stable
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||||
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||||
- platform: macos
|
- platform: windows
|
||||||
create_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||||
base_revision: eb6d86ee27deecba4a83536aa20f366a6044895c
|
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@ -2,13 +2,22 @@
|
|||||||
"cmake.configureOnOpen": false,
|
"cmake.configureOnOpen": false,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"acousticness",
|
"acousticness",
|
||||||
|
"ambiguate",
|
||||||
|
"Amoled",
|
||||||
|
"Buildless",
|
||||||
"danceability",
|
"danceability",
|
||||||
|
"fuzzywuzzy",
|
||||||
|
"gapless",
|
||||||
"instrumentalness",
|
"instrumentalness",
|
||||||
"Mpris",
|
"Mpris",
|
||||||
|
"RGBO",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
"Scrobblenaut",
|
"Scrobblenaut",
|
||||||
|
"skeletonizer",
|
||||||
|
"songlink",
|
||||||
"speechiness",
|
"speechiness",
|
||||||
"Spotube",
|
"Spotube",
|
||||||
|
"titlebar",
|
||||||
"winget"
|
"winget"
|
||||||
],
|
],
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
@ -16,5 +25,7 @@
|
|||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
|
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
|
||||||
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
||||||
}
|
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
|
||||||
|
},
|
||||||
|
"dart.flutterSdkPath": ".fvm/flutter_sdk"
|
||||||
}
|
}
|
170
.vscode/snippets.code-snippets
vendored
Normal file
170
.vscode/snippets.code-snippets
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"PaginatedState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedState",
|
||||||
|
"description": "Generate a PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class ${1:Model}State extends PaginatedState<${2:Model}> {",
|
||||||
|
" ${1:Model}State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" ${1:Model}State copyWith({",
|
||||||
|
" List<${2:Model}>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return ${1:Model}State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PaginatedAsyncNotifier": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedAsyncNotifier",
|
||||||
|
"description": "Generate a PaginatedAsyncNotifier",
|
||||||
|
"body": [
|
||||||
|
"class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {",
|
||||||
|
" ${1:NotifierName}Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(int offset, int limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build() async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PaginaitedNotifierWithState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedNotifierWithState",
|
||||||
|
"description": "Generate a PaginatedNotifier with PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class $1State extends PaginatedState<$2> {",
|
||||||
|
" $1State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" $1State copyWith({",
|
||||||
|
" List<$2>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return $1State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"class $1Notifier",
|
||||||
|
" extends PaginatedAsyncNotifier<$2, $1State> {",
|
||||||
|
" $1Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(int offset, int limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build() async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(",
|
||||||
|
" ()=> $1Notifier(),",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FamilyPaginatedAsyncNotifier": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "familyPaginatedAsyncNotifier",
|
||||||
|
"description": "Generate a FamilyPaginatedAsyncNotifier",
|
||||||
|
"body": [
|
||||||
|
"class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {",
|
||||||
|
" ${1:NotifierName}Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(arg, offset, limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build(arg) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FamilyPaginaitedNotifierWithState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "familyPaginatedNotifierWithState",
|
||||||
|
"description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class $1State extends PaginatedState<$2> {",
|
||||||
|
" $1State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" $1State copyWith({",
|
||||||
|
" List<$2>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return $1State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"class $1Notifier",
|
||||||
|
" extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {",
|
||||||
|
" $1Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(arg, offset, limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build(arg) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(",
|
||||||
|
" ()=> $1Notifier(),",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
185
CHANGELOG.md
185
CHANGELOG.md
@ -2,6 +2,191 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [3.8.3](https://github.com/krtirtho/spotube/compare/v3.8.2...v3.8.3) (2024-10-09)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- update youtube_explode_dart to 2.2.3 to fix no playback (#1980)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **macos**: enable same window webview support
|
||||||
|
|
||||||
|
## [3.8.2](https://github.com/krtirtho/spotube/compare/v3.8.1...v3.8.2) (2024-09-30)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- endless song loading issue and no playback #1925
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.1](https://github.com/krtirtho/spotube/compare/v3.8.0...v3.8.1) (2024-09-15)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **translations**: correct some basque incorrect translations (#1815)
|
||||||
|
- **lyrics**: LRCLIB lyrics should be usable without logging in #1803
|
||||||
|
- playlist displaying descriptions unescaped html #1784
|
||||||
|
- **android**: pressing back while the player is open doesn't take to previous page
|
||||||
|
- handle dublicated items in playback queue correctly #1852
|
||||||
|
- **desktop**: scrollbar overlapping with more options of tracks and playlists
|
||||||
|
- **discord**: stop discord rpc from try update presence when not connected
|
||||||
|
- **stats**: minutes page shows plays and streams page shows minutes which should be the opposite #1880
|
||||||
|
- **android**: clears queue upon swiping away notification
|
||||||
|
- **player**: shuffle button state resets after closing page #1657
|
||||||
|
- getting started page login page exception #1800
|
||||||
|
- **mobile**: queue doesn't persist
|
||||||
|
- local tracks takes time to load
|
||||||
|
- start radio not working #1629
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **desktop**: show error dialog if webview is not found on login #1871
|
||||||
|
- manually detect and define touch behavior #1763
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- translations: make state page's hard coded strings translatable (#1719)
|
||||||
|
- discord: add listening activity type
|
||||||
|
- discord: album art, playing time and play pause support (#1765)
|
||||||
|
- linux: Use XDG_STATE_HOME to storage logs (#1675)
|
||||||
|
- discord rpc for macOS, windows-arm64 and linux-arm64 (#1713)
|
||||||
|
- desktop: implement webview based login
|
||||||
|
- stats: add lazy loading support
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- translations: fix Russian translations (#1696)
|
||||||
|
- ios: permission exception
|
||||||
|
- linux: tray icon wrong name for flatpak
|
||||||
|
- windows: app crashes when no internet
|
||||||
|
- windows: local tracks plays but disabled playback controls
|
||||||
|
- go to track album shows up for local tracks
|
||||||
|
- local track metadata timeout
|
||||||
|
- windows: window stretching #1553
|
||||||
|
- android: app getting killed from background
|
||||||
|
- linux: OS Media control not working for Flatpak #1627
|
||||||
|
- incorrect datatype used for MPRIS position property #1521
|
||||||
|
- Too many artists for a track causing overflows
|
||||||
|
- playlist share button does not work #1639
|
||||||
|
- unescape html escape values #1300
|
||||||
|
- lyrics page doesn't scroll to top after song ends #885
|
||||||
|
- changed source doesn't get saved and uses the wrong once again
|
||||||
|
- null exception in album page navigated from /home
|
||||||
|
- popup menu item opacity
|
||||||
|
- linux: change app id in flatpak environment
|
||||||
|
|
||||||
|
|
||||||
|
## [3.7.1](https://github.com/krtirtho/spotube/compare/v3.7.0...v3.7.1) (2024-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* alternative sources not showing up for SongLink matched results ([37d002d](https://github.com/krtirtho/spotube/commit/37d002d133cacb3a34884713ac8f6637694af57c))
|
||||||
|
* **android:** Media Controls not working above Android 14 [#1561](https://github.com/krtirtho/spotube/issues/1561) ([3394c1b](https://github.com/krtirtho/spotube/commit/3394c1b0574e44dc624b2b2f0bf32f343ae9f049))
|
||||||
|
* browse anonymously button takes to wrong route ([73c5b30](https://github.com/krtirtho/spotube/commit/73c5b30b63a4c82bb7ad5b52bc10c5594566a800))
|
||||||
|
* **desktop:** titlebar drag to move not working ([5f280a1](https://github.com/krtirtho/spotube/commit/5f280a19f4d5f8882ae2ff60c6cc595ded7a5a1d))
|
||||||
|
* **desktop:** window is not centered ([47f98b9](https://github.com/krtirtho/spotube/commit/47f98b98aafab9b426733ed44cab2be8a646a98e))
|
||||||
|
* **ios:** download not working [#1575](https://github.com/krtirtho/spotube/issues/1575) ([6591ec0](https://github.com/krtirtho/spotube/commit/6591ec0e1b441dd8fd535eace19a58c7749389ca))
|
||||||
|
* **linux:** application window not visible after launch ([8fc44ed](https://github.com/krtirtho/spotube/commit/8fc44ed6550e8b2b804991ff82df08afb1c94ca8))
|
||||||
|
* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5))
|
||||||
|
* use weak match for Jiosaavn fallback to improve matching ([6cb2986](https://github.com/krtirtho/spotube/commit/6cb29868d2030b5e9312863c17e8f24889942e24))
|
||||||
|
* **windows:** media controls not showing up [#1542](https://github.com/krtirtho/spotube/issues/1542) ([d7d864f](https://github.com/krtirtho/spotube/commit/d7d864ff2bc937675a544a7edf645c5148ec836a))
|
||||||
|
* **windows:** revert Flutter version to 3.19.6 to avoid distortion [#1553](https://github.com/krtirtho/spotube/issues/1553) ([982cf0b](https://github.com/krtirtho/spotube/commit/982cf0bd435638fa20f17ef527fe21d031b5ffaf))
|
||||||
|
|
||||||
|
## [3.7.0](https://github.com/krtirtho/spotube/compare/v3.6.0...v3.7.0) (2024-06-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* local library folder cards ([fc5bfa0](https://github.com/krtirtho/spotube/commit/fc5bfa089ce2f46ab786565d6750564d704ee7e0))
|
||||||
|
* Local music library ([#1479](https://github.com/krtirtho/spotube/issues/1479)) ([22caa81](https://github.com/krtirtho/spotube/commit/22caa818f4ac31626aaff6952e43512b42237d00))
|
||||||
|
* personalized stats based on local music history ([#1522](https://github.com/krtirtho/spotube/issues/1522)) ([82307bc](https://github.com/krtirtho/spotube/commit/82307bc030035b03ab1b8d8ec7b24da19a866b12))
|
||||||
|
* play initially available tracks of playlist/album immediately and fetch rest in background [#670](https://github.com/krtirtho/spotube/issues/670) ([02acbd9](https://github.com/krtirtho/spotube/commit/02acbd93271145dde365f6c547e0d9d902be65f1))
|
||||||
|
* **player:** add volume slider floating label showing percentage ([#1445](https://github.com/krtirtho/spotube/issues/1445)) ([8fad225](https://github.com/krtirtho/spotube/commit/8fad2251b3536e9468e0fb193939ead98bad3bc6)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
|
||||||
|
* **translations:** add Basque translation ([#1493](https://github.com/krtirtho/spotube/issues/1493)) ([dbc1c45](https://github.com/krtirtho/spotube/commit/dbc1c452dd53153c61589f956ea9836cea7bf2bb))
|
||||||
|
* **translations:** add Finnish translations ([#1449](https://github.com/krtirtho/spotube/issues/1449)) ([edc997e](https://github.com/krtirtho/spotube/commit/edc997e7470ce17f60c96b8198dc8851cbf21f18)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
|
||||||
|
* **translations:** add georgian language ([#1450](https://github.com/krtirtho/spotube/issues/1450)) ([1e7f0e1](https://github.com/krtirtho/spotube/commit/1e7f0e1fe71e0a8d86614fc884861f8791469112))
|
||||||
|
* **translations:** add Indonesian translation ([#1426](https://github.com/krtirtho/spotube/issues/1426)) ([0280654](https://github.com/krtirtho/spotube/commit/0280654bb6bad373aee521f5a866228d2d38f038)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
|
||||||
|
* **translations:** Improve tr locales ([#1419](https://github.com/krtirtho/spotube/issues/1419)) ([bf45681](https://github.com/krtirtho/spotube/commit/bf45681deb951c772bf6ca05e213c949c04bded1)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
|
||||||
|
* upgrade to Flutter 3.22.0 ([71341ec](https://github.com/krtirtho/spotube/commit/71341ec0bda6ed985b43836712075b97a2cf8bac))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fallback to LRCLIB when lyrics line less than 6 lines [#1461](https://github.com/krtirtho/spotube/issues/1461) ([9aea354](https://github.com/krtirtho/spotube/commit/9aea35468fa7cd176ddc8810b37b90c2d8246931))
|
||||||
|
* **linux:** tray icon not showing [#541](https://github.com/krtirtho/spotube/issues/541) ([7ac7917](https://github.com/krtirtho/spotube/commit/7ac791757abb30f40374c169c4211916287bb3f3))
|
||||||
|
* local track not showing up in queue ([d82261c](https://github.com/krtirtho/spotube/commit/d82261cb25ece63f85af0e40216cf32dccdc9dd5))
|
||||||
|
* **macos:** Logs directory not created by default [#1353](https://github.com/krtirtho/spotube/issues/1353) ([4ca8939](https://github.com/krtirtho/spotube/commit/4ca893950b07f678acf7db690112c47d21e54782))
|
||||||
|
* **playback:** skipping tracks with unplayable sources instead of falling back [#1492](https://github.com/krtirtho/spotube/issues/1492) ([c607a33](https://github.com/krtirtho/spotube/commit/c607a330ed279dfbebe8d4bd325745ac6301a58f))
|
||||||
|
* **search:** load more button not working [#1417](https://github.com/krtirtho/spotube/issues/1417) ([7e07c2e](https://github.com/krtirtho/spotube/commit/7e07c2e1985da7ccb96b1fac2ecd703720068d26))
|
||||||
|
* some text are garbled in different parts of the app [#1463](https://github.com/krtirtho/spotube/issues/1463) [#1505](https://github.com/krtirtho/spotube/issues/1505) ([d2683c5](https://github.com/krtirtho/spotube/commit/d2683c52d81d807be6ff72f15b8e9eb18181e211))
|
||||||
|
* spotify friends and user profile icon (mobile) showing when not authenticated [#1410](https://github.com/krtirtho/spotube/issues/1410) ([9bccbc9](https://github.com/krtirtho/spotube/commit/9bccbc93c63dd34f6e15ff68c276976ecd1d9a33))
|
||||||
|
* **updater:** dead link ([#1408](https://github.com/krtirtho/spotube/issues/1408)) ([6907f9c](https://github.com/krtirtho/spotube/commit/6907f9c756d8f49aadb1b23a2a1dc8bf7d658dc0)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1171](https://github.com/krtirtho/spotube/issues/1171) [#1082](https://github.com/krtirtho/spotube/issues/1082) [#1082](https://github.com/krtirtho/spotube/issues/1082)
|
||||||
|
* windows SSL Certificate error breaking login [#905](https://github.com/krtirtho/spotube/issues/905) ([#1474](https://github.com/krtirtho/spotube/issues/1474)) ([937a706](https://github.com/krtirtho/spotube/commit/937a706ac9c0e59943b2609e5cc398dcdbed2344)), closes [#1468](https://github.com/krtirtho/spotube/issues/1468)
|
||||||
|
* **windows:** installer tries to install in current directory ([c3c9fc5](https://github.com/krtirtho/spotube/commit/c3c9fc544c68b3d897dd7241a61cab7a199b4539))
|
||||||
|
|
||||||
|
## [3.6.0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0) (2024-04-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Spotify homepage personalized recommendations ([#1402](https://github.com/krtirtho/spotube/issues/1402)) ([9e25c74](https://github.com/krtirtho/spotube/commit/9e25c742d4e43e4e10d2b48afb8e6d90288ffa11))
|
||||||
|
* add user profile page ([39e97ee](https://github.com/krtirtho/spotube/commit/39e97eef34d87348a264843e145f31f82832d12e))
|
||||||
|
* **android:** Filter Device To Force High Frame Rate ([#880](https://github.com/krtirtho/spotube/issues/880)) ([6e41b10](https://github.com/krtirtho/spotube/commit/6e41b106fa989adee393d3ce2535e75446ad3eea))
|
||||||
|
* improved caching based on riverpod ([#1343](https://github.com/krtirtho/spotube/issues/1343)) ([6673e5a](https://github.com/krtirtho/spotube/commit/6673e5a8a86b9667cf9dbff9bb7c40ea6b7de771))
|
||||||
|
* LAN connect a.k.a control remote Spotube playback and local output device selection ([#1355](https://github.com/krtirtho/spotube/issues/1355)) ([68374ef](https://github.com/krtirtho/spotube/commit/68374efd3ec556f31b937e5b96920787b54eec78))
|
||||||
|
* **lyrics:** add LRCLIB lyrics provider as fallback ([5afe823](https://github.com/krtirtho/spotube/commit/5afe823abdb198340b55d138d8173d886a811632))
|
||||||
|
* search history support [#1236](https://github.com/krtirtho/spotube/issues/1236) ([82b1cfa](https://github.com/krtirtho/spotube/commit/82b1cfa0d775e3958c666280943a893c9113d468))
|
||||||
|
* **translations:** Add Czech translation ([#1401](https://github.com/krtirtho/spotube/issues/1401)) ([5a6b800](https://github.com/krtirtho/spotube/commit/5a6b80091259359bc38c4b91cd8cb496c4270fa4))
|
||||||
|
* **translations:** add Thai Language ([#1319](https://github.com/krtirtho/spotube/issues/1319)) ([b70f250](https://github.com/krtirtho/spotube/commit/b70f250e8d5137fd990787ec9e3d058126cf14f3)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* instance of Artist bug [#1362](https://github.com/krtirtho/spotube/issues/1362) ([c8dd802](https://github.com/krtirtho/spotube/commit/c8dd8025ec96bd78ed77cae35f1429aa48c16fde))
|
||||||
|
* **playback:** sponsor block skips and stutters in same position ([0d080b7](https://github.com/krtirtho/spotube/commit/0d080b77b72529c0be5ebc27ace1c52307511f73))
|
||||||
|
|
||||||
|
## [3.5.0](https://github.com/krtirtho/spotube/compare/v3.4.1...v3.5.0) (2024-03-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add endless playback support [#285](https://github.com/krtirtho/spotube/issues/285) ([9dfd49c](https://github.com/krtirtho/spotube/commit/9dfd49ca04f0e915e333e205b17ac70456873f6e))
|
||||||
|
* add getting started page ([96a2a1f](https://github.com/krtirtho/spotube/commit/96a2a1f5a622cb3c580041417d5023e37fa69716))
|
||||||
|
* Add iOS background play support ([#1166](https://github.com/krtirtho/spotube/issues/1166)) ([095587e](https://github.com/krtirtho/spotube/commit/095587ee84f7d867c69fcf4b09ed608d63478e1e))
|
||||||
|
* add songlink based track matching for youtube and open song link button ([9095a8c](https://github.com/krtirtho/spotube/commit/9095a8c8f849e42daabb7efcc20085cfb863c974))
|
||||||
|
* **playlist:** show confirmation before deleting user playlist [#1222](https://github.com/krtirtho/spotube/issues/1222) ([9f92440](https://github.com/krtirtho/spotube/commit/9f9244062a39759aa0ce28d2d5f7c8fa53d73003))
|
||||||
|
* Sort by Duration ([#1238](https://github.com/krtirtho/spotube/issues/1238)) ([6f8271f](https://github.com/krtirtho/spotube/commit/6f8271f5e9394cb4053e41dd222aa2844c34d609))
|
||||||
|
* start radio support ([4defeef](https://github.com/krtirtho/spotube/commit/4defeefe7e5947aa00a2afb2a06577ec141cdc52))
|
||||||
|
* **translations:** add Korean translation ([#1275](https://github.com/krtirtho/spotube/issues/1275)) ([fdea930](https://github.com/krtirtho/spotube/commit/fdea9307bbfb8f3f62cfb795bfb3ca58c38c33d9))
|
||||||
|
* **translations:** Added Vietnamese ([#1135](https://github.com/krtirtho/spotube/issues/1135)) ([019ba86](https://github.com/krtirtho/spotube/commit/019ba865e20a8b54ea3490c01e47158eaf3a4c8d))
|
||||||
|
* **windows:** Install Visual C++ 2015-2022 Redistributable if missing when installing ([ba69496](https://github.com/krtirtho/spotube/commit/ba69496dcc9a1b7f6ea4e104e71764a854d27f1f))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* album images are small in certain places ([ca76a39](https://github.com/krtirtho/spotube/commit/ca76a39910b1a5af91aa7882a0d33c9d71db58a2))
|
||||||
|
* album, artist page not loading [#1282](https://github.com/krtirtho/spotube/issues/1282) ([a9a1d4c](https://github.com/krtirtho/spotube/commit/a9a1d4c9dc24aaf3181dc4090d1822ebfe755991))
|
||||||
|
* **android:** audio issue when screen is off and broadcast audio session id ([#1221](https://github.com/krtirtho/spotube/issues/1221) & [#1247](https://github.com/krtirtho/spotube/issues/1247)) ([17105a6](https://github.com/krtirtho/spotube/commit/17105a640bf5107bd5d333b9b4d097c14a3949a2)), closes [KRTirtho/spotube#571](https://github.com/KRTirtho/spotube/issues/571)
|
||||||
|
* **android:** only ask battery optimization once [#1252](https://github.com/krtirtho/spotube/issues/1252) ([e516afb](https://github.com/krtirtho/spotube/commit/e516afb185f616471822ea745495a3d1d1281bd3))
|
||||||
|
* **android:** pressing back button in any other tab other than home exits the app ([c3289a0](https://github.com/krtirtho/spotube/commit/c3289a0ba4e7de094a15246677ffcb940504ebde))
|
||||||
|
* **android:** system back button in player page exits the app ([3294f65](https://github.com/krtirtho/spotube/commit/3294f657fe8a03b18d9be8974968b6508465963d))
|
||||||
|
* cleanTitle removing feat and ft from words instead of whole words ([8612345](https://github.com/krtirtho/spotube/commit/86123456f2ff577921cf62cffca180427dfe1dd5))
|
||||||
|
* friends list not scrollable with mouse drag ([ab08c82](https://github.com/krtirtho/spotube/commit/ab08c82c8dd501263049f3adcbd48907ba13e3a9))
|
||||||
|
* no draggable scrollbar in playlist/album page [#1158](https://github.com/krtirtho/spotube/issues/1158) ([6f71e52](https://github.com/krtirtho/spotube/commit/6f71e52ea8a5712d2c3527f2a524af9fbb718bef))
|
||||||
|
* non-banger songs breaking the queue if sources not found ([90f7c53](https://github.com/krtirtho/spotube/commit/90f7c531cdc8640afdbabf5a0592159715ea1e6f))
|
||||||
|
* track loading when not found in Youtube ([e964f61](https://github.com/krtirtho/spotube/commit/e964f61d38cb303e3d3fd60c866414f57207181c))
|
||||||
|
* **translations:** Update app_nl.arb ([#1168](https://github.com/krtirtho/spotube/issues/1168)) ([8167963](https://github.com/krtirtho/spotube/commit/8167963212eeb5dfb0b4fb2eadf81d466659a9f1))
|
||||||
|
|
||||||
## [3.4.1](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.4.1) (2024-01-27)
|
## [3.4.1](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.4.1) (2024-01-27)
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,14 +25,14 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
|||||||
- [Before Submitting an Enhancement](#before-submitting-an-enhancement)
|
- [Before Submitting an Enhancement](#before-submitting-an-enhancement)
|
||||||
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
|
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
|
||||||
- [Your First Code Contribution](#your-first-code-contribution)
|
- [Your First Code Contribution](#your-first-code-contribution)
|
||||||
- [Submit translations](#submit-translations)
|
- [Submit Translations](#submit-translations)
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
This project and everyone participating in it is governed by the
|
This project and everyone participating in it is governed by the
|
||||||
[Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md).
|
[Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md).
|
||||||
By participating, you are expected to uphold this code. Please report unacceptable behavior
|
By participating, you are expected to uphold this code. Please report unacceptable behavior
|
||||||
to <>.
|
to krtirtho@gmail.com.
|
||||||
|
|
||||||
## I Have a Question
|
## I Have a Question
|
||||||
|
|
||||||
@ -123,16 +123,16 @@ Do the following:
|
|||||||
- Install Development dependencies in linux
|
- Install Development dependencies in linux
|
||||||
- Debian (>=12/Bookworm)/Ubuntu
|
- Debian (>=12/Bookworm)/Ubuntu
|
||||||
```bash
|
```bash
|
||||||
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev
|
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev
|
||||||
```
|
```
|
||||||
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
|
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
|
||||||
- Arch/Manjaro
|
- Arch/Manjaro
|
||||||
```bash
|
```bash
|
||||||
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify
|
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan webkit2gtk-4.1 libsoup3
|
||||||
```
|
```
|
||||||
- Fedora
|
- Fedora
|
||||||
```bash
|
```bash
|
||||||
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel
|
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns webkit2gtk4.1 webkit2gtk4.1-devel libsoup3 libsoup3-devel
|
||||||
```
|
```
|
||||||
- Clone the Repo
|
- Clone the Repo
|
||||||
- Create a `.env` in root of the project following the `.env.example` template
|
- Create a `.env` in root of the project following the `.env.example` template
|
||||||
|
103
README.md
103
README.md
@ -7,7 +7,7 @@ eliminating the need for Spotify Premium
|
|||||||
|
|
||||||
Btw it's not just another Electron app 😉
|
Btw it's not just another Electron app 😉
|
||||||
|
|
||||||
<a href="https://spotube.netlify.app"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
|
<a href="https://spotube.krtirtho.dev"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
|
||||||
<a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
|
<a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
|
||||||
|
|
||||||
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
|
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
|
||||||
@ -97,12 +97,7 @@ This handy table lists all the methods you can use to install Spotube:
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>AppImage</td>
|
<td>AppImage</td>
|
||||||
<td>
|
<td>AppImage's lacking stability led to it's temporal removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
||||||
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage">
|
|
||||||
<img width="220" alt="Download AppImage" src="https://user-images.githubusercontent.com/61944859/169455015-13385466-8901-48fe-ba90-b62d58b0be64.png">
|
|
||||||
</a>
|
|
||||||
<p><b>Note:</b> <a href="https://github.com/TheAssassin/AppImageLauncher">AppimageLauncher</a> is required!</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Debian/Ubuntu</td>
|
<td>Debian/Ubuntu</td>
|
||||||
@ -204,6 +199,8 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
||||||
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
||||||
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
||||||
|
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
|
||||||
|
1. [LRCLib](https://lrclib.net/) - A public synced lyric API
|
||||||
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
|
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
|
||||||
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
|
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
|
||||||
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
|
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
|
||||||
@ -213,106 +210,116 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
||||||
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
|
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
|
||||||
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
|
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
|
||||||
|
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
||||||
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
|
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
|
||||||
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
|
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
|
||||||
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
|
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
|
||||||
|
1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
|
||||||
|
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
||||||
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
|
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
|
||||||
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
|
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
|
||||||
1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
|
1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
|
||||||
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
||||||
1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons
|
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
|
||||||
1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration.
|
1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration.
|
||||||
|
1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
|
||||||
|
1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
|
||||||
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop.
|
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop.
|
||||||
1. [device_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
|
1. [device_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
|
||||||
1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device.
|
|
||||||
1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
|
1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
|
||||||
1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
|
1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
|
||||||
|
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
||||||
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
||||||
|
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
||||||
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
|
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
|
||||||
|
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
|
||||||
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
||||||
1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter
|
|
||||||
1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
|
|
||||||
1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query
|
|
||||||
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
||||||
|
1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
|
||||||
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
||||||
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
||||||
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
|
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
|
||||||
|
1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
|
||||||
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
||||||
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
||||||
|
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
|
||||||
|
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
|
||||||
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
||||||
1. [flutter_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
|
1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
||||||
|
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
||||||
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
||||||
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
||||||
|
1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
|
||||||
|
1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
|
||||||
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
||||||
|
1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
|
||||||
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
|
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
|
||||||
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
||||||
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
|
||||||
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
||||||
1. [hooks_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
|
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
|
||||||
|
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
||||||
|
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
|
1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
|
||||||
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
||||||
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
|
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
|
||||||
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
|
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
|
||||||
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
|
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
|
||||||
1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
|
1. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
|
||||||
|
1. [io](https://pub.dev/packages/io) - Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values.
|
||||||
|
1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
|
||||||
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
||||||
|
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
||||||
|
1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
|
||||||
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
||||||
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
|
||||||
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
|
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
|
||||||
|
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
||||||
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
|
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
|
||||||
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
|
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
|
||||||
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
|
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
|
||||||
1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
|
1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
|
||||||
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
|
|
||||||
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
||||||
|
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
|
||||||
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
||||||
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
||||||
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
||||||
|
1. [process_run](https://github.com/tekartik/process_run.dart/blob/master/packages/process_run) - Process run helpers for Linux/Win/Mac and which like feature for finding executables.
|
||||||
|
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
||||||
|
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
|
||||||
|
1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
|
||||||
|
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
||||||
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
|
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
|
||||||
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
|
|
||||||
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
|
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
|
||||||
|
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
|
||||||
|
1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
|
||||||
|
1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
|
||||||
|
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
|
||||||
|
1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
|
||||||
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
|
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
|
||||||
|
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
||||||
|
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
||||||
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
||||||
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
||||||
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
|
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
|
||||||
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
|
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
|
||||||
|
1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime.
|
||||||
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
|
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
|
||||||
|
1. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray.
|
||||||
1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
|
1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
|
||||||
1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart
|
1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart
|
||||||
1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/
|
1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/
|
||||||
1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
|
|
||||||
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
|
|
||||||
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
|
||||||
1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
|
|
||||||
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
|
||||||
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
|
|
||||||
1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
|
|
||||||
1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more.
|
1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more.
|
||||||
1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
|
1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
|
||||||
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel.
|
||||||
1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
|
|
||||||
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
|
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
|
||||||
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
||||||
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
|
||||||
1. [win32_registry](https://win32.pub) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
|
||||||
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||||
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
|
||||||
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
|
||||||
1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps.
|
|
||||||
1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
|
|
||||||
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
|
|
||||||
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
|
|
||||||
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
|
|
||||||
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
|
||||||
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
|
||||||
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
|
|
||||||
1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
|
|
||||||
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
|
||||||
1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
|
|
||||||
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
|
||||||
1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
||||||
|
@ -25,12 +25,17 @@ linter:
|
|||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
file_names: false
|
file_names: false
|
||||||
|
avoid_renaming_method_parameters: false
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
analyzer:
|
analyzer:
|
||||||
enable-experiment:
|
|
||||||
- records
|
|
||||||
- patterns
|
|
||||||
errors:
|
errors:
|
||||||
invalid_annotation_target: ignore
|
invalid_annotation_target: ignore
|
||||||
|
plugins:
|
||||||
|
- custom_lint
|
||||||
|
exclude:
|
||||||
|
- "**.freezed.dart"
|
||||||
|
- "**.g.dart"
|
||||||
|
- "**.gr.dart"
|
||||||
|
- "**/generated_plugin_registrant.dart"
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
@ -34,7 +31,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
compileSdkVersion 34
|
compileSdkVersion 34
|
||||||
|
|
||||||
ndkVersion "21.4.7075529"
|
ndkVersion "25.1.8937393"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -50,10 +47,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "oss.krtirtho.spotube"
|
applicationId "oss.krtirtho.spotube"
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@ -71,6 +67,9 @@ android {
|
|||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "default"
|
flavorDimensions "default"
|
||||||
@ -81,16 +80,19 @@ android {
|
|||||||
resValue "string", "app_name_en", "Spotube Nightly"
|
resValue "string", "app_name_en", "Spotube Nightly"
|
||||||
applicationIdSuffix ".nightly"
|
applicationIdSuffix ".nightly"
|
||||||
versionNameSuffix "-nightly"
|
versionNameSuffix "-nightly"
|
||||||
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
dev {
|
dev {
|
||||||
dimension "default"
|
dimension "default"
|
||||||
resValue "string", "app_name_en", "Spotube Dev"
|
resValue "string", "app_name_en", "Spotube Dev"
|
||||||
applicationIdSuffix ".dev"
|
applicationIdSuffix ".dev"
|
||||||
versionNameSuffix "-dev"
|
versionNameSuffix "-dev"
|
||||||
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
stable {
|
stable {
|
||||||
dimension "default"
|
dimension "default"
|
||||||
resValue "string", "app_name_en", "Spotube"
|
resValue "string", "app_name_en", "Spotube"
|
||||||
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,15 +103,6 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
constraints {
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
|
|
||||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
|
||||||
}
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
|
|
||||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
|
|
||||||
// other deps so just ignore
|
// other deps so just ignore
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
@ -24,6 +25,11 @@
|
|||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
>
|
||||||
|
<!-- Enable Impeller -->
|
||||||
|
<!-- <meta-data
|
||||||
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
|
android:value="true" /> -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
@ -1,16 +1,3 @@
|
|||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.8.22'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx4608m
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
id "com.android.application" version "7.2.1" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
BIN
assets/spotube-logo.bmp
Normal file
BIN
assets/spotube-logo.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
@ -1,17 +1,18 @@
|
|||||||
pkgbase = spotube-bin
|
pkgbase = spotube-bin
|
||||||
pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
|
pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
|
||||||
pkgver = 2.3.0
|
pkgver = 3.7.1
|
||||||
pkgrel = 1
|
pkgrel = 2
|
||||||
url = https://github.com/KRTirtho/spotube/
|
url = https://github.com/KRTirtho/spotube/
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = BSD-4-Clause
|
license = BSD-4-Clause
|
||||||
depends = mpv
|
depends = mpv
|
||||||
depends = libappindicator-gtk3
|
depends = libappindicator-gtk3
|
||||||
depends = libsecret
|
depends = libsecret
|
||||||
depends = jsoncpp
|
depends = jsoncpp
|
||||||
depends = libnotify
|
depends = libnotify
|
||||||
depends = xdg-user-dirs
|
depends = xdg-user-dirs
|
||||||
source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz
|
depends = webkit2gtk-4.1
|
||||||
md5sums = 8cd6a7385c5c75d203dccd762f1d63ec
|
source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
|
||||||
|
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
||||||
|
|
||||||
pkgname = spotube-bin
|
pkgname = spotube-bin
|
||||||
|
@ -8,7 +8,7 @@ arch=(x86_64)
|
|||||||
url="https://github.com/KRTirtho/spotube/"
|
url="https://github.com/KRTirtho/spotube/"
|
||||||
license=('BSD-4-Clause')
|
license=('BSD-4-Clause')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs')
|
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||||
makedepends=()
|
makedepends=()
|
||||||
checkdepends=()
|
checkdepends=()
|
||||||
optdepends=()
|
optdepends=()
|
||||||
|
@ -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,49 +0,0 @@
|
|||||||
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.",
|
|
||||||
);
|
|
||||||
// ignore: avoid_print
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
12
build.yaml
12
build.yaml
@ -3,3 +3,15 @@ targets:
|
|||||||
sources:
|
sources:
|
||||||
exclude:
|
exclude:
|
||||||
- bin/*.dart
|
- bin/*.dart
|
||||||
|
builders:
|
||||||
|
json_serializable:
|
||||||
|
options:
|
||||||
|
any_map: true
|
||||||
|
explicit_to_json: true
|
||||||
|
drift_dev:
|
||||||
|
options:
|
||||||
|
sql:
|
||||||
|
dialect: sqlite
|
||||||
|
options:
|
||||||
|
modules:
|
||||||
|
- json1
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
92
cli/commands/build/android.dart
Normal file
92
cli/commands/build/android.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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"
|
||||||
|
"\nHIDE_DONATIONS=1",
|
||||||
|
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 clean
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
119
cli/commands/build/windows.dart
Normal file
119
cli/commands/build/windows.dart
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
|
class WindowsBuildCommand extends Command with BuildCommandCommonSteps {
|
||||||
|
@override
|
||||||
|
String get description => "Build Windows exe";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => "windows";
|
||||||
|
|
||||||
|
Future<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();
|
||||||
|
|
||||||
|
final runnerRCFile = File(
|
||||||
|
join(cwd.path, "windows", "runner", "Runner.rc"),
|
||||||
|
);
|
||||||
|
|
||||||
|
runnerRCFile.writeAsStringSync(
|
||||||
|
runnerRCFile
|
||||||
|
.readAsStringSync()
|
||||||
|
.replaceAll("%{{SPOTUBE_VERSION}}%", versionWithoutBuildNumber)
|
||||||
|
.replaceAll(
|
||||||
|
"%{{SPOTUBE_VERSION_AS_NUMBER}}%",
|
||||||
|
[
|
||||||
|
pubspec.version!.major,
|
||||||
|
pubspec.version!.minor,
|
||||||
|
pubspec.version!.patch,
|
||||||
|
0
|
||||||
|
].join(","),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await shell.run(
|
||||||
|
"flutter_distributor package --platform=windows --targets=exe --skip-clean",
|
||||||
|
);
|
||||||
|
|
||||||
|
final ogExe = File(
|
||||||
|
join(
|
||||||
|
cwd.path,
|
||||||
|
"dist",
|
||||||
|
pubspec.version.toString(),
|
||||||
|
"spotube-${pubspec.version}-windows-setup.exe",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final exePath = join(cwd.path, "dist", "Spotube-windows-x86_64-setup.exe");
|
||||||
|
|
||||||
|
await ogExe.copy(exePath);
|
||||||
|
await ogExe.delete();
|
||||||
|
|
||||||
|
stdout.writeln("✅ Windows exe built at $exePath");
|
||||||
|
|
||||||
|
final exeFile = File(exePath);
|
||||||
|
|
||||||
|
final hash = sha256.convert(await exeFile.readAsBytes()).toString();
|
||||||
|
|
||||||
|
final chocoVerificationFile = File(chocoFiles.first);
|
||||||
|
|
||||||
|
chocoVerificationFile.writeAsStringSync(
|
||||||
|
chocoVerificationFile.readAsStringSync().replaceAll(
|
||||||
|
RegExp(r"\%\{\{WIN_SHA256\}\}\%"),
|
||||||
|
hash,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await exeFile.copy(
|
||||||
|
join(cwd.path, "choco-struct", "tools", basename(exeFile.path)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await shell.run(
|
||||||
|
"choco pack ${chocoFiles[1]} --outputdirectory ${join(cwd.path, "dist")}",
|
||||||
|
);
|
||||||
|
|
||||||
|
final chocoNupkg = File(
|
||||||
|
join(cwd.path, "dist", "spotube.$versionWithoutBuildNumber.nupkg"),
|
||||||
|
);
|
||||||
|
|
||||||
|
final distNupkgPath = join(
|
||||||
|
cwd.path,
|
||||||
|
"dist",
|
||||||
|
"Spotube-windows-x86_64.nupkg",
|
||||||
|
);
|
||||||
|
|
||||||
|
await chocoNupkg.copy(distNupkgPath);
|
||||||
|
await chocoNupkg.delete();
|
||||||
|
|
||||||
|
stdout.writeln("✅ Windows nupkg built at $distNupkgPath");
|
||||||
|
}
|
||||||
|
}
|
121
cli/commands/credits.dart
Normal file
121
cli/commands/credits.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:pub_api_client/pub_api_client.dart';
|
||||||
|
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||||
|
|
||||||
|
class CreditsCommand extends Command {
|
||||||
|
final dio = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
responseType: ResponseType.plain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => "Generate credits for used Library's authors";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => "credits";
|
||||||
|
|
||||||
|
@override
|
||||||
|
run() async {
|
||||||
|
final client = PubClient();
|
||||||
|
final cwd = Directory.current;
|
||||||
|
|
||||||
|
final pubspec = Pubspec.parse(
|
||||||
|
File(join(cwd.path, 'pubspec.yaml')).readAsStringSync(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final allDeps = [
|
||||||
|
...pubspec.dependencies.entries,
|
||||||
|
...pubspec.devDependencies.entries,
|
||||||
|
];
|
||||||
|
|
||||||
|
final dependencies = allDeps
|
||||||
|
.where((d) => d.value is HostedDependency)
|
||||||
|
.map((d) => d.key)
|
||||||
|
.toSet();
|
||||||
|
final packageInfo = await Future.wait(dependencies.map(client.packageInfo));
|
||||||
|
|
||||||
|
final gitDepsList = List.castFrom<MapEntry<String, Dependency>,
|
||||||
|
MapEntry<String, GitDependency>>(
|
||||||
|
allDeps
|
||||||
|
.where((d) => d.value is GitDependency)
|
||||||
|
.map((d) => MapEntry(d.key, d.value as GitDependency))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final gitDeps = gitDepsList.map(
|
||||||
|
(d) {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
d.value.url.toString().replaceAll('.git', ''),
|
||||||
|
);
|
||||||
|
return MapEntry(
|
||||||
|
d.key,
|
||||||
|
uri.replace(
|
||||||
|
pathSegments: [
|
||||||
|
...uri.pathSegments,
|
||||||
|
'raw',
|
||||||
|
d.value.ref ?? 'main',
|
||||||
|
d.value.path ?? '',
|
||||||
|
'pubspec.yaml',
|
||||||
|
],
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
final gitPubspecs = await Future.wait(
|
||||||
|
gitDeps.map(
|
||||||
|
(d) {
|
||||||
|
Pubspec parser(Response res) {
|
||||||
|
try {
|
||||||
|
return Pubspec.parse(res.data);
|
||||||
|
} catch (e) {
|
||||||
|
final document = parse(res.data);
|
||||||
|
final pre = document.querySelector('pre');
|
||||||
|
if (pre == null) {
|
||||||
|
stdout.writeln(d.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
return Pubspec.parse(pre.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dio.get(d.value).then(parser).catchError(
|
||||||
|
(_) => dio
|
||||||
|
.get(d.value.replaceFirst('/main', '/master'))
|
||||||
|
.then(parser),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
stdout.writeln(
|
||||||
|
packageInfo
|
||||||
|
.map(
|
||||||
|
(package) =>
|
||||||
|
'1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}',
|
||||||
|
)
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
stdout.writeln(
|
||||||
|
gitPubspecs.map(
|
||||||
|
(package) {
|
||||||
|
final packageUrl = package.homepage ??
|
||||||
|
gitDepsList
|
||||||
|
.firstWhereOrNull((dep) => dep.key == package.name)
|
||||||
|
?.value
|
||||||
|
.url
|
||||||
|
.toString();
|
||||||
|
return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}';
|
||||||
|
},
|
||||||
|
).join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
79
cli/commands/install-dependencies.dart
Normal file
79
cli/commands/install-dependencies.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:process_run/shell_run.dart';
|
||||||
|
|
||||||
|
class InstallDependenciesCommand extends Command {
|
||||||
|
@override
|
||||||
|
String get description => "Install platform dependencies";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => "install-dependencies";
|
||||||
|
|
||||||
|
InstallDependenciesCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
"platform",
|
||||||
|
abbr: "p",
|
||||||
|
allowed: [
|
||||||
|
"windows",
|
||||||
|
"linux",
|
||||||
|
"linux_arm",
|
||||||
|
"macos",
|
||||||
|
"ios",
|
||||||
|
"android",
|
||||||
|
],
|
||||||
|
mandatory: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr? run() async {
|
||||||
|
final shell = Shell();
|
||||||
|
|
||||||
|
switch (argResults!.option("platform")) {
|
||||||
|
case "windows":
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libsoup-3.0-0 libsoup-3.0-dev
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "linux_arm":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y pkg-config make python3-pip python3-setuptools
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "macos":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
brew install python-setuptools
|
||||||
|
npm install -g appdmg
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ios":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
rustup target add aarch64-apple-ios
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "android":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
|
||||||
|
""",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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
devtools_options.yaml
Normal file
1
devtools_options.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
extensions:
|
@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
# platform :ios, '12.0'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
130
ios/Podfile.lock
130
ios/Podfile.lock
@ -1,10 +1,13 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- app_links (0.0.1):
|
- app_links (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- audio_service (0.0.1):
|
- audio_service (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- audio_session (0.0.1):
|
- audio_session (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- bonsoir_darwin (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.4):
|
- DKImagePickerController/Core (4.3.4):
|
||||||
@ -44,29 +47,23 @@ PODS:
|
|||||||
- file_selector_ios (0.0.1):
|
- file_selector_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_inappwebview (0.0.1):
|
- flutter_broadcasts (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_inappwebview/Core (= 0.0.1)
|
- flutter_discord_rpc (0.0.1):
|
||||||
- OrderedSet (~> 5.0)
|
|
||||||
- flutter_inappwebview/Core (0.0.1):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- OrderedSet (~> 5.0)
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
- flutter_keyboard_visibility (0.0.1):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_mailer (0.0.1):
|
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
|
- flutter_inappwebview_ios/Core (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_sharing_intent (0.0.1):
|
- flutter_sharing_intent (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
|
||||||
- Flutter
|
|
||||||
- Toast
|
|
||||||
- FMDB (2.7.5):
|
|
||||||
- FMDB/standard (= 2.7.5)
|
|
||||||
- FMDB/standard (2.7.5)
|
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
@ -75,14 +72,15 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_native_event_loop (1.0.0):
|
- media_kit_native_event_loop (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- metadata_god (0.0.1)
|
- metadata_god (0.0.1):
|
||||||
- OrderedSet (5.0.0)
|
- Flutter
|
||||||
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.1.1):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.18.8):
|
- SDWebImage (5.18.8):
|
||||||
- SDWebImage/Core (= 5.18.8)
|
- SDWebImage/Core (= 5.18.8)
|
||||||
@ -92,9 +90,23 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FlutterMacOS
|
||||||
|
- "sqlite3 (3.46.0+1)":
|
||||||
|
- "sqlite3/common (= 3.46.0+1)"
|
||||||
|
- "sqlite3/common (3.46.0+1)"
|
||||||
|
- "sqlite3/fts5 (3.46.0+1)":
|
||||||
|
- sqlite3/common
|
||||||
|
- "sqlite3/perf-threadsafe (3.46.0+1)":
|
||||||
|
- sqlite3/common
|
||||||
|
- "sqlite3/rtree (3.46.0+1)":
|
||||||
|
- sqlite3/common
|
||||||
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- sqlite3 (~> 3.46.0)
|
||||||
|
- sqlite3/fts5
|
||||||
|
- sqlite3/perf-threadsafe
|
||||||
|
- sqlite3/rtree
|
||||||
- SwiftyGif (5.4.4)
|
- SwiftyGif (5.4.4)
|
||||||
- Toast (4.0.0)
|
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
@ -102,17 +114,17 @@ DEPENDENCIES:
|
|||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||||
|
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
|
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`)
|
||||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
|
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`)
|
- media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`)
|
||||||
@ -122,18 +134,18 @@ DEPENDENCIES:
|
|||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
- FMDB
|
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- Toast
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
app_links:
|
||||||
@ -142,6 +154,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/audio_service/ios"
|
:path: ".symlinks/plugins/audio_service/ios"
|
||||||
audio_session:
|
audio_session:
|
||||||
:path: ".symlinks/plugins/audio_session/ios"
|
:path: ".symlinks/plugins/audio_session/ios"
|
||||||
|
bonsoir_darwin:
|
||||||
|
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
@ -150,20 +164,18 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/file_selector_ios/ios"
|
:path: ".symlinks/plugins/file_selector_ios/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_inappwebview:
|
flutter_broadcasts:
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
:path: ".symlinks/plugins/flutter_broadcasts/ios"
|
||||||
flutter_keyboard_visibility:
|
flutter_discord_rpc:
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
:path: ".symlinks/plugins/flutter_discord_rpc/ios"
|
||||||
flutter_mailer:
|
flutter_inappwebview_ios:
|
||||||
:path: ".symlinks/plugins/flutter_mailer/ios"
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_sharing_intent:
|
flutter_sharing_intent:
|
||||||
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
||||||
fluttertoast:
|
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
integration_test:
|
integration_test:
|
||||||
@ -183,44 +195,46 @@ EXTERNAL SOURCES:
|
|||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/darwin"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
|
||||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||||
|
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5
|
||||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||||
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
||||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
|
||||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||||
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
|
||||||
metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7
|
metadata_god: 4bbd8523cdb5d42c5e59d2fabad01ff8f4bc53f9
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
|
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
|
||||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
|
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
|
||||||
|
sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31
|
||||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
|
||||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 5129d2e80ab0dfc533f262cedf032011b1dfe4fd
|
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
@ -324,6 +324,7 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */,
|
6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */,
|
||||||
|
46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -346,6 +347,7 @@
|
|||||||
B536BD992B405DB1009B3CE4 /* Embed Frameworks */,
|
B536BD992B405DB1009B3CE4 /* Embed Frameworks */,
|
||||||
B536BD9A2B405DB1009B3CE4 /* Thin Binary */,
|
B536BD9A2B405DB1009B3CE4 /* Thin Binary */,
|
||||||
A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */,
|
A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */,
|
||||||
|
2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -368,6 +370,7 @@
|
|||||||
B536BDB62B405FDE009B3CE4 /* Embed Frameworks */,
|
B536BDB62B405FDE009B3CE4 /* Embed Frameworks */,
|
||||||
B536BDB72B405FDE009B3CE4 /* Thin Binary */,
|
B536BDB72B405FDE009B3CE4 /* Thin Binary */,
|
||||||
244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */,
|
244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */,
|
||||||
|
4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -390,6 +393,7 @@
|
|||||||
B536BDD82B4060B3009B3CE4 /* Embed Frameworks */,
|
B536BDD82B4060B3009B3CE4 /* Embed Frameworks */,
|
||||||
B536BDD92B4060B3009B3CE4 /* Thin Binary */,
|
B536BDD92B4060B3009B3CE4 /* Thin Binary */,
|
||||||
D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */,
|
D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */,
|
||||||
|
5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -523,6 +527,23 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
2DEF3CF18D30E819C0FF4BCE /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@ -539,6 +560,57 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
|
46F6EB27C31C41D86428A28B /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
4DD66E9E53D92195290872BE /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
5C9D945D6569D9C3AC420285 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = {
|
5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -66,5 +66,11 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true />
|
<true />
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>To allow other devices on the network control playback of Spotube securely.</string>
|
||||||
|
<key>NSBonjourServices</key>
|
||||||
|
<array>
|
||||||
|
<string>_spotube._tcp</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -59,6 +59,8 @@ class Assets {
|
|||||||
AssetGenImage('assets/spotube-hero-banner.png');
|
AssetGenImage('assets/spotube-hero-banner.png');
|
||||||
static const AssetGenImage spotubeLogoForeground =
|
static const AssetGenImage spotubeLogoForeground =
|
||||||
AssetGenImage('assets/spotube-logo-foreground.jpg');
|
AssetGenImage('assets/spotube-logo-foreground.jpg');
|
||||||
|
static const AssetGenImage spotubeLogoBmp =
|
||||||
|
AssetGenImage('assets/spotube-logo.bmp');
|
||||||
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
|
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
|
||||||
static const AssetGenImage spotubeLogoPng =
|
static const AssetGenImage spotubeLogoPng =
|
||||||
AssetGenImage('assets/spotube-logo.png');
|
AssetGenImage('assets/spotube-logo.png');
|
||||||
@ -88,7 +90,7 @@ class Assets {
|
|||||||
AssetGenImage('assets/user-placeholder.png');
|
AssetGenImage('assets/user-placeholder.png');
|
||||||
|
|
||||||
/// List of all assets
|
/// List of all assets
|
||||||
List<dynamic> get values => [
|
static List<dynamic> get values => [
|
||||||
albumPlaceholder,
|
albumPlaceholder,
|
||||||
bengaliPatternsBg,
|
bengaliPatternsBg,
|
||||||
branding,
|
branding,
|
||||||
@ -98,6 +100,7 @@ class Assets {
|
|||||||
placeholder,
|
placeholder,
|
||||||
spotubeHeroBanner,
|
spotubeHeroBanner,
|
||||||
spotubeLogoForeground,
|
spotubeLogoForeground,
|
||||||
|
spotubeLogoBmp,
|
||||||
spotubeLogoIco,
|
spotubeLogoIco,
|
||||||
spotubeLogoPng,
|
spotubeLogoPng,
|
||||||
spotubeLogoSvg,
|
spotubeLogoSvg,
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
abstract class LocalStorageKeys {
|
|
||||||
static String saveTrackLyrics = 'save_track_lyrics';
|
|
||||||
static String recommendationMarket = 'recommendation_market';
|
|
||||||
static String ytSearchFormate = 'youtube_search_format';
|
|
||||||
|
|
||||||
static String clientId = 'clientId';
|
|
||||||
static String clientSecret = 'clientSecret';
|
|
||||||
static String accessToken = 'accessToken';
|
|
||||||
static String refreshToken = 'refreshToken';
|
|
||||||
static String expiration = "expiration";
|
|
||||||
static String geniusAccessToken = "genius_access_token";
|
|
||||||
|
|
||||||
static String themeMode = "theme_mode";
|
|
||||||
static String nextTrackHotKey = "next_track_hot_key";
|
|
||||||
static String prevTrackHotKey = "prev_track_hot_key";
|
|
||||||
static String playPauseHotKey = "play_pause_hot_key";
|
|
||||||
|
|
||||||
static String volume = "volume";
|
|
||||||
|
|
||||||
static String windowSizeInfo = "window_size_info";
|
|
||||||
}
|
|
@ -1,8 +1,13 @@
|
|||||||
import 'package:envied/envied.dart';
|
import 'package:envied/envied.dart';
|
||||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
part 'env.g.dart';
|
part 'env.g.dart';
|
||||||
|
|
||||||
|
enum ReleaseChannel {
|
||||||
|
nightly,
|
||||||
|
stable,
|
||||||
|
}
|
||||||
|
|
||||||
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
|
||||||
abstract class Env {
|
abstract class Env {
|
||||||
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
@EnviedField(varName: 'SPOTIFY_SECRETS')
|
||||||
@ -14,6 +19,11 @@ abstract class Env {
|
|||||||
@EnviedField(varName: 'LASTFM_API_SECRET')
|
@EnviedField(varName: 'LASTFM_API_SECRET')
|
||||||
static final String lastFmApiSecret = _Env.lastFmApiSecret;
|
static final String lastFmApiSecret = _Env.lastFmApiSecret;
|
||||||
|
|
||||||
|
@EnviedField(varName: 'HIDE_DONATIONS', defaultValue: "0")
|
||||||
|
static final int _hideDonations = _Env._hideDonations;
|
||||||
|
|
||||||
|
static bool get hideDonations => _hideDonations == 1;
|
||||||
|
|
||||||
static final spotifySecrets = rawSpotifySecrets.split(',').map((e) {
|
static final spotifySecrets = rawSpotifySecrets.split(',').map((e) {
|
||||||
final secrets = e.trim().split(":").map((e) => e.trim());
|
final secrets = e.trim().split(":").map((e) => e.trim());
|
||||||
return {
|
return {
|
||||||
@ -25,8 +35,15 @@ abstract class Env {
|
|||||||
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
|
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
|
||||||
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
|
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
|
||||||
|
|
||||||
|
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
||||||
|
static final String _releaseChannel = _Env._releaseChannel;
|
||||||
|
|
||||||
|
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
||||||
|
? ReleaseChannel.stable
|
||||||
|
: ReleaseChannel.nightly;
|
||||||
|
|
||||||
static bool get enableUpdateChecker =>
|
static bool get enableUpdateChecker =>
|
||||||
DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1";
|
kIsFlatpak || _enableUpdateChecker == "1";
|
||||||
|
|
||||||
static String discordAppId = "1176718791388975124";
|
static String discordAppId = "1176718791388975124";
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
import 'package:spotube/provider/history/summary.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final Image image = Image()
|
static final Image image = Image()
|
||||||
..height = 1
|
..height = 1
|
||||||
..width = 1
|
..width = 1
|
||||||
..url = "url";
|
..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
|
||||||
|
|
||||||
static final Followers followers = Followers()
|
static final Followers followers = Followers()
|
||||||
..href = "text"
|
..href = "text"
|
||||||
@ -196,4 +198,62 @@ abstract class FakeData {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final feedSection = SpotifyHomeFeedSection(
|
||||||
|
typename: "HomeGenericSectionData",
|
||||||
|
uri: "spotify:section:lol",
|
||||||
|
title: "Dummy",
|
||||||
|
items: [
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
SpotifyHomeFeedSectionItem(
|
||||||
|
typename: "PlaylistResponseWrapper",
|
||||||
|
playlist: SpotifySectionPlaylist(
|
||||||
|
name: "Playlist $i",
|
||||||
|
description: "Really super important description $i",
|
||||||
|
format: "daily-mix",
|
||||||
|
images: [
|
||||||
|
const SpotifySectionItemImage(
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
owner: "Spotify",
|
||||||
|
uri: "spotify:playlist:id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const historySummary = PlaybackHistorySummary(
|
||||||
|
albums: 1,
|
||||||
|
artists: 1,
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
playlists: 1,
|
||||||
|
tracks: 1,
|
||||||
|
fees: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedPlaylist = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: playlist.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedAlbum = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: album.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedItems = List.generate(
|
||||||
|
10,
|
||||||
|
(index) => index % 2 == 0
|
||||||
|
? historyRecentlyPlayedPlaylist
|
||||||
|
: historyRecentlyPlayedAlbum,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
8
lib/collections/formatters.dart
Normal file
8
lib/collections/formatters.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
final compactNumberFormatter = NumberFormat.compact();
|
||||||
|
final usdFormatter = NumberFormat.compactCurrency(
|
||||||
|
locale: 'en-US',
|
||||||
|
symbol: r"$",
|
||||||
|
decimalDigits: 2,
|
||||||
|
);
|
@ -1,9 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:win32_registry/win32_registry.dart';
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
Future<void> registerWindowsScheme(String scheme) async {
|
Future<void> registerWindowsScheme(String scheme) async {
|
||||||
if (!DesktopTools.platform.isWindows) return;
|
if (!kIsWindows) return;
|
||||||
String appPath = Platform.resolvedExecutable;
|
String appPath = Platform.resolvedExecutable;
|
||||||
|
|
||||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||||
|
@ -5,9 +5,12 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/components/player/player_controls.dart';
|
import 'package:spotube/modules/player/player_controls.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/pages/library/library.dart';
|
||||||
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
|
import 'package:spotube/pages/search/search.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
@ -17,8 +20,6 @@ class PlayPauseIntent extends Intent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PlayPauseAction extends Action<PlayPauseIntent> {
|
class PlayPauseAction extends Action<PlayPauseIntent> {
|
||||||
final logger = getLogger(PlayPauseAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
invoke(intent) async {
|
invoke(intent) async {
|
||||||
if (PlayerControls.focusNode.canRequestFocus) {
|
if (PlayerControls.focusNode.canRequestFocus) {
|
||||||
@ -67,16 +68,16 @@ class HomeTabAction extends Action<HomeTabIntent> {
|
|||||||
final router = intent.ref.read(routerProvider);
|
final router = intent.ref.read(routerProvider);
|
||||||
switch (intent.tab) {
|
switch (intent.tab) {
|
||||||
case HomeTabs.browse:
|
case HomeTabs.browse:
|
||||||
router.go("/");
|
router.goNamed(HomePage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.search:
|
case HomeTabs.search:
|
||||||
router.go("/search");
|
router.goNamed(SearchPage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.library:
|
case HomeTabs.library:
|
||||||
router.go("/library");
|
router.goNamed(LibraryPage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.lyrics:
|
case HomeTabs.lyrics:
|
||||||
router.go("/lyrics");
|
router.goNamed(LyricsPage.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -92,8 +93,8 @@ class SeekIntent extends Intent {
|
|||||||
class SeekAction extends Action<SeekIntent> {
|
class SeekAction extends Action<SeekIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) async {
|
invoke(intent) async {
|
||||||
final playlist = intent.ref.read(ProxyPlaylistNotifier.provider);
|
final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider);
|
||||||
if (playlist.isFetching) {
|
if (isFetchingActiveTrack) {
|
||||||
DirectionalFocusAction().invoke(
|
DirectionalFocusAction().invoke(
|
||||||
DirectionalFocusIntent(
|
DirectionalFocusIntent(
|
||||||
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
||||||
@ -101,7 +102,7 @@ class SeekAction extends Action<SeekIntent> {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
|
final position = audioPlayer.position.inSeconds;
|
||||||
await audioPlayer.seek(
|
await audioPlayer.seek(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: intent.forward ? position + 5 : position - 5,
|
seconds: intent.forward ? position + 5 : position - 5,
|
||||||
|
@ -81,10 +81,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Bashkir",
|
// name: "Bashkir",
|
||||||
// nativeName: "башҡорт теле",
|
// nativeName: "башҡорт теле",
|
||||||
// ),
|
// ),
|
||||||
// "eu": const ISOLanguageName(
|
"eu": const ISOLanguageName(
|
||||||
// name: "Basque",
|
name: "Basque",
|
||||||
// nativeName: "euskara,",
|
nativeName: "Euskara",
|
||||||
// ),
|
),
|
||||||
// "be": const ISOLanguageName(
|
// "be": const ISOLanguageName(
|
||||||
// name: "Belarusian",
|
// name: "Belarusian",
|
||||||
// nativeName: "Беларуская",
|
// nativeName: "Беларуская",
|
||||||
@ -157,10 +157,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Croatian",
|
// name: "Croatian",
|
||||||
// nativeName: "hrvatski",
|
// nativeName: "hrvatski",
|
||||||
// ),
|
// ),
|
||||||
// "cs": const ISOLanguageName(
|
"cs": const ISOLanguageName(
|
||||||
// name: "Czech",
|
name: "Czech",
|
||||||
// nativeName: "česky, čeština",
|
nativeName: "česky, čeština",
|
||||||
// ),
|
),
|
||||||
// "da": const ISOLanguageName(
|
// "da": const ISOLanguageName(
|
||||||
// name: "Danish",
|
// name: "Danish",
|
||||||
// nativeName: "dansk",
|
// nativeName: "dansk",
|
||||||
@ -197,10 +197,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Fijian",
|
// name: "Fijian",
|
||||||
// nativeName: "vosa Vakaviti",
|
// nativeName: "vosa Vakaviti",
|
||||||
// ),
|
// ),
|
||||||
// "fi": const ISOLanguageName(
|
"fi": const ISOLanguageName(
|
||||||
// name: "Finnish",
|
name: "Finnish",
|
||||||
// nativeName: "suomi",
|
nativeName: "suomi",
|
||||||
// ),
|
),
|
||||||
"fr": const ISOLanguageName(
|
"fr": const ISOLanguageName(
|
||||||
name: "French",
|
name: "French",
|
||||||
nativeName: "français",
|
nativeName: "français",
|
||||||
@ -213,10 +213,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Galician",
|
// name: "Galician",
|
||||||
// nativeName: "Galego",
|
// nativeName: "Galego",
|
||||||
// ),
|
// ),
|
||||||
// "ka": const ISOLanguageName(
|
"ka": const ISOLanguageName(
|
||||||
// name: "Georgian",
|
name: "Georgian",
|
||||||
// nativeName: "ქართული",
|
nativeName: "ქართული",
|
||||||
// ),
|
),
|
||||||
"de": const ISOLanguageName(
|
"de": const ISOLanguageName(
|
||||||
name: "German",
|
name: "German",
|
||||||
nativeName: "Deutsch",
|
nativeName: "Deutsch",
|
||||||
@ -265,10 +265,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Interlingua",
|
// name: "Interlingua",
|
||||||
// nativeName: "Interlingua",
|
// nativeName: "Interlingua",
|
||||||
// ),
|
// ),
|
||||||
// "id": const ISOLanguageName(
|
"id": const ISOLanguageName(
|
||||||
// name: "Indonesian",
|
name: "Indonesian",
|
||||||
// nativeName: "Bahasa Indonesia",
|
nativeName: "Bahasa Indonesia",
|
||||||
// ),
|
),
|
||||||
// "ie": const ISOLanguageName(
|
// "ie": const ISOLanguageName(
|
||||||
// name: "Interlingue",
|
// name: "Interlingue",
|
||||||
// nativeName: "Occidental",
|
// nativeName: "Occidental",
|
||||||
@ -637,10 +637,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Tajik",
|
// name: "Tajik",
|
||||||
// nativeName: "тоҷикӣ, toğikī, تاجیکی",
|
// nativeName: "тоҷикӣ, toğikī, تاجیکی",
|
||||||
// ),
|
// ),
|
||||||
// "th": const ISOLanguageName(
|
"th": const ISOLanguageName(
|
||||||
// name: "Thai",
|
name: "Thai",
|
||||||
// nativeName: "ไทย",
|
nativeName: "ไทย",
|
||||||
// ),
|
),
|
||||||
// "ti": const ISOLanguageName(
|
// "ti": const ISOLanguageName(
|
||||||
// name: "Tigrinya",
|
// name: "Tigrinya",
|
||||||
// nativeName: "ትግርኛ",
|
// nativeName: "ትግርኛ",
|
||||||
|
@ -1,39 +1,48 @@
|
|||||||
import 'package:catcher_2/catcher_2.dart';
|
|
||||||
import 'package:flutter/foundation.dart' hide Category;
|
import 'package:flutter/foundation.dart' hide Category;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Search;
|
import 'package:spotify/spotify.dart' hide Search;
|
||||||
|
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
import 'package:spotube/pages/album/album.dart';
|
||||||
|
import 'package:spotube/pages/connect/connect.dart';
|
||||||
|
import 'package:spotube/pages/connect/control/control.dart';
|
||||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||||
|
import 'package:spotube/pages/home/feed/feed_section.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
||||||
|
import 'package:spotube/pages/library/local_folder.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
||||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
||||||
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
|
import 'package:spotube/pages/profile/profile.dart';
|
||||||
import 'package:spotube/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/pages/settings/blacklist.dart';
|
import 'package:spotube/pages/settings/blacklist.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
import 'package:spotube/pages/settings/about.dart';
|
||||||
import 'package:spotube/pages/settings/logs.dart';
|
import 'package:spotube/pages/settings/logs.dart';
|
||||||
|
import 'package:spotube/pages/stats/albums/albums.dart';
|
||||||
|
import 'package:spotube/pages/stats/artists/artists.dart';
|
||||||
|
import 'package:spotube/pages/stats/fees/fees.dart';
|
||||||
|
import 'package:spotube/pages/stats/minutes/minutes.dart';
|
||||||
|
import 'package:spotube/pages/stats/playlists/playlists.dart';
|
||||||
|
import 'package:spotube/pages/stats/stats.dart';
|
||||||
|
import 'package:spotube/pages/stats/streams/streams.dart';
|
||||||
import 'package:spotube/pages/track/track.dart';
|
import 'package:spotube/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/components/spotube_page_route.dart';
|
||||||
import 'package:spotube/components/shared/spotube_page_route.dart';
|
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
import 'package:spotube/pages/artist/artist.dart';
|
||||||
import 'package:spotube/pages/library/library.dart';
|
import 'package:spotube/pages/library/library.dart';
|
||||||
import 'package:spotube/pages/desktop_login/login_tutorial.dart';
|
|
||||||
import 'package:spotube/pages/desktop_login/desktop_login.dart';
|
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
import 'package:spotube/pages/root/root_app.dart';
|
import 'package:spotube/pages/root/root_app.dart';
|
||||||
import 'package:spotube/pages/settings/settings.dart';
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
||||||
|
|
||||||
final rootNavigatorKey = Catcher2.navigatorKey;
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final routerProvider = Provider((ref) {
|
final routerProvider = Provider((ref) {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
@ -45,13 +54,11 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/",
|
path: "/",
|
||||||
|
name: HomePage.name,
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
final authNotifier =
|
final auth = await ref.read(authenticationProvider.future);
|
||||||
ref.read(AuthenticationNotifier.provider.notifier);
|
|
||||||
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
|
||||||
|
|
||||||
if (json?["cookie"] == null &&
|
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||||
!KVStoreService.doneGettingStarted) {
|
|
||||||
return "/getting-started";
|
return "/getting-started";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,61 +69,88 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genres",
|
path: "genres",
|
||||||
|
name: GenrePage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: GenrePage()),
|
const SpotubePage(child: GenrePage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genre/:categoryId",
|
path: "genre/:categoryId",
|
||||||
|
name: GenrePlaylistsPage.name,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: GenrePlaylistsPage(
|
child: GenrePlaylistsPage(
|
||||||
category: state.extra as Category,
|
category: state.extra as Category,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "feeds/:feedId",
|
||||||
|
name: HomeFeedSectionPage.name,
|
||||||
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
|
child: HomeFeedSectionPage(
|
||||||
|
sectionUri: state.pathParameters["feedId"] as String,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/search",
|
path: "/search",
|
||||||
name: "Search",
|
name: SearchPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: SearchPage()),
|
const SpotubePage(child: SearchPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/library",
|
path: "/library",
|
||||||
name: "Library",
|
name: LibraryPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LibraryPage()),
|
const SpotubePage(child: LibraryPage()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "generate",
|
path: "generate",
|
||||||
|
name: PlaylistGeneratorPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "result",
|
path: "result",
|
||||||
|
name: PlaylistGenerateResultPage.name,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: PlaylistGenerateResultPage(
|
child: PlaylistGenerateResultPage(
|
||||||
state:
|
state: state.extra as GeneratePlaylistProviderInput,
|
||||||
state.extra as PlaylistGenerateResultRouteState,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "local",
|
||||||
|
name: LocalLibraryPage.name,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
assert(state.extra is String);
|
||||||
|
return SpotubePage(
|
||||||
|
child: LocalLibraryPage(state.extra as String,
|
||||||
|
isDownloads:
|
||||||
|
state.uri.queryParameters["downloads"] != null),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lyrics",
|
path: "/lyrics",
|
||||||
name: "Lyrics",
|
name: LyricsPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LyricsPage()),
|
const SpotubePage(child: LyricsPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
name: SettingsPage.name,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: SettingsPage(),
|
child: SettingsPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "blacklist",
|
path: "blacklist",
|
||||||
|
name: BlackListPage.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const BlackListPage(),
|
child: const BlackListPage(),
|
||||||
),
|
),
|
||||||
@ -124,12 +158,14 @@ final routerProvider = Provider((ref) {
|
|||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "logs",
|
path: "logs",
|
||||||
|
name: LogsPage.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const LogsPage(),
|
child: const LogsPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "about",
|
path: "about",
|
||||||
|
name: AboutSpotube.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const AboutSpotube(),
|
child: const AboutSpotube(),
|
||||||
),
|
),
|
||||||
@ -138,6 +174,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/album/:id",
|
path: "/album/:id",
|
||||||
|
name: AlbumPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is AlbumSimple);
|
assert(state.extra is AlbumSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -147,6 +184,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/artist/:id",
|
path: "/artist/:id",
|
||||||
|
name: ArtistPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.pathParameters["id"] != null);
|
assert(state.pathParameters["id"] != null);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -155,6 +193,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/playlist/:id",
|
path: "/playlist/:id",
|
||||||
|
name: PlaylistPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is PlaylistSimple);
|
assert(state.extra is PlaylistSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -166,6 +205,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/track/:id",
|
path: "/track/:id",
|
||||||
|
name: TrackPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final id = state.pathParameters["id"]!;
|
final id = state.pathParameters["id"]!;
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -173,10 +213,86 @@ final routerProvider = Provider((ref) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/connect",
|
||||||
|
name: ConnectPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: ConnectPage(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: "control",
|
||||||
|
name: ConnectControlPage.name,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
return const SpotubePage(
|
||||||
|
child: ConnectControlPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/profile",
|
||||||
|
name: ProfilePage.name,
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: ProfilePage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/stats",
|
||||||
|
name: StatsPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsPage(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: "minutes",
|
||||||
|
name: StatsMinutesPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsMinutesPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "streams",
|
||||||
|
name: StatsStreamsPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsStreamsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "fees",
|
||||||
|
name: StatsStreamFeesPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsStreamFeesPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "artists",
|
||||||
|
name: StatsArtistsPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsArtistsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "albums",
|
||||||
|
name: StatsAlbumsPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsAlbumsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "playlists",
|
||||||
|
name: StatsPlaylistsPage.name,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: StatsPlaylistsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
|
name: MiniLyricsPage.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||||
@ -184,6 +300,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/getting-started",
|
path: "/getting-started",
|
||||||
|
name: GettingStarting.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: GettingStarting(),
|
child: GettingStarting(),
|
||||||
@ -191,20 +308,15 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/login",
|
path: "/login",
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
name: WebViewLogin.name,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/login-tutorial",
|
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: LoginTutorial(),
|
child: WebViewLogin(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lastfm-login",
|
path: "/lastfm-login",
|
||||||
|
name: LastFMLoginPage.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
const SpotubePage(child: LastFMLoginPage()),
|
||||||
|
@ -1,33 +1,82 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:spotube/pages/home/home.dart';
|
||||||
|
import 'package:spotube/pages/library/library.dart';
|
||||||
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
|
import 'package:spotube/pages/search/search.dart';
|
||||||
|
import 'package:spotube/pages/stats/stats.dart';
|
||||||
|
|
||||||
class SideBarTiles {
|
class SideBarTiles {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final String id;
|
final String id;
|
||||||
SideBarTiles({required this.icon, required this.title, required this.id});
|
final String name;
|
||||||
|
|
||||||
|
SideBarTiles({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
||||||
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
|
||||||
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library", icon: SpotubeIcons.library, title: l10n.library),
|
id: "browse",
|
||||||
SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics),
|
name: HomePage.name,
|
||||||
];
|
icon: SpotubeIcons.home,
|
||||||
|
title: l10n.browse,
|
||||||
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
),
|
||||||
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
SideBarTiles(
|
||||||
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
id: "search",
|
||||||
|
name: SearchPage.name,
|
||||||
|
icon: SpotubeIcons.search,
|
||||||
|
title: l10n.search,
|
||||||
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library",
|
id: "library",
|
||||||
|
name: LibraryPage.name,
|
||||||
icon: SpotubeIcons.library,
|
icon: SpotubeIcons.library,
|
||||||
title: l10n.library,
|
title: l10n.library,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "settings",
|
id: "lyrics",
|
||||||
icon: SpotubeIcons.settings,
|
name: LyricsPage.name,
|
||||||
title: l10n.settings,
|
icon: SpotubeIcons.music,
|
||||||
)
|
title: l10n.lyrics,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "stats",
|
||||||
|
name: StatsPage.name,
|
||||||
|
icon: SpotubeIcons.chart,
|
||||||
|
title: l10n.stats,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
||||||
|
SideBarTiles(
|
||||||
|
id: "browse",
|
||||||
|
name: HomePage.name,
|
||||||
|
icon: SpotubeIcons.home,
|
||||||
|
title: l10n.browse,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "search",
|
||||||
|
name: SearchPage.name,
|
||||||
|
icon: SpotubeIcons.search,
|
||||||
|
title: l10n.search,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "library",
|
||||||
|
name: LibraryPage.name,
|
||||||
|
icon: SpotubeIcons.library,
|
||||||
|
title: l10n.library,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "stats",
|
||||||
|
name: StatsPage.name,
|
||||||
|
icon: SpotubeIcons.chart,
|
||||||
|
title: l10n.stats,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
@ -115,4 +115,13 @@ abstract class SpotubeIcons {
|
|||||||
static const github = SimpleIcons.github;
|
static const github = SimpleIcons.github;
|
||||||
static const openCollective = SimpleIcons.opencollective;
|
static const openCollective = SimpleIcons.opencollective;
|
||||||
static const anonymous = FeatherIcons.user;
|
static const anonymous = FeatherIcons.user;
|
||||||
|
static const history = FeatherIcons.clock;
|
||||||
|
static const connect = FeatherIcons.link;
|
||||||
|
static const speaker = FeatherIcons.speaker;
|
||||||
|
static const monitor = FeatherIcons.monitor;
|
||||||
|
static const power = FeatherIcons.power;
|
||||||
|
static const bluetooth = FeatherIcons.bluetooth;
|
||||||
|
static const chart = FeatherIcons.barChart2;
|
||||||
|
static const folderAdd = FeatherIcons.folderPlus;
|
||||||
|
static const folderRemove = FeatherIcons.folderMinus;
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
style: theme.iconButtonTheme.style?.copyWith(
|
style: theme.iconButtonTheme.style?.copyWith(
|
||||||
shape: MaterialStatePropertyAll(
|
shape: WidgetStatePropertyAll(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
@ -226,8 +226,11 @@ class _AdaptivePopSheetListItem<T> extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: const IconThemeData(opacity: 1),
|
||||||
child: IgnorePointer(child: item),
|
child: IgnorePointer(child: item),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,13 +12,13 @@ class Action extends StatelessWidget {
|
|||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
const Action({
|
const Action({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.isExpanded = true,
|
this.isExpanded = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
class AdaptiveSelectTile<T> extends HookWidget {
|
class AdaptiveSelectTile<T> extends HookWidget {
|
||||||
@ -38,11 +39,22 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final rawControl = DropdownButton<T>(
|
final rawControl = DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: DropdownButton<T>(
|
||||||
items: options,
|
items: options,
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
menuMaxHeight: mediaQuery.size.height * 0.6,
|
menuMaxHeight: mediaQuery.size.height * 0.6,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
icon: const Icon(SpotubeIcons.angleDown),
|
||||||
|
dropdownColor: theme.colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final controlPlaceholder = useMemoized(
|
final controlPlaceholder = useMemoized(
|
||||||
() => options
|
() => options
|
@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
|
|
||||||
class AnimateGradient extends HookWidget {
|
class AnimateGradient extends HookWidget {
|
||||||
const AnimateGradient({
|
const AnimateGradient({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.primaryColors,
|
required this.primaryColors,
|
||||||
required this.secondaryColors,
|
required this.secondaryColors,
|
||||||
this.child,
|
this.child,
|
||||||
@ -17,8 +17,7 @@ class AnimateGradient extends HookWidget {
|
|||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
}) : assert(primaryColors.length >= 2),
|
}) : assert(primaryColors.length >= 2),
|
||||||
assert(primaryColors.length == secondaryColors.length),
|
assert(primaryColors.length == secondaryColors.length),
|
||||||
_controller = controller,
|
_controller = controller;
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// [controller]: pass this to have a fine control over the [Animation]
|
/// [controller]: pass this to have a fine control over the [Animation]
|
||||||
final AnimationController? _controller;
|
final AnimationController? _controller;
|
@ -1,42 +0,0 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/models/logger.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
|
|
||||||
class ArtistAlbumList extends HookConsumerWidget {
|
|
||||||
final String artistId;
|
|
||||||
ArtistAlbumList(
|
|
||||||
this.artistId, {
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final logger = getLogger(ArtistAlbumList);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
|
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
|
||||||
return albumsQuery.pages
|
|
||||||
.expand<Album>((page) => page.items ?? const Iterable.empty())
|
|
||||||
.toList();
|
|
||||||
}, [albumsQuery.pages]);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<Album>(
|
|
||||||
isLoadingNextPage: albumsQuery.isLoadingNextPage,
|
|
||||||
hasNextPage: albumsQuery.hasNextPage,
|
|
||||||
items: albums,
|
|
||||||
onFetchMore: albumsQuery.fetchNext,
|
|
||||||
title: Text(
|
|
||||||
context.l10n.albums,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -79,7 +79,7 @@ class BorderedText extends StatelessWidget {
|
|||||||
strutStyle: child.strutStyle,
|
strutStyle: child.strutStyle,
|
||||||
textAlign: child.textAlign,
|
textAlign: child.textAlign,
|
||||||
textDirection: child.textDirection,
|
textDirection: child.textDirection,
|
||||||
textScaleFactor: child.textScaleFactor,
|
textScaler: child.textScaler,
|
||||||
),
|
),
|
||||||
child,
|
child,
|
||||||
],
|
],
|
@ -11,12 +11,12 @@ class CompactSearch extends HookWidget {
|
|||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
|
||||||
const CompactSearch({
|
const CompactSearch({
|
||||||
Key? key,
|
super.key,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.placeholder = "Search...",
|
this.placeholder = "Search...",
|
||||||
this.icon = SpotubeIcons.search,
|
this.icon = SpotubeIcons.search,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,74 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
|
||||||
|
|
||||||
class TokenLoginForm extends HookConsumerWidget {
|
|
||||||
final void Function()? onDone;
|
|
||||||
const TokenLoginForm({
|
|
||||||
Key? key,
|
|
||||||
this.onDone,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final authenticationNotifier =
|
|
||||||
ref.watch(AuthenticationNotifier.provider.notifier);
|
|
||||||
final directCodeController = useTextEditingController();
|
|
||||||
final mounted = useIsMounted();
|
|
||||||
|
|
||||||
final isLoading = useState(false);
|
|
||||||
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 400,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: directCodeController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.l10n.spotify_cookie("\"sp_dc\""),
|
|
||||||
labelText: context.l10n.cookie_name_cookie("sp_dc"),
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.visiblePassword,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: isLoading.value
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
if (directCodeController.text.isEmpty) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(context.l10n.fill_in_all_fields),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final cookieHeader =
|
|
||||||
"sp_dc=${directCodeController.text.trim()}";
|
|
||||||
|
|
||||||
authenticationNotifier.setCredentials(
|
|
||||||
await AuthenticationCredentials.fromCookie(
|
|
||||||
cookieHeader),
|
|
||||||
);
|
|
||||||
if (mounted()) {
|
|
||||||
onDone?.call();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.submit),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
class ConfirmDownloadDialog extends StatelessWidget {
|
class ConfirmDownloadDialog extends StatelessWidget {
|
||||||
const ConfirmDownloadDialog({Key? key}) : super(key: key);
|
const ConfirmDownloadDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -82,7 +82,7 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
|
|
||||||
class BulletPoint extends StatelessWidget {
|
class BulletPoint extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
const BulletPoint(this.text, {Key? key}) : super(key: key);
|
const BulletPoint(this.text, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -5,7 +5,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
class PipedDownDialog extends HookConsumerWidget {
|
class PipedDownDialog extends HookConsumerWidget {
|
||||||
const PipedDownDialog({Key? key}) : super(key: key);
|
const PipedDownDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
@ -1,16 +1,14 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
||||||
/// The id of the playlist this dialog was opened from
|
/// The id of the playlist this dialog was opened from
|
||||||
@ -19,33 +17,40 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
const PlaylistAddTrackDialog({
|
const PlaylistAddTrackDialog({
|
||||||
required this.tracks,
|
required this.tracks,
|
||||||
required this.openFromPlaylist,
|
required this.openFromPlaylist,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final ThemeData(:textTheme) = Theme.of(context);
|
final ThemeData(:textTheme) = Theme.of(context);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||||
final userPlaylists = useQueries.playlist.ofMineAll(ref);
|
final favoritePlaylistsNotifier =
|
||||||
|
ref.watch(favoritePlaylistsProvider.notifier);
|
||||||
|
|
||||||
final me = useQueries.user.me(ref);
|
final me = ref.watch(meProvider);
|
||||||
|
|
||||||
final filteredPlaylists = useMemoized(
|
final filteredPlaylists = useMemoized(
|
||||||
() =>
|
() =>
|
||||||
userPlaylists.data
|
userPlaylists.asData?.value.items
|
||||||
?.where(
|
.where(
|
||||||
(playlist) =>
|
(playlist) =>
|
||||||
playlist.owner?.id != null &&
|
playlist.owner?.id != null &&
|
||||||
playlist.owner!.id == me.data?.id &&
|
playlist.owner!.id == me.asData?.value.id &&
|
||||||
playlist.id != openFromPlaylist,
|
playlist.id != openFromPlaylist,
|
||||||
)
|
)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
[userPlaylists.data, me.data?.id, openFromPlaylist],
|
[userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
|
||||||
);
|
);
|
||||||
|
|
||||||
final playlistsCheck = useState(<String, bool>{});
|
final playlistsCheck = useState(<String, bool>{});
|
||||||
final queryClient = useQueryClient();
|
|
||||||
|
useEffect(() {
|
||||||
|
if (userPlaylists.asData?.value != null) {
|
||||||
|
favoritePlaylistsNotifier.fetchAll();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [userPlaylists.asData?.value]);
|
||||||
|
|
||||||
Future<void> onAdd() async {
|
Future<void> onAdd() async {
|
||||||
final selectedPlaylists = playlistsCheck.value.entries
|
final selectedPlaylists = playlistsCheck.value.entries
|
||||||
@ -54,21 +59,12 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
selectedPlaylists.map(
|
selectedPlaylists.map(
|
||||||
(playlistId) => spotify.playlists.addTracks(
|
(playlistId) => favoritePlaylistsNotifier.addTracks(
|
||||||
tracks
|
playlistId,
|
||||||
.map(
|
tracks.map((e) => e.id!).toList(),
|
||||||
(track) => track.uri!,
|
),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
playlistId),
|
|
||||||
),
|
),
|
||||||
).then((_) => Navigator.pop(context, true));
|
).then((_) => Navigator.pop(context, true));
|
||||||
|
|
||||||
await queryClient.refreshQueries(
|
|
||||||
selectedPlaylists
|
|
||||||
.map((playlistId) => "playlist-tracks/$playlistId")
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
@ -109,8 +105,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
secondary: CircleAvatar(
|
secondary: CircleAvatar(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
backgroundImage: UniversalImage.imageProvider(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
playlist.images.asUrlString(
|
||||||
playlist.images,
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
),
|
),
|
@ -8,8 +8,7 @@ final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
|||||||
|
|
||||||
class ReplaceDownloadedDialog extends ConsumerWidget {
|
class ReplaceDownloadedDialog extends ConsumerWidget {
|
||||||
final Track track;
|
final Track track;
|
||||||
const ReplaceDownloadedDialog({required this.track, Key? key})
|
const ReplaceDownloadedDialog({required this.track, super.key});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
67
lib/components/dialogs/select_device_dialog.dart
Normal file
67
lib/components/dialogs/select_device_dialog.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
|
||||||
|
class SelectDeviceDialog extends HookConsumerWidget {
|
||||||
|
const SelectDeviceDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final isRemoteService = useState(false);
|
||||||
|
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
final remoteService = connectClients.asData!.value.resolvedService!;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(context.l10n.choose_the_device),
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.multiple_device_connected),
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
title: Text(remoteService.name),
|
||||||
|
value: true,
|
||||||
|
groupValue: isRemoteService.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isRemoteService.value = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
title: Text(context.l10n.this_device),
|
||||||
|
value: false,
|
||||||
|
groupValue: isRemoteService.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isRemoteService.value = !value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(isRemoteService.value);
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.select),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showSelectDeviceDialog(BuildContext context, WidgetRef ref) async {
|
||||||
|
final connectClients = ref.read(connectClientsProvider);
|
||||||
|
|
||||||
|
if (connectClients.asData?.value.resolvedService == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isRemote = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const SelectDeviceDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isRemote ?? false;
|
||||||
|
}
|
@ -2,20 +2,20 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/links/hyper_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
import 'package:spotube/components/shared/links/link_text.dart';
|
import 'package:spotube/components/links/hyper_link.dart';
|
||||||
|
import 'package:spotube/components/links/link_text.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
|
|
||||||
class TrackDetailsDialog extends HookWidget {
|
class TrackDetailsDialog extends HookWidget {
|
||||||
final Track track;
|
final Track track;
|
||||||
const TrackDetailsDialog({
|
const TrackDetailsDialog({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -24,10 +24,11 @@ class TrackDetailsDialog extends HookWidget {
|
|||||||
|
|
||||||
final detailsMap = {
|
final detailsMap = {
|
||||||
context.l10n.title: track.name!,
|
context.l10n.title: track.name!,
|
||||||
context.l10n.artist: TypeConversionUtils.artists_X_ClickableArtists(
|
context.l10n.artist: ArtistLink(
|
||||||
track.artists ?? <Artist>[],
|
artists: track.artists ?? <Artist>[],
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
textStyle: const TextStyle(color: Colors.blue),
|
textStyle: const TextStyle(color: Colors.blue),
|
||||||
|
hideOverflowArtist: false,
|
||||||
),
|
),
|
||||||
context.l10n.album: LinkText(
|
context.l10n.album: LinkText(
|
||||||
track.album!.name!,
|
track.album!.name!,
|
@ -10,12 +10,12 @@ class ExpandableSearchField extends StatelessWidget {
|
|||||||
final FocusNode searchFocus;
|
final FocusNode searchFocus;
|
||||||
|
|
||||||
const ExpandableSearchField({
|
const ExpandableSearchField({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.isFiltering,
|
required this.isFiltering,
|
||||||
required this.onChangeFiltering,
|
required this.onChangeFiltering,
|
||||||
required this.searchController,
|
required this.searchController,
|
||||||
required this.searchFocus,
|
required this.searchFocus,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -60,12 +60,12 @@ class ExpandableSearchButton extends StatelessWidget {
|
|||||||
final ValueChanged<bool>? onPressed;
|
final ValueChanged<bool>? onPressed;
|
||||||
|
|
||||||
const ExpandableSearchButton({
|
const ExpandableSearchButton({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.isFiltering,
|
required this.isFiltering,
|
||||||
required this.searchFocus,
|
required this.searchFocus,
|
||||||
this.icon = const Icon(SpotubeIcons.filter),
|
this.icon = const Icon(SpotubeIcons.filter),
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,22 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class AnonymousFallback extends ConsumerWidget {
|
class AnonymousFallback extends ConsumerWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
const AnonymousFallback({
|
const AnonymousFallback({
|
||||||
Key? key,
|
super.key,
|
||||||
this.child,
|
this.child,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final isLoggedIn = ref.watch(AuthenticationNotifier.provider) != null;
|
final isLoggedIn = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
if (isLoggedIn && child != null) return child!;
|
if (isLoggedIn.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn.asData?.value != null && child != null) return child!;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -25,7 +30,7 @@ class AnonymousFallback extends ConsumerWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: Text(context.l10n.login_with_spotify),
|
child: Text(context.l10n.login_with_spotify),
|
||||||
onPressed: () => ServiceUtils.push(context, "/settings"),
|
onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
class NotFound extends StatelessWidget {
|
class NotFound extends StatelessWidget {
|
||||||
final bool vertical;
|
final bool vertical;
|
||||||
const NotFound({Key? key, this.vertical = false}) : super(key: key);
|
const NotFound({super.key, this.vertical = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -18,9 +19,9 @@ class NotFound extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text("Nothing found", style: theme.textTheme.titleLarge),
|
Text(context.l10n.nothing_found, style: theme.textTheme.titleLarge),
|
||||||
Text(
|
Text(
|
||||||
"The box is empty",
|
context.l10n.the_box_is_empty,
|
||||||
style: theme.textTheme.titleMedium,
|
style: theme.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
104
lib/components/framework/app_pop_scope.dart
Normal file
104
lib/components/framework/app_pop_scope.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A temporary workaround for [WillPopScope] and [PopScope] not working in GoRouter
|
||||||
|
/// https://github.com/flutter/flutter/issues/140869#issuecomment-2247181468
|
||||||
|
class AppPopScope extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
final PopInvokedCallback? onPopInvoked;
|
||||||
|
|
||||||
|
final bool canPop;
|
||||||
|
|
||||||
|
const AppPopScope({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.canPop = true,
|
||||||
|
this.onPopInvoked,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppPopScope> createState() => _AppPopScopeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPopScopeState extends State<AppPopScope> {
|
||||||
|
final bool _enable = Platform.isAndroid;
|
||||||
|
ModalRoute? _route;
|
||||||
|
BackButtonDispatcher? _parentBackBtnDispatcher;
|
||||||
|
ChildBackButtonDispatcher? _backBtnDispatcher;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_route = ModalRoute.of(context);
|
||||||
|
_updateBackButtonDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void activate() {
|
||||||
|
super.activate();
|
||||||
|
_updateBackButtonDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
super.deactivate();
|
||||||
|
_disposeBackBtnDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposeBackBtnDispatcher();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: widget.canPop,
|
||||||
|
onPopInvoked: widget.onPopInvoked,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBackButtonDispatcher() {
|
||||||
|
if (!_enable) return;
|
||||||
|
|
||||||
|
var dispatcher = Router.maybeOf(context)?.backButtonDispatcher;
|
||||||
|
if (dispatcher != _parentBackBtnDispatcher) {
|
||||||
|
_disposeBackBtnDispatcher();
|
||||||
|
_parentBackBtnDispatcher = dispatcher;
|
||||||
|
if (dispatcher is BackButtonDispatcher &&
|
||||||
|
dispatcher is! ChildBackButtonDispatcher) {
|
||||||
|
dispatcher = dispatcher.createChildBackButtonDispatcher();
|
||||||
|
}
|
||||||
|
_backBtnDispatcher = dispatcher as ChildBackButtonDispatcher;
|
||||||
|
}
|
||||||
|
_backBtnDispatcher?.removeCallback(_handleBackButton);
|
||||||
|
_backBtnDispatcher?.addCallback(_handleBackButton);
|
||||||
|
_backBtnDispatcher?.takePriority();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposeBackBtnDispatcher() {
|
||||||
|
_backBtnDispatcher?.removeCallback(_handleBackButton);
|
||||||
|
if (_backBtnDispatcher is ChildBackButtonDispatcher) {
|
||||||
|
final child = _backBtnDispatcher as ChildBackButtonDispatcher;
|
||||||
|
_parentBackBtnDispatcher?.forget(child);
|
||||||
|
}
|
||||||
|
_backBtnDispatcher = null;
|
||||||
|
_parentBackBtnDispatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _onlyRoute => _route != null && _route!.isFirst && _route!.isCurrent;
|
||||||
|
|
||||||
|
Future<bool> _handleBackButton() async {
|
||||||
|
if (_onlyRoute) {
|
||||||
|
widget.onPopInvoked?.call(widget.canPop);
|
||||||
|
if (!widget.canPop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
86
lib/components/heart_button/heart_button.dart
Normal file
86
lib/components/heart_button/heart_button.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
|
class HeartButton extends HookConsumerWidget {
|
||||||
|
final bool isLiked;
|
||||||
|
final void Function()? onPressed;
|
||||||
|
final IconData? icon;
|
||||||
|
final Color? color;
|
||||||
|
final String? tooltip;
|
||||||
|
const HeartButton({
|
||||||
|
required this.isLiked,
|
||||||
|
required this.onPressed,
|
||||||
|
this.color,
|
||||||
|
this.tooltip,
|
||||||
|
this.icon,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
|
if (auth.asData?.value == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
tooltip: tooltip,
|
||||||
|
icon: AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
icon ??
|
||||||
|
(isLiked
|
||||||
|
? Icons.favorite_rounded
|
||||||
|
: Icons.favorite_outline_rounded),
|
||||||
|
key: ValueKey(isLiked),
|
||||||
|
color: color ?? (isLiked ? color ?? Colors.red : null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackHeartButton extends HookConsumerWidget {
|
||||||
|
final Track track;
|
||||||
|
const TrackHeartButton({
|
||||||
|
super.key,
|
||||||
|
required this.track,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final savedTracks = ref.watch(likedTracksProvider);
|
||||||
|
final me = ref.watch(meProvider);
|
||||||
|
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
|
||||||
|
|
||||||
|
if (me.isLoading) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return HeartButton(
|
||||||
|
tooltip: isLiked
|
||||||
|
? context.l10n.remove_from_favorites
|
||||||
|
: context.l10n.save_as_favorite,
|
||||||
|
isLiked: isLiked,
|
||||||
|
onPressed: savedTracks.asData?.value != null
|
||||||
|
? () {
|
||||||
|
toggleTrackLike(track);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
lib/components/heart_button/use_track_toggle_like.dart
Normal file
37
lib/components/heart_button/use_track_toggle_like.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
|
typedef UseTrackToggleLike = ({
|
||||||
|
bool isLiked,
|
||||||
|
Future<void> Function(Track track) toggleTrackLike,
|
||||||
|
});
|
||||||
|
|
||||||
|
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
|
||||||
|
final savedTracks = ref.watch(likedTracksProvider);
|
||||||
|
final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
|
||||||
|
|
||||||
|
final isLiked = useMemoized(
|
||||||
|
() =>
|
||||||
|
savedTracks.asData?.value.any((element) => element.id == track.id) ??
|
||||||
|
false,
|
||||||
|
[savedTracks.asData?.value, track.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
|
||||||
|
|
||||||
|
return (
|
||||||
|
isLiked: isLiked,
|
||||||
|
toggleTrackLike: (track) async {
|
||||||
|
await savedTracksNotifier.toggleFavorite(track);
|
||||||
|
|
||||||
|
if (!isLiked) {
|
||||||
|
await scrobblerNotifier.love(track);
|
||||||
|
} else {
|
||||||
|
await scrobblerNotifier.unlove(track);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
|
|
||||||
class HomeFeaturedSection extends HookConsumerWidget {
|
|
||||||
const HomeFeaturedSection({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
|
|
||||||
final playlists = useMemoized(
|
|
||||||
() => featuredPlaylistsQuery.pages
|
|
||||||
.whereType<Page<PlaylistSimple>>()
|
|
||||||
.expand((page) => page.items ?? const <PlaylistSimple>[]),
|
|
||||||
[featuredPlaylistsQuery.pages],
|
|
||||||
);
|
|
||||||
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
|
|
||||||
!featuredPlaylistsQuery.isLoadingNextPage;
|
|
||||||
|
|
||||||
return Skeletonizer(
|
|
||||||
enabled: isLoadingFeaturedPlaylists,
|
|
||||||
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
|
||||||
items: playlists.toList(),
|
|
||||||
title: Text(context.l10n.featured),
|
|
||||||
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage,
|
|
||||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
|
||||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
|
||||||
const HomeNewReleasesSection({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
|
||||||
|
|
||||||
final newReleases = useQueries.album.newReleases(ref);
|
|
||||||
final userArtistsQuery = useQueries.artist.followedByMeAll(ref);
|
|
||||||
final userArtists =
|
|
||||||
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
|
|
||||||
|
|
||||||
final albums = useMemoized(
|
|
||||||
() {
|
|
||||||
final allReleases = newReleases.pages
|
|
||||||
.whereType<Page<AlbumSimple>>()
|
|
||||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
|
||||||
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
|
|
||||||
|
|
||||||
final userArtistReleases = allReleases.where((album) {
|
|
||||||
return album.artists
|
|
||||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
|
||||||
true;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
if (userArtistReleases.isEmpty) return allReleases.toList();
|
|
||||||
return userArtistReleases;
|
|
||||||
},
|
|
||||||
[newReleases.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
final hasNewReleases = newReleases.hasPageData &&
|
|
||||||
userArtistsQuery.hasData &&
|
|
||||||
!newReleases.isLoadingNextPage;
|
|
||||||
|
|
||||||
if (auth == null || !hasNewReleases) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<Album>(
|
|
||||||
items: albums,
|
|
||||||
title: Text(context.l10n.new_releases),
|
|
||||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
|
||||||
hasNextPage: newReleases.hasNextPage,
|
|
||||||
onFetchMore: newReleases.fetchNext,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,9 +5,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/components/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/components/artist/artist_card.dart';
|
import 'package:spotube/modules/artist/artist_card.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@ -17,20 +17,22 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
final VoidCallback onFetchMore;
|
final VoidCallback onFetchMore;
|
||||||
final bool isLoadingNextPage;
|
final bool isLoadingNextPage;
|
||||||
final bool hasNextPage;
|
final bool hasNextPage;
|
||||||
|
final Widget? titleTrailing;
|
||||||
|
|
||||||
const HorizontalPlaybuttonCardView({
|
HorizontalPlaybuttonCardView({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.hasNextPage,
|
required this.hasNextPage,
|
||||||
required this.onFetchMore,
|
required this.onFetchMore,
|
||||||
required this.isLoadingNextPage,
|
required this.isLoadingNextPage,
|
||||||
Key? key,
|
this.titleTrailing,
|
||||||
|
super.key,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
items is List<PlaylistSimple> ||
|
items.every(
|
||||||
items is List<Album> ||
|
(item) =>
|
||||||
items is List<Artist>,
|
item is PlaylistSimple || item is Artist || item is AlbumSimple,
|
||||||
),
|
),
|
||||||
super(key: key);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -48,11 +50,17 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
DefaultTextStyle(
|
DefaultTextStyle(
|
||||||
style: textTheme.titleMedium!,
|
style: textTheme.titleMedium!,
|
||||||
child: title,
|
child: title,
|
||||||
),
|
),
|
||||||
|
if (titleTrailing != null) titleTrailing!,
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: height,
|
height: height,
|
||||||
child: NotificationListener(
|
child: NotificationListener(
|
||||||
@ -85,11 +93,11 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
|
|
||||||
return switch (item.runtimeType) {
|
return switch (item) {
|
||||||
PlaylistSimple =>
|
PlaylistSimple() =>
|
||||||
PlaylistCard(item as PlaylistSimple),
|
PlaylistCard(item as PlaylistSimple),
|
||||||
Album => AlbumCard(item as Album),
|
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
||||||
Artist => Padding(
|
Artist() => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12.0),
|
horizontal: 12.0),
|
||||||
child: ArtistCard(item as Artist),
|
child: ArtistCard(item as Artist),
|
@ -7,8 +7,8 @@ class HoverBuilder extends HookWidget {
|
|||||||
const HoverBuilder({
|
const HoverBuilder({
|
||||||
required this.builder,
|
required this.builder,
|
||||||
this.permanentState,
|
this.permanentState,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -20,8 +20,8 @@ class UniversalImage extends HookWidget {
|
|||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.fit,
|
this.fit,
|
||||||
this.scale = 1,
|
this.scale = 1,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
static ImageProvider imageProvider(
|
static ImageProvider imageProvider(
|
||||||
String path, {
|
String path, {
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class InterScrollbar extends HookWidget {
|
class InterScrollbar extends HookWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -15,7 +15,7 @@ class InterScrollbar extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (DesktopTools.platform.isDesktop) return child;
|
if (kIsDesktop) return child;
|
||||||
|
|
||||||
return DraggableScrollbar.semicircle(
|
return DraggableScrollbar.semicircle(
|
||||||
controller: controller,
|
controller: controller,
|
@ -1,128 +0,0 @@
|
|||||||
import 'package:flutter/material.dart' hide Image;
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package: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/album/album_card.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/fallbacks/anonymous_fallback.dart';
|
|
||||||
import 'package:spotube/components/shared/waypoint.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class UserAlbums extends HookConsumerWidget {
|
|
||||||
const UserAlbums({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
|
||||||
final albumsQuery = useQueries.album.ofMine(ref);
|
|
||||||
|
|
||||||
final controller = useScrollController();
|
|
||||||
|
|
||||||
final searchText = useState('');
|
|
||||||
|
|
||||||
final allAlbums = useMemoized(
|
|
||||||
() => albumsQuery.pages
|
|
||||||
.expand((element) => element.items ?? <AlbumSimple>[]),
|
|
||||||
[albumsQuery.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
|
||||||
if (searchText.value.isEmpty) {
|
|
||||||
return allAlbums;
|
|
||||||
}
|
|
||||||
return allAlbums
|
|
||||||
.map((e) => (
|
|
||||||
weightedRatio(e.name!, searchText.value),
|
|
||||||
e,
|
|
||||||
))
|
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
|
||||||
.where((e) => e.$1 > 50)
|
|
||||||
.map((e) => e.$2)
|
|
||||||
.toList();
|
|
||||||
}, [allAlbums, searchText.value]);
|
|
||||||
|
|
||||||
if (auth == null) {
|
|
||||||
return const AnonymousFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await albumsQuery.refresh();
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(50),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
child: SearchBar(
|
|
||||||
onChanged: (value) => searchText.value = value,
|
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
|
||||||
hintText: context.l10n.filter_albums,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SizedBox.expand(
|
|
||||||
child: InterScrollbar(
|
|
||||||
controller: controller,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
controller: controller,
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: albumsQuery.pages.isEmpty,
|
|
||||||
child: Center(
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 20,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (albumsQuery.pages.isEmpty)
|
|
||||||
...List.generate(
|
|
||||||
10,
|
|
||||||
(index) => AlbumCard(FakeData.album),
|
|
||||||
)
|
|
||||||
else if (albums.isEmpty)
|
|
||||||
const Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [NotFound()],
|
|
||||||
),
|
|
||||||
for (final album in albums)
|
|
||||||
AlbumCard(
|
|
||||||
TypeConversionUtils.simpleAlbum_X_Album(album),
|
|
||||||
),
|
|
||||||
if (albums.isNotEmpty && albumsQuery.hasNextPage)
|
|
||||||
Waypoint(
|
|
||||||
controller: controller,
|
|
||||||
isGrid: true,
|
|
||||||
onTouchEdge: albumsQuery.fetchNext,
|
|
||||||
child: AlbumCard(FakeData.album),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
import 'package:spotube/collections/fake.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
|
||||||
import 'package:spotube/components/artist/artist_card.dart';
|
|
||||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
|
|
||||||
class UserArtists extends HookConsumerWidget {
|
|
||||||
const UserArtists({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
|
||||||
|
|
||||||
final artistQuery = useQueries.artist.followedByMeAll(ref);
|
|
||||||
|
|
||||||
final searchText = useState('');
|
|
||||||
|
|
||||||
final filteredArtists = useMemoized(() {
|
|
||||||
final artists = artistQuery.data ?? [];
|
|
||||||
|
|
||||||
if (searchText.value.isEmpty) {
|
|
||||||
return artists.toList();
|
|
||||||
}
|
|
||||||
return artists
|
|
||||||
.map((e) => (
|
|
||||||
weightedRatio(e.name!, searchText.value),
|
|
||||||
e,
|
|
||||||
))
|
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
|
||||||
.where((e) => e.$1 > 50)
|
|
||||||
.map((e) => e.$2)
|
|
||||||
.toList();
|
|
||||||
}, [artistQuery.data, searchText.value]);
|
|
||||||
|
|
||||||
final controller = useScrollController();
|
|
||||||
|
|
||||||
if (auth == null) {
|
|
||||||
return const AnonymousFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(50),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
child: SearchBar(
|
|
||||||
onChanged: (value) => searchText.value = value,
|
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
|
||||||
hintText: context.l10n.filter_artist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
|
||||||
body: artistQuery.data?.isEmpty == true
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(context.l10n.loading),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await artistQuery.refresh();
|
|
||||||
},
|
|
||||||
child: InterScrollbar(
|
|
||||||
controller: controller,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: controller,
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: SafeArea(
|
|
||||||
child: Center(
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: artistQuery.isLoading,
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 15,
|
|
||||||
runSpacing: 5,
|
|
||||||
children: artistQuery.isLoading
|
|
||||||
? List.generate(
|
|
||||||
10, (index) => ArtistCard(FakeData.artist))
|
|
||||||
: filteredArtists.isEmpty
|
|
||||||
? [
|
|
||||||
const Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
NotFound(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: filteredArtists
|
|
||||||
.mapIndexed((index, artist) =>
|
|
||||||
ArtistCard(artist))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,324 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:metadata_god/metadata_god.dart';
|
|
||||||
import 'package:mime/mime.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/fake.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
|
||||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
|
||||||
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
|
||||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
|
||||||
|
|
||||||
const supportedAudioTypes = [
|
|
||||||
"audio/webm",
|
|
||||||
"audio/ogg",
|
|
||||||
"audio/mpeg",
|
|
||||||
"audio/mp4",
|
|
||||||
"audio/opus",
|
|
||||||
"audio/wav",
|
|
||||||
"audio/aac",
|
|
||||||
];
|
|
||||||
|
|
||||||
const imgMimeToExt = {
|
|
||||||
"image/png": ".png",
|
|
||||||
"image/jpeg": ".jpg",
|
|
||||||
"image/webp": ".webp",
|
|
||||||
"image/gif": ".gif",
|
|
||||||
};
|
|
||||||
|
|
||||||
enum SortBy {
|
|
||||||
none,
|
|
||||||
ascending,
|
|
||||||
descending,
|
|
||||||
newest,
|
|
||||||
oldest,
|
|
||||||
duration,
|
|
||||||
artist,
|
|
||||||
album,
|
|
||||||
}
|
|
||||||
|
|
||||||
final localTracksProvider = FutureProvider<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: TypeConversionUtils.localTrack_X_Track(
|
|
||||||
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({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
Future<void> playLocalTracks(
|
|
||||||
WidgetRef ref,
|
|
||||||
List<LocalTrack> tracks, {
|
|
||||||
LocalTrack? currentTrack,
|
|
||||||
}) async {
|
|
||||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
|
||||||
final playback = ref.read(ProxyPlaylistNotifier.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(ProxyPlaylistNotifier.provider);
|
|
||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
|
||||||
final isPlaylistPlaying =
|
|
||||||
playlist.containsTracks(trackSnapshot.value ?? []);
|
|
||||||
|
|
||||||
final searchController = useTextEditingController();
|
|
||||||
useValueListenable(searchController);
|
|
||||||
final searchFocus = useFocusNode();
|
|
||||||
final isFiltering = useState(false);
|
|
||||||
|
|
||||||
final controller = useScrollController();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: trackSnapshot.value != null
|
|
||||||
? () async {
|
|
||||||
if (trackSnapshot.value?.isNotEmpty == true) {
|
|
||||||
if (!isPlaylistPlaying) {
|
|
||||||
await playLocalTracks(
|
|
||||||
ref,
|
|
||||||
trackSnapshot.value!,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// TODO: Remove stop capability
|
|
||||||
// playlistNotifier.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: 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: 10),
|
|
||||||
FilledButton(
|
|
||||||
child: const Icon(SpotubeIcons.refresh),
|
|
||||||
onPressed: () {
|
|
||||||
ref.refresh(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} - ${TypeConversionUtils.artists_X_String<Artist>(e.artists ?? [])}",
|
|
||||||
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.refresh(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(track: FakeData.track, index: index);
|
|
||||||
}
|
|
||||||
|
|
||||||
final track = filteredTracks[index];
|
|
||||||
return TrackTile(
|
|
||||||
index: index,
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
error: (error, stackTrace) =>
|
|
||||||
Text(error.toString() + stackTrace.toString()),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,13 +11,13 @@ class AnchorButton<T> extends HookWidget {
|
|||||||
|
|
||||||
const AnchorButton(
|
const AnchorButton(
|
||||||
this.text, {
|
this.text, {
|
||||||
Key? key,
|
super.key,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
this.overflow,
|
this.overflow,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.style = const TextStyle(),
|
this.style = const TextStyle(),
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -29,7 +29,7 @@ class AnchorButton<T> extends HookWidget {
|
|||||||
onTapUp: (event) => tap.value = false,
|
onTapUp: (event) => tap.value = false,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: MaterialStateMouseCursor.clickable,
|
cursor: WidgetStateMouseCursor.clickable,
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: style.copyWith(
|
style: style.copyWith(
|
81
lib/components/links/artist_link.dart
Normal file
81
lib/components/links/artist_link.dart
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/links/anchor_button.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/pages/artist/artist.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class ArtistLink extends StatelessWidget {
|
||||||
|
final List<ArtistSimple> artists;
|
||||||
|
final WrapCrossAlignment crossAxisAlignment;
|
||||||
|
final WrapAlignment mainAxisAlignment;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
final bool hideOverflowArtist;
|
||||||
|
final void Function(String route)? onRouteChange;
|
||||||
|
final VoidCallback? onOverflowArtistClick;
|
||||||
|
|
||||||
|
const ArtistLink({
|
||||||
|
super.key,
|
||||||
|
required this.artists,
|
||||||
|
this.crossAxisAlignment = WrapCrossAlignment.center,
|
||||||
|
this.mainAxisAlignment = WrapAlignment.center,
|
||||||
|
this.textStyle = const TextStyle(),
|
||||||
|
this.onRouteChange,
|
||||||
|
this.hideOverflowArtist = true,
|
||||||
|
this.onOverflowArtistClick,
|
||||||
|
}) : assert(hideOverflowArtist ? onOverflowArtistClick != null : true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
|
alignment: mainAxisAlignment,
|
||||||
|
children: [
|
||||||
|
...(hideOverflowArtist ? artists.take(3).toList() : artists)
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map(
|
||||||
|
(artist) => Builder(builder: (context) {
|
||||||
|
if (artist.value.name == null) {
|
||||||
|
return Text("Spotify", style: textStyle);
|
||||||
|
}
|
||||||
|
return AnchorButton(
|
||||||
|
(artist.key != artists.length - 1)
|
||||||
|
? "${artist.value.name}, "
|
||||||
|
: artist.value.name!,
|
||||||
|
onTap: () {
|
||||||
|
if (onRouteChange != null) {
|
||||||
|
onRouteChange?.call("/artist/${artist.value.id}");
|
||||||
|
} else {
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
ArtistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": artist.value.id!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
if (hideOverflowArtist && artists.length > 3)
|
||||||
|
AnchorButton(
|
||||||
|
context.l10n.and_n_more(artists.length - 3),
|
||||||
|
onTap: () {
|
||||||
|
onOverflowArtistClick?.call();
|
||||||
|
},
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textStyle.copyWith(
|
||||||
|
color: colorScheme.secondary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/links/anchor_button.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class Hyperlink extends StatelessWidget {
|
class Hyperlink extends StatelessWidget {
|
||||||
@ -13,12 +13,12 @@ class Hyperlink extends StatelessWidget {
|
|||||||
const Hyperlink(
|
const Hyperlink(
|
||||||
this.text,
|
this.text,
|
||||||
this.url, {
|
this.url, {
|
||||||
Key? key,
|
super.key,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
this.overflow,
|
this.overflow,
|
||||||
this.style = const TextStyle(),
|
this.style = const TextStyle(),
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/links/anchor_button.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class LinkText<T> extends StatelessWidget {
|
class LinkText<T> extends StatelessWidget {
|
||||||
@ -15,14 +15,14 @@ class LinkText<T> extends StatelessWidget {
|
|||||||
const LinkText(
|
const LinkText(
|
||||||
this.text,
|
this.text,
|
||||||
this.route, {
|
this.route, {
|
||||||
Key? key,
|
super.key,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
this.extra,
|
this.extra,
|
||||||
this.overflow,
|
this.overflow,
|
||||||
this.style = const TextStyle(),
|
this.style = const TextStyle(),
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.push = false,
|
this.push = false,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,4 +1,4 @@
|
|||||||
part of panels;
|
part of 'sliding_up_panel.dart';
|
||||||
|
|
||||||
class PanelController extends ChangeNotifier {
|
class PanelController extends ChangeNotifier {
|
||||||
SlidingUpPanelState? _panelState;
|
SlidingUpPanelState? _panelState;
|
||||||
@ -41,29 +41,33 @@ class PanelController extends ChangeNotifier {
|
|||||||
bool get isAttached => _panelState != null;
|
bool get isAttached => _panelState != null;
|
||||||
|
|
||||||
/// Closes the sliding panel to its collapsed state (i.e. to the minHeight)
|
/// Closes the sliding panel to its collapsed state (i.e. to the minHeight)
|
||||||
Future<void> close() {
|
Future<void> close() async {
|
||||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||||
return _panelState!._close();
|
await _panelState!._close();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the sliding panel fully
|
/// Opens the sliding panel fully
|
||||||
/// (i.e. to the maxHeight)
|
/// (i.e. to the maxHeight)
|
||||||
Future<void> open() {
|
Future<void> open() async {
|
||||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||||
return _panelState!._open();
|
await _panelState!._open();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hides the sliding panel (i.e. is invisible)
|
/// Hides the sliding panel (i.e. is invisible)
|
||||||
Future<void> hide() {
|
Future<void> hide() async {
|
||||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||||
return _panelState!._hide();
|
await _panelState!._hide();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the sliding panel in its collapsed state
|
/// Shows the sliding panel in its collapsed state
|
||||||
/// (i.e. "un-hide" the sliding panel)
|
/// (i.e. "un-hide" the sliding panel)
|
||||||
Future<void> show() {
|
Future<void> show() async {
|
||||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||||
return _panelState!._show();
|
await _panelState!._show();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Animates the panel position to the value.
|
/// Animates the panel position to the value.
|
@ -1,4 +1,4 @@
|
|||||||
part of panels;
|
part of "sliding_up_panel.dart";
|
||||||
|
|
||||||
/// if you want to prevent the panel from being dragged using the widget,
|
/// if you want to prevent the panel from being dragged using the widget,
|
||||||
/// wrap the widget with this
|
/// wrap the widget with this
|
||||||
@ -47,8 +47,7 @@ class ForceDraggableWidgetRenderBox extends RenderPointerListener {
|
|||||||
/// To make [ForceDraggableWidget] work in [Scrollable] widgets
|
/// To make [ForceDraggableWidget] work in [Scrollable] widgets
|
||||||
class PanelScrollPhysics extends ScrollPhysics {
|
class PanelScrollPhysics extends ScrollPhysics {
|
||||||
final PanelController controller;
|
final PanelController controller;
|
||||||
const PanelScrollPhysics({required this.controller, ScrollPhysics? parent})
|
const PanelScrollPhysics({required this.controller, super.parent});
|
||||||
: super(parent: parent);
|
|
||||||
@override
|
@override
|
||||||
PanelScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
PanelScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||||
return PanelScrollPhysics(
|
return PanelScrollPhysics(
|
@ -146,7 +146,7 @@ class SlidingUpPanel extends StatefulWidget {
|
|||||||
final BoxDecoration? panelDecoration;
|
final BoxDecoration? panelDecoration;
|
||||||
|
|
||||||
const SlidingUpPanel(
|
const SlidingUpPanel(
|
||||||
{Key? key,
|
{super.key,
|
||||||
this.body,
|
this.body,
|
||||||
this.collapsed,
|
this.collapsed,
|
||||||
this.minHeight = 100.0,
|
this.minHeight = 100.0,
|
||||||
@ -176,8 +176,7 @@ class SlidingUpPanel extends StatefulWidget {
|
|||||||
this.panelBuilder})
|
this.panelBuilder})
|
||||||
: assert(panelBuilder != null),
|
: assert(panelBuilder != null),
|
||||||
assert(0 <= backdropOpacity && backdropOpacity <= 1.0),
|
assert(0 <= backdropOpacity && backdropOpacity <= 1.0),
|
||||||
assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0),
|
assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0);
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SlidingUpPanelState createState() => SlidingUpPanelState();
|
SlidingUpPanelState createState() => SlidingUpPanelState();
|
@ -3,23 +3,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/hover_builder.dart';
|
import 'package:spotube/components/hover_builder.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
|
|
||||||
final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true);
|
|
||||||
|
|
||||||
String? useDescription(String? description) {
|
|
||||||
return useMemoized(() {
|
|
||||||
if (description == null) return null;
|
|
||||||
return description.replaceAll(htmlTagRegexp, '');
|
|
||||||
}, [description]);
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaybuttonCard extends HookWidget {
|
class PlaybuttonCard extends HookWidget {
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onPlaybuttonPressed;
|
final void Function()? onPlaybuttonPressed;
|
||||||
@ -43,8 +35,8 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
this.onAddToQueuePressed,
|
this.onAddToQueuePressed,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.isOwner = false,
|
this.isOwner = false,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -66,19 +58,18 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
others: 15,
|
others: 15,
|
||||||
);
|
);
|
||||||
|
|
||||||
final cleanDescription = useDescription(description);
|
final unescapeHtml = description?.unescapeHtml().cleanHtml();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(maxWidth: size),
|
constraints: BoxConstraints(maxWidth: size),
|
||||||
margin: margin,
|
margin: margin,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Color.lerp(
|
color: Color.lerp(
|
||||||
theme.colorScheme.surfaceVariant,
|
theme.colorScheme.surfaceContainerHighest,
|
||||||
theme.colorScheme.surface,
|
theme.colorScheme.surface,
|
||||||
useBrightnessValue(.9, .7),
|
useBrightnessValue(.9, .7),
|
||||||
),
|
),
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
shadowColor: theme.colorScheme.background,
|
shadowColor: theme.colorScheme.surface,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
mouseCursor: SystemMouseCursors.click,
|
mouseCursor: SystemMouseCursors.click,
|
||||||
@ -137,7 +128,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
),
|
),
|
||||||
if (isHovered)
|
if (isHovered)
|
||||||
Text(
|
Text(
|
||||||
"Owned by you",
|
context.l10n.owned_by_you,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@ -158,7 +149,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
Skeleton.keep(
|
Skeleton.keep(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.background,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
foregroundColor: theme.colorScheme.primary,
|
foregroundColor: theme.colorScheme.primary,
|
||||||
minimumSize: const Size.square(10),
|
minimumSize: const Size.square(10),
|
||||||
),
|
),
|
||||||
@ -205,11 +196,11 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (cleanDescription != null)
|
if (description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
child: AutoSizeText(
|
child: AutoSizeText(
|
||||||
cleanDescription,
|
unescapeHtml!,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(.5),
|
color: theme.colorScheme.onSurface.withOpacity(.5),
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user