@ -14,3 +14,4 @@ LASTFM_API_SECRET=$LASTFM_API_SECRET
|
|||||||
RELEASE_CHANNEL=$RELEASE_CHANNEL
|
RELEASE_CHANNEL=$RELEASE_CHANNEL
|
||||||
|
|
||||||
HIDE_DONATIONS=$HIDE_DONATIONS
|
HIDE_DONATIONS=$HIDE_DONATIONS
|
||||||
|
DISABLE_SPOTIFY_IMAGES=$DISABLE_SPOTIFY_IMAGES
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.24.5"
|
"flutterSdkVersion": "3.29.0"
|
||||||
}
|
}
|
2
.fvmrc
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.24.5",
|
"flutter": "3.29.0",
|
||||||
"flavors": {}
|
"flavors": {}
|
||||||
}
|
}
|
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -9,7 +9,8 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this? (Please read the description)
|
label: Is there an existing issue for this? (Please read the description)
|
||||||
description: |
|
description: |
|
||||||
PLEASE! Make sure to check if this issue is a duplicate.
|
🚨 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.
|
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.
|
Try with multiple similar keywords, and check the closed issues too.
|
||||||
@ -50,7 +51,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
<details>
|
<details>
|
||||||
<summary>Logs</summary>
|
<summary>Logs</summary>
|
||||||
|
|
||||||
```
|
```
|
||||||
<Replace this line by pasting your logs here>
|
<Replace this line by pasting your logs here>
|
||||||
```
|
```
|
||||||
@ -60,7 +61,7 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Operating System
|
label: Operating System
|
||||||
description: The OS in which you used Spotube to face the issue.
|
description: The OS in which you used Spotube to face the issue. Use comma to separate multiple OS.
|
||||||
placeholder: Android, Linux, macOS or Windows? Make sure to include the version too.
|
placeholder: Android, Linux, macOS or Windows? Make sure to include the version too.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@ -96,7 +97,10 @@ 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. Any contributions are welcome!
|
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!
|
||||||
|
|
||||||
|
This project is maintained by one person. So PRs are always welcome. This is the best way to get your issue fixed faster.
|
||||||
options:
|
options:
|
||||||
- label: I'm ready to work on this issue!
|
- label: I'm ready to work on this issue!
|
||||||
required: false
|
required: false
|
||||||
|
5
.github/workflows/pr-lint.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.24.5
|
FLUTTER_VERSION: 3.29.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -28,7 +28,6 @@ jobs:
|
|||||||
RELEASE_CHANNEL: nightly
|
RELEASE_CHANNEL: nightly
|
||||||
HIDE_DONATIONS: 0
|
HIDE_DONATIONS: 0
|
||||||
|
|
||||||
|
|
||||||
- name: Configure repo
|
- name: Configure repo
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
flutter pub get
|
||||||
@ -36,4 +35,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Lint Dart files
|
- name: Lint Dart files
|
||||||
run: |
|
run: |
|
||||||
dart analyze --no-fatal-warnings
|
dart analyze --no-fatal-warnings
|
||||||
|
10
.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.8.3
|
default: 4.0.0
|
||||||
required: true
|
required: true
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
description: Dry run
|
||||||
@ -55,7 +55,7 @@ jobs:
|
|||||||
- uses: dsaltares/fetch-gh-release-asset@master
|
- uses: dsaltares/fetch-gh-release-asset@master
|
||||||
with:
|
with:
|
||||||
version: tags/v${{ inputs.version }} # mind the "v" prefix
|
version: tags/v${{ inputs.version }} # mind the "v" prefix
|
||||||
file: spotube-linux-${{inputs.version}}-x86_64.tar.xz
|
file: spotube-linux-${{inputs.version}}-x86_64.tar.xz
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Update PKGBUILD versions
|
- name: Update PKGBUILD versions
|
||||||
@ -111,7 +111,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Tagname (workflow dispatch)
|
- name: Tagname (workflow dispatch)
|
||||||
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
|
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: robinraju/release-downloader@main
|
- uses: robinraju/release-downloader@main
|
||||||
with:
|
with:
|
||||||
repository: KRTirtho/spotube
|
repository: KRTirtho/spotube
|
||||||
@ -120,11 +120,11 @@ jobs:
|
|||||||
zipBall: false
|
zipBall: false
|
||||||
out-file-path: dist
|
out-file-path: dist
|
||||||
fileName: "Spotube-playstore-all-arch.aab"
|
fileName: "Spotube-playstore-all-arch.aab"
|
||||||
|
|
||||||
- name: Create service-account.json
|
- name: Create service-account.json
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
|
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
|
||||||
|
|
||||||
- name: Upload Android Release to Play Store
|
- name: Upload Android Release to Play Store
|
||||||
if: ${{!inputs.dry_run}}
|
if: ${{!inputs.dry_run}}
|
||||||
uses: r0adkll/upload-google-play@v1
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
95
.github/workflows/spotube-release-binary.yml
vendored
@ -20,7 +20,8 @@ on:
|
|||||||
description: Dry run without uploading to release
|
description: Dry run without uploading to release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.24.5
|
FLUTTER_VERSION: 3.29.0
|
||||||
|
FLUTTER_CHANNEL: master
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@ -30,64 +31,72 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
platform: linux
|
platform: linux
|
||||||
|
arch: x86
|
||||||
files: |
|
files: |
|
||||||
dist/Spotube-linux-x86_64.deb
|
dist/Spotube-linux-x86_64.deb
|
||||||
dist/Spotube-linux-x86_64.rpm
|
dist/Spotube-linux-x86_64.rpm
|
||||||
dist/spotube-linux-*-x86_64.tar.xz
|
dist/spotube-linux-*-x86_64.tar.xz
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04-arm
|
||||||
platform: linux_arm
|
platform: linux
|
||||||
|
arch: arm64
|
||||||
files: |
|
files: |
|
||||||
dist/Spotube-linux-aarch64.deb
|
dist/Spotube-linux-aarch64.deb
|
||||||
dist/spotube-linux-*-aarch64.tar.xz
|
dist/spotube-linux-*-aarch64.tar.xz
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-22.04
|
||||||
platform: android
|
platform: android
|
||||||
|
arch: all
|
||||||
files: |
|
files: |
|
||||||
build/Spotube-android-all-arch.apk
|
build/Spotube-android-all-arch.apk
|
||||||
build/Spotube-playstore-all-arch.aab
|
build/Spotube-playstore-all-arch.aab
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
platform: windows
|
platform: windows
|
||||||
|
arch: x86
|
||||||
files: |
|
files: |
|
||||||
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
|
- os: macos-latest
|
||||||
platform: ios
|
platform: ios
|
||||||
|
arch: all
|
||||||
files: |
|
files: |
|
||||||
Spotube-iOS.ipa
|
Spotube-iOS.ipa
|
||||||
- os: macos-14
|
- os: macos-14
|
||||||
platform: macos
|
platform: macos
|
||||||
|
arch: all
|
||||||
files: |
|
files: |
|
||||||
build/Spotube-macos-universal.dmg
|
build/Spotube-macos-universal.dmg
|
||||||
build/Spotube-macos-universal.pkg
|
build/Spotube-macos-universal.pkg
|
||||||
runs-on: ${{matrix.os}}
|
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.18.0
|
||||||
with:
|
with:
|
||||||
cache: true
|
|
||||||
cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: ${{ env.FLUTTER_CHANNEL }}
|
||||||
|
cache: true
|
||||||
|
git-source: https://github.com/flutter/flutter.git
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
if: ${{matrix.platform == 'android'}}
|
if: ${{matrix.platform == 'android'}}
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: "zulu"
|
||||||
java-version: '17'
|
java-version: "17"
|
||||||
cache: 'gradle'
|
cache: "gradle"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Set up QEMU
|
|
||||||
if: ${{matrix.platform == 'linux_arm'}}
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
if: ${{matrix.platform == 'linux_arm'}}
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
if: ${{matrix.platform != 'linux_arm'}}
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Install Xcode
|
||||||
|
if: ${{matrix.platform == 'ios'}}
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: "16.1"
|
||||||
|
|
||||||
- name: Install ${{matrix.platform}} dependencies
|
- name: Install ${{matrix.platform}} dependencies
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
flutter pub get
|
||||||
@ -98,29 +107,17 @@ jobs:
|
|||||||
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: Unessary hosted tools
|
|
||||||
if: ${{matrix.platform == 'linux_arm'}}
|
|
||||||
uses: jlumbroso/free-disk-space@main
|
|
||||||
with:
|
|
||||||
tool-cache: false
|
|
||||||
swap-storage: false
|
|
||||||
android: true
|
|
||||||
dotnet: true
|
|
||||||
haskell: true
|
|
||||||
large-packages: true
|
|
||||||
docker-images: true
|
|
||||||
|
|
||||||
- name: Build ${{matrix.platform}} binaries
|
- name: Build ${{matrix.platform}} binaries
|
||||||
run: dart cli/cli.dart build ${{matrix.platform}}
|
run: dart cli/cli.dart build --arch=${{matrix.arch}} ${{matrix.platform}}
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{inputs.channel}}
|
CHANNEL: ${{inputs.channel}}
|
||||||
DOTENV: ${{secrets.DOTENV_RELEASE}}
|
DOTENV: ${{secrets.DOTENV_RELEASE}}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: Spotube-Release-Binaries
|
name: ${{matrix.platform}}-${{matrix.arch}}
|
||||||
path: ${{matrix.files}}
|
path: ${{matrix.files}}
|
||||||
|
|
||||||
- name: Debug With SSH When fails
|
- name: Debug With SSH When fails
|
||||||
@ -130,14 +127,13 @@ jobs:
|
|||||||
limit-access-to-actor: true
|
limit-access-to-actor: true
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build_platform
|
- build_platform
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Spotube-Release-Binaries
|
|
||||||
path: ./Spotube-Release-Binaries
|
path: ./Spotube-Release-Binaries
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@ -146,18 +142,19 @@ jobs:
|
|||||||
- name: Generate Checksums
|
- name: Generate Checksums
|
||||||
run: |
|
run: |
|
||||||
tree .
|
tree .
|
||||||
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
|
find Spotube-Release-Binaries -type f -exec md5sum {} \; >> RELEASE.md5sum
|
||||||
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
find Spotube-Release-Binaries -type f -exec sha256sum {} \; >> RELEASE.sha256sum
|
||||||
|
sed -i 's|Spotube-Release-Binaries/.*/\([^/]*\)$|\1|' RELEASE.sha256sum RELEASE.md5sum
|
||||||
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
|
||||||
|
|
||||||
- name: Extract pubspec version
|
- name: Extract pubspec version
|
||||||
run: |
|
run: |
|
||||||
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
|
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: Spotube-Release-Binaries
|
name: sums
|
||||||
path: |
|
path: |
|
||||||
RELEASE.md5sum
|
RELEASE.md5sum
|
||||||
RELEASE.sha256sum
|
RELEASE.sha256sum
|
||||||
@ -172,7 +169,7 @@ jobs:
|
|||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
artifacts: Spotube-Release-Binaries/**/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||||
|
|
||||||
- name: Upload Release Binaries (nightly)
|
- name: Upload Release Binaries (nightly)
|
||||||
if: ${{ !inputs.dry_run && inputs.channel == 'nightly' }}
|
if: ${{ !inputs.dry_run && inputs.channel == 'nightly' }}
|
||||||
@ -184,9 +181,15 @@ jobs:
|
|||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
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: |
|
body: |
|
||||||
Build Number: ${{github.run_number}}
|
Build Number: ${{github.run_number}}
|
||||||
|
|
||||||
Nightly release includes newest features but may contain bugs
|
Nightly release includes newest features but may contain bugs
|
||||||
It is preferred to use the stable version unless you know what you're doing
|
It is preferred to use the stable version unless you know what you're doing
|
||||||
|
|
||||||
|
- name: Debug With SSH When fails
|
||||||
|
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||||
|
uses: mxschmitt/action-tmate@v3
|
||||||
|
with:
|
||||||
|
limit-access-to-actor: true
|
||||||
|
5
.gitignore
vendored
@ -79,4 +79,7 @@ android/key.properties
|
|||||||
tm.json
|
tm.json
|
||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/
|
||||||
|
|
||||||
|
android/build
|
||||||
|
android/app/.cxx
|
||||||
|
11
.vscode/launch.json
vendored
@ -30,6 +30,17 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "lib/main.dart",
|
"program": "lib/main.dart",
|
||||||
"flutterMode": "release"
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spotube (mobile) (release)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "release",
|
||||||
|
"args": [
|
||||||
|
"--flavor",
|
||||||
|
"dev"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compounds": []
|
"compounds": []
|
||||||
|
3
.vscode/settings.json
vendored
@ -13,6 +13,7 @@
|
|||||||
"RGBO",
|
"RGBO",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
"Scrobblenaut",
|
"Scrobblenaut",
|
||||||
|
"shadcn",
|
||||||
"skeletonizer",
|
"skeletonizer",
|
||||||
"songlink",
|
"songlink",
|
||||||
"speechiness",
|
"speechiness",
|
||||||
@ -27,5 +28,5 @@
|
|||||||
"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": "${capture}.g.dart,${capture}.freezed.dart"
|
||||||
},
|
},
|
||||||
"dart.flutterSdkPath": ".fvm/flutter_sdk"
|
"dart.flutterSdkPath": ".fvm/versions/3.29.0"
|
||||||
}
|
}
|
1156
CHANGELOG.md
6
LICENSE
@ -1,12 +1,12 @@
|
|||||||
BSD-4-Clause License
|
BSD-4-Clause License
|
||||||
|
|
||||||
Copyright (c) 2023 Kingkor Roy Tirtho. All rights reserved.
|
Copyright (c) 2025 Kingkor Roy Tirtho. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
||||||
This product includes software developed by Kingkor Roy Tirtho.
|
This product includes software developed by Kingkor Roy Tirtho.
|
||||||
4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
9
Makefile
@ -45,4 +45,11 @@ gensums:
|
|||||||
sh -c scripts/gensums.sh
|
sh -c scripts/gensums.sh
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
dart run drift_dev make-migrations
|
dart run drift_dev make-migrations
|
||||||
|
|
||||||
|
dmg:
|
||||||
|
flutter build macos &&\
|
||||||
|
if [ -f dist/Spotube-macos-universal.dmg ];\
|
||||||
|
then rm dist/Spotube-macos-universal.dmg;\
|
||||||
|
fi &&\
|
||||||
|
appdmg appdmg.json dist/Spotube-macos-universal.dmg
|
40
README.md
@ -110,7 +110,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>AppImage's lacking stability led to it's temporal removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
<td>AppImage's lacking stability led to it's temporary removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Debian/Ubuntu</td>
|
<td>Debian/Ubuntu</td>
|
||||||
@ -207,10 +207,15 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
### Services
|
### Services
|
||||||
|
|
||||||
1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
|
1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
|
||||||
|
1. [MPV](https://mpv.io) - mpv is a free (as in freedom) media player for the command line. It supports a wide variety of media file formats, audio and video codecs, and subtitle types.
|
||||||
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
|
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
|
||||||
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. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube.
|
||||||
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. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader
|
||||||
|
1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites
|
||||||
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. [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. [LRCLib](https://lrclib.net/) - A public synced lyric API
|
||||||
@ -220,24 +225,23 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
|
1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
|
||||||
1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
|
1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
|
||||||
1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device
|
1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device
|
||||||
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. [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](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_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
||||||
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_route](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
|
||||||
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. [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. [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. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
1. [connectivity_plus](https://github.com/fluttercommunity/plus_plugins) - Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS.
|
||||||
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. [device_info_plus](https://github.com/fluttercommunity/plus_plugins) - 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://github.com/fluttercommunity/plus_plugins) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
|
||||||
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. [drift](https://drift.simonbinder.eu/) - Drift is a reactive library to store relational data in Dart and Flutter applications.
|
1. [drift](https://drift.simonbinder.eu/) - Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||||
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. [encrypt](https://pub.dev/packages/encrypt) - A set of high-level APIs over PointyCastle for two-way cryptography.
|
1. [encrypt](https://pub.dev/packages/encrypt) - A set of high-level APIs over PointyCastle for two-way cryptography.
|
||||||
@ -245,26 +249,25 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
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_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. [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_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
|
1. [flutter_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
|
||||||
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_form_builder](https://github.com/flutter-form-builder-ecosystem) - This package helps in creation of forms in Flutter by removing the boilerplate code, reusing validation, react to changes, and collect final user input.
|
||||||
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_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 reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
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_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_undraw](https://github.com/KRTirtho/flutter_undraw) - Undraw.co Illustrations for Flutter with customization options
|
||||||
|
1. [form_builder_validators](https://github.com/flutter-form-builder-ecosystem) - Form Builder Validators set of validators for FlutterFormBuilder. Provides common validators and a way to make your own.
|
||||||
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_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
|
||||||
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. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
|
||||||
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
|
|
||||||
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
1. [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. [home_widget](https://pub.dev/packages/home_widget) - A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS.
|
||||||
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 reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
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. [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_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.
|
||||||
@ -276,6 +279,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
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. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
|
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. [logging](https://pub.dev/packages/logging) - Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger.
|
||||||
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. [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](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
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. [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.
|
||||||
@ -288,16 +292,16 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
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. [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. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
1. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
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. [shadcn_flutter](https://github.com/sunarya-thito/shadcn_flutter) - Beautifully designed components from Shadcn/UI is now available for Flutter
|
||||||
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](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
|
1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
|
||||||
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
|
1. [shelf_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_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
|
||||||
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. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
|
||||||
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
||||||
|
1. [sliding_up_panel](https://github.com/akshathjain/sliding_up_panel) - A draggable Flutter widget that makes implementing a SlidingUpPanel much easier!
|
||||||
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
||||||
1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
1. [smtc_windows](https://pub.dev/packages/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.
|
||||||
@ -319,26 +323,30 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
||||||
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
|
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. [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. [http_parser](https://pub.dev/packages/http_parser) - A platform-independent package for parsing and serializing HTTP formats.
|
||||||
|
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
||||||
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
||||||
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
|
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
|
||||||
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
||||||
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_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_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_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. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
||||||
1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
|
1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
|
||||||
1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
|
|
||||||
1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
|
|
||||||
1. [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. [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. [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. [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. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
||||||
1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
|
1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
|
||||||
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. [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. [drift_dev](https://drift.simonbinder.eu/) - Dev-dependency for users of drift. Contains the generator and development tools.
|
1. [drift_dev](https://drift.simonbinder.eu/) - Dev-dependency for users of drift. Contains the generator and development tools.
|
||||||
|
1. [auto_route_generator](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
|
||||||
1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application.
|
1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application.
|
||||||
|
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. [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. [flutter_broadcasts](https://github.com/KRTirtho/flutter_broadcasts.git) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
|
||||||
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. [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. [yt_dlp_dart](https://github.com/KRTirtho/yt_dlp_dart.git) - yt-dlp binding in Dart
|
||||||
|
1. [flutter_new_pipe_extractor](https://github.com/KRTirtho/flutter_new_pipe_extractor) - NewPipeExtractor binding for Flutter (Android only)
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
||||||
|
@ -32,8 +32,6 @@ linter:
|
|||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
invalid_annotation_target: ignore
|
invalid_annotation_target: ignore
|
||||||
plugins:
|
|
||||||
- custom_lint
|
|
||||||
exclude:
|
exclude:
|
||||||
- "**.freezed.dart"
|
- "**.freezed.dart"
|
||||||
- "**.g.dart"
|
- "**.g.dart"
|
||||||
|
@ -28,12 +28,17 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
def composeVersion = "1.4.8"
|
||||||
compileSdkVersion 34
|
|
||||||
|
|
||||||
ndkVersion "25.1.8937393"
|
android {
|
||||||
|
namespace "oss.krtirtho.spotube"
|
||||||
|
|
||||||
|
compileSdkVersion 35
|
||||||
|
|
||||||
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@ -46,10 +51,18 @@ android {
|
|||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion "$composeVersion" // Correlates with org.jetbrains.kotlin.android plugin in settings.gradle
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "oss.krtirtho.spotube"
|
applicationId "oss.krtirtho.spotube"
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdkVersion 34
|
targetSdkVersion 35
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@ -63,6 +76,7 @@ android {
|
|||||||
storePassword keystoreProperties['storePassword']
|
storePassword keystoreProperties['storePassword']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
@ -96,15 +110,30 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes += "DebugProbesKt.bin"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def glanceVersion = "1.1.1"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
|
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
// other deps so just ignore
|
// other deps so just ignore
|
||||||
implementation 'com.android.support:multidex:2.0.1'
|
implementation 'com.android.support:multidex:2.0.1'
|
||||||
|
|
||||||
|
implementation "androidx.glance:glance-appwidget:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-appwidget-preview:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-preview:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-material3:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-material:$glanceVersion"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||||
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
}
|
}
|
22
android/app/proguard-rules.pro
vendored
@ -1 +1,21 @@
|
|||||||
-keep class androidx.lifecycle.DefaultLifecycleObserver
|
-keep class androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
|
||||||
|
-keepnames class kotlinx.serialization.** { *; }
|
||||||
|
-keepnames class oss.krtirtho.spotube.glance.models.** { *; }
|
||||||
|
-keep @kotlinx.serialization.Serializable class *
|
||||||
|
-keepclassmembers class ** {
|
||||||
|
@kotlinx.serialization.* <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
## We don't need beans
|
||||||
|
-dontwarn java.beans.BeanDescriptor
|
||||||
|
-dontwarn java.beans.BeanInfo
|
||||||
|
-dontwarn java.beans.IntrospectionException
|
||||||
|
-dontwarn java.beans.Introspector
|
||||||
|
-dontwarn java.beans.PropertyDescriptor
|
||||||
|
|
||||||
|
## Rules for NewPipeExtractor
|
||||||
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
@ -1,7 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="oss.krtirtho.spotube">
|
<!-- Flutter needs it to communicate with the running application
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
</manifest>
|
</manifest>
|
@ -1,4 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<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" />
|
||||||
@ -17,38 +17,36 @@
|
|||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name="${applicationName}"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="false"
|
android:fullBackupContent="false"
|
||||||
android:label="@string/app_name_en"
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
android:label="@string/app_name_en"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
android:usesCleartextTraffic="true">
|
||||||
<!-- Enable Impeller -->
|
<!-- Enable Impeller -->
|
||||||
<!-- <meta-data
|
<!-- <meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
android:value="true" /> -->
|
android:value="false" /> -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:windowSoftInputMode="adjustResize">
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
>
|
|
||||||
<!--
|
<!--
|
||||||
Specifies an Android theme to apply to this Activity as soon as
|
Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI.
|
to determine the Window background behind the Flutter UI.
|
||||||
-->
|
-->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -56,12 +54,13 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:scheme="https"
|
|
||||||
android:host="open.spotify.com"
|
android:host="open.spotify.com"
|
||||||
/>
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -72,23 +71,30 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<!-- Accepts URIs that begin with "spotify:// -->
|
<!-- Accepts URIs that begin with "spotify:// -->
|
||||||
<data android:scheme="spotify" />
|
<data android:scheme="spotify" />
|
||||||
|
<data android:scheme="spotube" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- AudioService Config -->
|
<!-- AudioService Config -->
|
||||||
<service android:name="com.ryanheise.audioservice.AudioService"
|
<service
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:name="com.ryanheise.audioservice.AudioService"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
<receiver
|
||||||
|
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
@ -96,11 +102,40 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
<!-- =================== -->
|
<!-- =================== -->
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.car.application"
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.car.application"
|
||||||
android:resource="@xml/automotive_app_desc" />
|
android:resource="@xml/automotive_app_desc" />
|
||||||
|
|
||||||
|
<!-- Home Widget config -->
|
||||||
|
<receiver
|
||||||
|
android:name=".glance.HomePlayerWidgetReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/home_player_widget_config" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
<!-- =================== -->
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -0,0 +1,207 @@
|
|||||||
|
package oss.krtirtho.spotube.glance
|
||||||
|
|
||||||
|
import HomeWidgetGlanceState
|
||||||
|
import HomeWidgetGlanceStateDefinition
|
||||||
|
import android.R
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.action.ActionParameters
|
||||||
|
import androidx.glance.action.actionParametersOf
|
||||||
|
import androidx.glance.action.clickable
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidget
|
||||||
|
import androidx.glance.appwidget.SizeMode
|
||||||
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
|
import androidx.glance.appwidget.action.actionRunCallback
|
||||||
|
import androidx.glance.appwidget.background
|
||||||
|
import androidx.glance.appwidget.components.CircleIconButton
|
||||||
|
import androidx.glance.appwidget.components.Scaffold
|
||||||
|
import androidx.glance.appwidget.cornerRadius
|
||||||
|
import androidx.glance.appwidget.provideContent
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.currentState
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Box
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ContentScale
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.fillMaxSize
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
||||||
|
import androidx.glance.preview.Preview
|
||||||
|
import androidx.glance.state.GlanceStateDefinition
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
|
||||||
|
import es.antonborri.home_widget.actionStartActivity
|
||||||
|
import oss.krtirtho.spotube.MainActivity
|
||||||
|
import oss.krtirtho.spotube.glance.models.Track
|
||||||
|
import oss.krtirtho.spotube.glance.widgets.FlutterAssetImageProvider
|
||||||
|
import oss.krtirtho.spotube.glance.widgets.TrackDetailsView
|
||||||
|
import oss.krtirtho.spotube.glance.widgets.TrackProgress
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val serverAddressKey = ActionParameters.Key<String>("serverAddress")
|
||||||
|
|
||||||
|
class Breakpoints {
|
||||||
|
companion object {
|
||||||
|
val SMALL_SQUARE = DpSize(100.dp, 100.dp)
|
||||||
|
val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
|
||||||
|
val BIG_SQUARE = DpSize(250.dp, 250.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomePlayerWidget : GlanceAppWidget() {
|
||||||
|
|
||||||
|
override val sizeMode = SizeMode.Responsive(
|
||||||
|
setOf(
|
||||||
|
Breakpoints.SMALL_SQUARE,
|
||||||
|
Breakpoints.HORIZONTAL_RECTANGLE,
|
||||||
|
Breakpoints.BIG_SQUARE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val stateDefinition: GlanceStateDefinition<*>?
|
||||||
|
get() = HomeWidgetGlanceStateDefinition()
|
||||||
|
|
||||||
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||||
|
provideContent {
|
||||||
|
GlanceContent(context, currentState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||||
|
@Preview(widthDp = 100, heightDp = 100)
|
||||||
|
@Composable
|
||||||
|
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
||||||
|
val prefs = currentState.preferences
|
||||||
|
val size = LocalSize.current
|
||||||
|
|
||||||
|
val activeTrackStr = prefs.getString("activeTrack", null)
|
||||||
|
|
||||||
|
val isPlaying = prefs.getBoolean("isPlaying", false)
|
||||||
|
val playbackServerAddress = prefs.getString("playbackServerAddress", null) ?: ""
|
||||||
|
|
||||||
|
var activeTrack: Track? = null
|
||||||
|
if (activeTrackStr != null) {
|
||||||
|
activeTrack = gson.fromJson(activeTrackStr, Track::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val playIcon = Icon.createWithResource(context, R.drawable.ic_media_play);
|
||||||
|
val pauseIcon = Icon.createWithResource(context, R.drawable.ic_media_pause);
|
||||||
|
val previousIcon = Icon.createWithResource(context, R.drawable.ic_media_previous);
|
||||||
|
val nextIcon = Icon.createWithResource(context, R.drawable.ic_media_next);
|
||||||
|
|
||||||
|
GlanceTheme {
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.cornerRadius(8.dp)
|
||||||
|
.background(
|
||||||
|
color = GlanceTheme.colors.surface.getColor(context)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
actionStartActivity<MainActivity>(context)
|
||||||
|
}
|
||||||
|
,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.background(
|
||||||
|
color =
|
||||||
|
GlanceTheme.colors.surface.getColor(context)
|
||||||
|
.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) {}
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
|
||||||
|
TrackDetailsView(activeTrack)
|
||||||
|
}
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
if (size != Breakpoints.SMALL_SQUARE) {
|
||||||
|
TrackProgress(prefs)
|
||||||
|
}
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider = ImageProvider(previousIcon),
|
||||||
|
contentDescription = "Previous",
|
||||||
|
onClick = actionRunCallback<PreviousAction>(
|
||||||
|
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider =
|
||||||
|
if (isPlaying) ImageProvider(pauseIcon)
|
||||||
|
else ImageProvider(playIcon),
|
||||||
|
contentDescription = "Play/Pause",
|
||||||
|
onClick = actionRunCallback<PlayPauseAction>(
|
||||||
|
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider = ImageProvider(nextIcon),
|
||||||
|
contentDescription = "Previous",
|
||||||
|
onClick = actionRunCallback<NextAction>(
|
||||||
|
parameters = actionParametersOf(
|
||||||
|
serverAddressKey to playbackServerAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayPauseAction : InteractiveAction("toggle-playback")
|
||||||
|
class NextAction : InteractiveAction("next")
|
||||||
|
class PreviousAction : InteractiveAction("previous")
|
||||||
|
|
||||||
|
|
||||||
|
abstract class InteractiveAction(val command: String) : ActionCallback {
|
||||||
|
override suspend fun onAction(
|
||||||
|
context: Context,
|
||||||
|
glanceId: GlanceId,
|
||||||
|
parameters: ActionParameters
|
||||||
|
) {
|
||||||
|
val serverAddress = parameters[serverAddressKey] ?: ""
|
||||||
|
|
||||||
|
Log.d("HomePlayerWidget", "Sending command $command to $serverAddress")
|
||||||
|
|
||||||
|
if (serverAddress == null || serverAddress.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
Uri.parse("spotube://playback/$command?serverAddress=$serverAddress")
|
||||||
|
)
|
||||||
|
backgroundIntent.send()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package oss.krtirtho.spotube.glance
|
||||||
|
|
||||||
|
import HomeWidgetGlanceWidgetReceiver
|
||||||
|
|
||||||
|
class HomePlayerWidgetReceiver : HomeWidgetGlanceWidgetReceiver<HomePlayerWidget>() {
|
||||||
|
override val glanceAppWidget = HomePlayerWidget()
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AlbumSimple(
|
||||||
|
@SerializedName("album_type")
|
||||||
|
val albumType: AlbumType?,
|
||||||
|
|
||||||
|
@SerializedName("available_markets")
|
||||||
|
val availableMarkets: List<Market>?,
|
||||||
|
|
||||||
|
val href: String?,
|
||||||
|
val id: String?,
|
||||||
|
val images: List<Image>?,
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
@SerializedName("release_date")
|
||||||
|
val releaseDate: String?,
|
||||||
|
|
||||||
|
@SerializedName("release_date_precision")
|
||||||
|
val releaseDatePrecision: DatePrecision?,
|
||||||
|
|
||||||
|
val type: String?,
|
||||||
|
val uri: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AlbumType {
|
||||||
|
album,
|
||||||
|
single,
|
||||||
|
compilation
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DatePrecision {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Artist(
|
||||||
|
val href: String?,
|
||||||
|
val id: String?,
|
||||||
|
val name: String?,
|
||||||
|
val type: String?,
|
||||||
|
val uri: String?,
|
||||||
|
|
||||||
|
val followers: Followers?,
|
||||||
|
val genres: List<String>?,
|
||||||
|
val images: List<Image>?,
|
||||||
|
|
||||||
|
@SerializedName("popularity")
|
||||||
|
val popularity: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Followers(
|
||||||
|
val total: Int?
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Image(
|
||||||
|
val height: Int?,
|
||||||
|
val width: Int?,
|
||||||
|
val path: String,
|
||||||
|
)
|
@ -0,0 +1,37 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Track(
|
||||||
|
val album: AlbumSimple?, val artists: List<Artist>?,
|
||||||
|
|
||||||
|
@SerializedName("available_markets") val availableMarkets: List<Market>?,
|
||||||
|
|
||||||
|
@SerializedName("disc_number") val discNumber: Int?,
|
||||||
|
|
||||||
|
@SerializedName("duration_ms") val durationMs: Int,
|
||||||
|
|
||||||
|
val explicit: Boolean?, val href: String?, val id: String?,
|
||||||
|
|
||||||
|
@SerializedName("is_playable") val isPlayable: Boolean?,
|
||||||
|
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
@SerializedName("popularity") val popularity: Int?,
|
||||||
|
|
||||||
|
@SerializedName("preview_url") val previewUrl: String?,
|
||||||
|
|
||||||
|
@SerializedName("track_number") val trackNumber: Int?,
|
||||||
|
|
||||||
|
val type: String?, val uri: String?
|
||||||
|
) {
|
||||||
|
val duration: kotlin.time.Duration
|
||||||
|
get() = durationMs.toLong().milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Market {
|
||||||
|
AD, AE, AF, AG, AI, AL, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BL, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, SS, ST, SV, SX, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, XK, YE, YT, ZA, ZM, ZW,
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun Base64ImageProvider(base64: String): ImageProvider {
|
||||||
|
var bytes = Base64.decode(base64, Base64.DEFAULT);
|
||||||
|
|
||||||
|
var bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size);
|
||||||
|
|
||||||
|
return ImageProvider(bitmap)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun FlutterAssetImageProvider(context: Context, path: String): ImageProvider {
|
||||||
|
var inputStream = context.assets.open("flutter_assets/$path")
|
||||||
|
|
||||||
|
return ImageProvider(
|
||||||
|
BitmapFactory.decodeStream(inputStream)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalContext
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.appwidget.cornerRadius
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ContentScale
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import oss.krtirtho.spotube.glance.Breakpoints
|
||||||
|
import oss.krtirtho.spotube.glance.models.Track
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TrackDetailsView(activeTrack: Track?) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val size = LocalSize.current
|
||||||
|
|
||||||
|
val artistStr = activeTrack?.artists?.map { it.name }?.joinToString(", ") ?: "<No Artist>"
|
||||||
|
val imgLocalPath = activeTrack?.album?.images?.get(0)?.path;
|
||||||
|
val title = activeTrack?.name ?: "<No Track>"
|
||||||
|
|
||||||
|
|
||||||
|
Image(
|
||||||
|
provider =
|
||||||
|
if (imgLocalPath == null)
|
||||||
|
ImageProvider(
|
||||||
|
BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
android.R.drawable.ic_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else ImageProvider(BitmapFactory.decodeFile(imgLocalPath)),
|
||||||
|
contentDescription = "Album Art",
|
||||||
|
modifier = GlanceModifier.cornerRadius(8.dp)
|
||||||
|
.size(
|
||||||
|
if (size.height < 200.dp) 50.dp
|
||||||
|
else 100.dp
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GlanceTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (size != Breakpoints.SMALL_SQUARE) {
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Text(
|
||||||
|
text = artistStr,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = GlanceTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.appwidget.LinearProgressIndicator
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import oss.krtirtho.spotube.glance.Breakpoints
|
||||||
|
|
||||||
|
fun Duration.format(): String {
|
||||||
|
return this.toComponents { hour, minutes, seconds, nanoseconds ->
|
||||||
|
var paddedSeconds = seconds.toString().padStart(2, '0')
|
||||||
|
var paddedMinutes = minutes.toString().padStart(2, '0')
|
||||||
|
var paddedHour = hour.toString().padStart(2, '0')
|
||||||
|
if (hour == 0L) {
|
||||||
|
"$paddedMinutes:$paddedSeconds"
|
||||||
|
} else {
|
||||||
|
"$paddedHour:$paddedMinutes:$paddedSeconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TrackProgress(prefs: SharedPreferences) {
|
||||||
|
val size = LocalSize.current
|
||||||
|
val position = prefs.getInt("position", 0).seconds
|
||||||
|
var duration = prefs.getInt("duration", 0).seconds
|
||||||
|
|
||||||
|
var progress = position.inWholeSeconds.toFloat() / max(duration.inWholeSeconds.toFloat(), 1.0f)
|
||||||
|
|
||||||
|
var textStyle =
|
||||||
|
TextStyle(
|
||||||
|
color = GlanceTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
Text(text = position.format(), style = textStyle)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = GlanceModifier.defaultWeight(),
|
||||||
|
color = GlanceTheme.colors.primary,
|
||||||
|
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Text(text = duration.format(), style = textStyle)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
color = GlanceTheme.colors.primary,
|
||||||
|
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
Text(text = position.format(), style = textStyle)
|
||||||
|
Spacer(modifier = GlanceModifier.defaultWeight())
|
||||||
|
Text(text = duration.format(), style = textStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
|
android:minWidth="100dp"
|
||||||
|
android:minHeight="100dp"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="10000">
|
||||||
|
</appwidget-provider>
|
@ -0,0 +1,27 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="762"
|
||||||
|
android:viewportHeight="762">
|
||||||
|
<path
|
||||||
|
android:pathData="M309.08,370.99L309.08,479.87C309.08,486.36 314.33,491.6 320.83,491.6C327.31,491.6 332.58,486.36 332.58,479.87L332.58,370.99C332.58,364.51 327.31,359.26 320.83,359.26C314.33,359.26 309.08,364.51 309.08,370.99Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="14"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M254.59,491.73L280.46,491.73L280.46,362.47C280.53,361.85 280.64,361.23 280.64,360.6C280.64,304.83 325.72,259.46 381.12,259.46C436.51,259.46 481.59,304.83 481.59,360.6C481.59,361.45 481.71,362.27 481.84,363.1L481.84,491.73L507.71,491.73C525.72,491.73 540.33,476.65 540.33,458.03L540.33,390.62C540.33,375.26 530.37,362.33 516.78,358.26C515.53,284.17 455.17,224.26 381.12,224.26C307.05,224.26 246.69,284.18 245.45,358.29C231.88,362.36 221.96,375.29 221.96,390.63L221.96,458.03C221.96,476.64 236.56,491.73 254.59,491.73Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M431.08,370.99L431.08,479.87C431.08,486.36 436.33,491.6 442.83,491.6C449.31,491.6 454.58,486.36 454.58,479.87L454.58,370.99C454.58,364.51 449.31,359.26 442.83,359.26C436.33,359.26 431.08,364.51 431.08,370.99Z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="14"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</vector>
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
|
</adaptive-icon>
|
@ -1,7 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="oss.krtirtho.spotube">
|
<!-- Flutter needs it to communicate with the running application
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
</manifest>
|
</manifest>
|
@ -15,4 +15,4 @@ subprojects {
|
|||||||
|
|
||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
#Fri Jun 23 08:50:38 CEST 2017
|
#Fri Dec 13 21:53:13 BDT 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
|
||||||
|
@ -18,8 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.2.1" apply false
|
id "com.android.application" version '8.7.0' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ':app'
|
BIN
assets/backgrounds/xmas-effect.png
Normal file
After Width: | Height: | Size: 176 KiB |
BIN
assets/patterns/black_white_visualized.jpg
Normal file
After Width: | Height: | Size: 336 KiB |
BIN
assets/patterns/brazil_carnival.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
assets/patterns/cotton_balls.jpg
Normal file
After Width: | Height: | Size: 498 KiB |
BIN
assets/patterns/cute_worms.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
assets/patterns/flash_cross_axis.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
assets/patterns/memphis_shapes.jpg
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
assets/patterns/oval_gloomy.jpg
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
assets/patterns/oval_sunny.jpg
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
assets/patterns/red_nimbuses.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
assets/patterns/tree_bark.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
assets/patterns/vibrant_pentagons.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
assets/patterns/wiring_pattern.jpg
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
assets/patterns/zigzags_gloomy.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
assets/patterns/zigzags_sunny.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
@ -1,8 +1,8 @@
|
|||||||
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 = 3.7.1
|
pkgver = 4.0.0
|
||||||
pkgrel = 2
|
pkgrel = 1
|
||||||
url = https://github.com/KRTirtho/spotube/
|
url = https://spotube.krtirtho.dev
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = BSD-4-Clause
|
license = BSD-4-Clause
|
||||||
depends = mpv
|
depends = mpv
|
||||||
@ -12,6 +12,7 @@ depends = jsoncpp
|
|||||||
depends = libnotify
|
depends = libnotify
|
||||||
depends = xdg-user-dirs
|
depends = xdg-user-dirs
|
||||||
depends = webkit2gtk-4.1
|
depends = webkit2gtk-4.1
|
||||||
|
optdepends = yt-dlp-git
|
||||||
source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
|
source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
|
||||||
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
||||||
|
|
||||||
|
@ -5,13 +5,13 @@ pkgrel=%{{PKGREL}}%
|
|||||||
epoch=
|
epoch=
|
||||||
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!"
|
||||||
arch=(x86_64)
|
arch=(x86_64)
|
||||||
url="https://github.com/KRTirtho/spotube/"
|
url="https://spotube.krtirtho.dev"
|
||||||
license=('BSD-4-Clause')
|
license=('BSD-4-Clause')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||||
makedepends=()
|
makedepends=()
|
||||||
checkdepends=()
|
checkdepends=()
|
||||||
optdepends=()
|
optdepends=('yt-dlp-git')
|
||||||
provides=()
|
provides=()
|
||||||
conflicts=()
|
conflicts=()
|
||||||
replaces=()
|
replaces=()
|
||||||
|
10
build.yaml
@ -4,6 +4,16 @@ targets:
|
|||||||
exclude:
|
exclude:
|
||||||
- bin/*.dart
|
- bin/*.dart
|
||||||
builders:
|
builders:
|
||||||
|
auto_route_generator:auto_route_generator: # this for @RoutePage
|
||||||
|
options:
|
||||||
|
enable_cached_builds: true
|
||||||
|
generate_for:
|
||||||
|
- lib/pages/**/*.dart
|
||||||
|
auto_route_generator:auto_router_generator: # this for @AutoRouterConfig
|
||||||
|
options:
|
||||||
|
enable_cached_builds: true
|
||||||
|
generate_for:
|
||||||
|
- lib/collections/routes.dart
|
||||||
json_serializable:
|
json_serializable:
|
||||||
options:
|
options:
|
||||||
any_map: true
|
any_map: true
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter
|
||||||
|
enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<!-- == PACKAGE SPECIFIC SECTION == -->
|
<!-- == PACKAGE SPECIFIC SECTION == -->
|
||||||
@ -12,34 +13,39 @@
|
|||||||
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
||||||
<title>spotube (Install)</title>
|
<title>spotube (Install)</title>
|
||||||
<authors>Kingkor Roy Tirtho</authors>
|
<authors>Kingkor Roy Tirtho</authors>
|
||||||
<projectUrl>https://github.com/KRTirtho/spotube/</projectUrl>
|
<projectUrl>https://spotube.krtirtho.dev</projectUrl>
|
||||||
<iconUrl>https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
<iconUrl>
|
||||||
|
https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
||||||
<copyright>2022 Spotube</copyright>
|
<copyright>2022 Spotube</copyright>
|
||||||
<!-- If there is a license Url available, it is required for the community feed -->
|
<!-- If there is a license Url available, it is required for the community feed -->
|
||||||
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
|
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
|
||||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||||
<projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl>
|
<projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl>
|
||||||
<docsUrl>https://github.com/KRTirtho/spotube#readme</docsUrl>
|
<docsUrl>https://spotube.krtirtho.dev</docsUrl>
|
||||||
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
|
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
|
||||||
<tags>spotube music audio spotify youtube flutter</tags>
|
<tags>spotube music audio spotify youtube flutter</tags>
|
||||||
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile! </summary>
|
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available
|
||||||
|
for both desktop & mobile! </summary>
|
||||||
<description>
|
<description>
|
||||||
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
||||||
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
||||||
friendly User Experience
|
friendly User Experience
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
- Open source/libre software
|
- Open source/libre software
|
||||||
- Anonymous/guest login
|
- Anonymous/guest login
|
||||||
- Cross platform support
|
- Cross platform support
|
||||||
- No telemetry, diagnostics or user data collection
|
- No telemetry, diagnostics or user data collection
|
||||||
- Lightweight & resource-friendly
|
- Lightweight & resource-friendly
|
||||||
- Native performance (Thanks to Flutter+Skia)
|
- Native performance (Thanks to Flutter+Skia)
|
||||||
- Playback control is done locally instead of on the server
|
- Playback control is done locally instead of on the server
|
||||||
- Small size & less data usage
|
- Small size & less data usage
|
||||||
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support their valuable creations.)
|
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended
|
||||||
- Time synced lyrics
|
to support the creators by watching/liking/subscribing to the artists' YouTube channels or
|
||||||
- Downloadable tracks
|
liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support
|
||||||
|
their valuable creations.)
|
||||||
|
- Time synced lyrics
|
||||||
|
- Downloadable tracks
|
||||||
</description>
|
</description>
|
||||||
<releaseNotes>https://github.com/KRTirtho/spotube/releases/tag/v%{{SPOTUBE_VERSION}}%</releaseNotes>
|
<releaseNotes>https://github.com/KRTirtho/spotube/releases/tag/v%{{SPOTUBE_VERSION}}%</releaseNotes>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
BSD 4-Clause License
|
BSD 4-Clause License
|
||||||
|
|
||||||
Copyright (c) 2022 Kingkor Roy Tirtho. All rights reserved.
|
Copyright (c) 2025 Kingkor Roy Tirtho. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import 'package:args/command_runner.dart';
|
|||||||
import 'build/android.dart';
|
import 'build/android.dart';
|
||||||
import 'build/ios.dart';
|
import 'build/ios.dart';
|
||||||
import 'build/linux.dart';
|
import 'build/linux.dart';
|
||||||
import 'build/linux_arm.dart';
|
|
||||||
import 'build/macos.dart';
|
import 'build/macos.dart';
|
||||||
import 'build/windows.dart';
|
import 'build/windows.dart';
|
||||||
|
|
||||||
@ -18,8 +17,13 @@ class BuildCommand extends Command {
|
|||||||
addSubcommand(AndroidBuildCommand());
|
addSubcommand(AndroidBuildCommand());
|
||||||
addSubcommand(IosBuildCommand());
|
addSubcommand(IosBuildCommand());
|
||||||
addSubcommand(LinuxBuildCommand());
|
addSubcommand(LinuxBuildCommand());
|
||||||
addSubcommand(LinuxArmBuildCommand());
|
|
||||||
addSubcommand(MacosBuildCommand());
|
addSubcommand(MacosBuildCommand());
|
||||||
addSubcommand(WindowsBuildCommand());
|
addSubcommand(WindowsBuildCommand());
|
||||||
|
argParser.addOption(
|
||||||
|
"arch",
|
||||||
|
abbr: "a",
|
||||||
|
defaultsTo: "x86",
|
||||||
|
allowed: ["x86", "arm64", "all"],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,4 +63,6 @@ mixin BuildCommandCommonSteps on Command {
|
|||||||
""",
|
""",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get architecture => parent?.argResults?.option("arch") as String;
|
||||||
}
|
}
|
||||||
|
@ -37,23 +37,32 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
|||||||
await bootstrap();
|
await bootstrap();
|
||||||
|
|
||||||
await shell.run(
|
await shell.run(
|
||||||
"""
|
"flutter_distributor package --platform=linux --targets=deb",
|
||||||
flutter_distributor package --platform=linux --targets=deb
|
|
||||||
flutter_distributor package --platform=linux --targets=rpm
|
|
||||||
""",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final tempDir = join(Directory.systemTemp.path, "spotube-tar");
|
if (architecture == "x86") {
|
||||||
|
await shell.run(
|
||||||
|
"flutter_distributor package --platform=linux --targets=rpm",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final bundleDirPath =
|
final tempDir = join(Directory.systemTemp.path, "spotube-tar");
|
||||||
join(cwd.path, "build", "linux", "x64", "release", "bundle");
|
final bundleArchName = architecture == "x86" ? "x86_64" : "aarch64";
|
||||||
|
final bundleDirPath = join(
|
||||||
|
cwd.path,
|
||||||
|
"build",
|
||||||
|
"linux",
|
||||||
|
architecture == "x86" ? "x64" : architecture,
|
||||||
|
"release",
|
||||||
|
"bundle",
|
||||||
|
);
|
||||||
|
|
||||||
final tarFile = File(join(
|
final tarFile = File(join(
|
||||||
cwd.path,
|
cwd.path,
|
||||||
"dist",
|
"dist",
|
||||||
"spotube-linux-"
|
"spotube-linux-"
|
||||||
"${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}"
|
"${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}"
|
||||||
"-x86_64.tar.xz",
|
"-$bundleArchName.tar.xz",
|
||||||
));
|
));
|
||||||
|
|
||||||
await copyPath(bundleDirPath, tempDir);
|
await copyPath(bundleDirPath, tempDir);
|
||||||
@ -81,25 +90,31 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
|||||||
"spotube-${pubspec.version}-linux.deb",
|
"spotube-${pubspec.version}-linux.deb",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await ogDeb.copy(
|
||||||
final ogRpm = File(
|
|
||||||
join(
|
join(
|
||||||
cwd.path,
|
cwd.path,
|
||||||
"dist",
|
"dist",
|
||||||
pubspec.version.toString(),
|
"Spotube-linux-$bundleArchName.deb",
|
||||||
"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 ogDeb.delete();
|
||||||
await ogRpm.delete();
|
|
||||||
|
if (architecture == "x86") {
|
||||||
|
final ogRpm = File(
|
||||||
|
join(
|
||||||
|
cwd.path,
|
||||||
|
"dist",
|
||||||
|
pubspec.version.toString(),
|
||||||
|
"spotube-${pubspec.version}-linux.rpm",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await ogRpm.copy(
|
||||||
|
join(cwd.path, "dist", "Spotube-linux-$bundleArchName.rpm"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await ogRpm.delete();
|
||||||
|
}
|
||||||
|
|
||||||
stdout.writeln("✅ Linux building done");
|
stdout.writeln("✅ Linux building done");
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import '../../core/env.dart';
|
|
||||||
import 'common.dart';
|
|
||||||
|
|
||||||
class LinuxArmBuildCommand extends Command with BuildCommandCommonSteps {
|
|
||||||
@override
|
|
||||||
String get description => "Build Linux Arm";
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name => "linux_arm";
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr? run() async {
|
|
||||||
await bootstrap();
|
|
||||||
|
|
||||||
await shell.run(
|
|
||||||
"docker buildx build --platform=linux/arm64 "
|
|
||||||
"-f ${join(cwd.path, ".github", "Dockerfile")} ${cwd.path} "
|
|
||||||
"--build-arg FLUTTER_VERSION=${CliEnv.flutterVersion} "
|
|
||||||
"--build-arg BUILD_VERSION=${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber} "
|
|
||||||
"-t krtirtho/spotube_linux_arm:latest "
|
|
||||||
"--load",
|
|
||||||
);
|
|
||||||
|
|
||||||
await shell.run(
|
|
||||||
"""
|
|
||||||
docker images ls
|
|
||||||
docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest
|
|
||||||
docker cp spotube_linux_arm:/app/dist/ dist/
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,6 +24,13 @@ class InstallDependenciesCommand extends Command {
|
|||||||
],
|
],
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
argParser.addOption(
|
||||||
|
"arch",
|
||||||
|
abbr: "a",
|
||||||
|
allowed: ["x86", "arm64", "all"],
|
||||||
|
defaultsTo: "x86",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,14 +48,6 @@ class InstallDependenciesCommand extends Command {
|
|||||||
""",
|
""",
|
||||||
);
|
);
|
||||||
break;
|
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":
|
case "macos":
|
||||||
await shell.run(
|
await shell.run(
|
||||||
"""
|
"""
|
||||||
|
1
drift_schemas/app_db/drift_schema_v4.json
Normal file
@ -1,13 +0,0 @@
|
|||||||
flutter_launcher_icons:
|
|
||||||
ios: true
|
|
||||||
android: true
|
|
||||||
image_path: "assets/spotube-logo.png"
|
|
||||||
adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg"
|
|
||||||
adaptive_icon_background: "#242832"
|
|
||||||
windows:
|
|
||||||
generate: true
|
|
||||||
image_path: "assets/spotube-logo.png"
|
|
||||||
icon_size: 48 # min:48, max:256, default: 48
|
|
||||||
macos:
|
|
||||||
generate: true
|
|
||||||
image_path: "assets/spotube-logo-macos.png"
|
|
29
flutter_launcher_icons.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# flutter pub run flutter_launcher_icons
|
||||||
|
flutter_launcher_icons:
|
||||||
|
image_path: "assets/spotube-logo.png"
|
||||||
|
|
||||||
|
android: true
|
||||||
|
# image_path_android: "assets/icon/icon.png"
|
||||||
|
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||||
|
adaptive_icon_background: "#242832"
|
||||||
|
adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg"
|
||||||
|
# adaptive_icon_monochrome: "assets/icon/monochrome.png"
|
||||||
|
|
||||||
|
ios: true
|
||||||
|
# image_path_ios: "assets/icon/icon.png"
|
||||||
|
remove_alpha_channel_ios: true
|
||||||
|
# image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
|
||||||
|
# image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
|
||||||
|
# desaturate_tinted_to_grayscale_ios: true
|
||||||
|
|
||||||
|
web:
|
||||||
|
generate: false
|
||||||
|
|
||||||
|
windows:
|
||||||
|
generate: true
|
||||||
|
image_path: "assets/spotube-logo.png"
|
||||||
|
icon_size: 48 # min:48, max:256, default: 48
|
||||||
|
|
||||||
|
macos:
|
||||||
|
generate: true
|
||||||
|
image_path: "assets/spotube-logo-macos.png"
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
6
ios/HomePlayerWidget/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
86
ios/HomePlayerWidget/HomePlayerWidget.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// HomePlayerWidget.swift
|
||||||
|
// HomePlayerWidget
|
||||||
|
//
|
||||||
|
// Created by Kingkor Roy Tirtho on 15/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private let widgetGroupId = "group.spotube_home_player_widget"
|
||||||
|
|
||||||
|
struct Provider: TimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> SimpleEntry {
|
||||||
|
SimpleEntry(date: Date(), emoji: "😀")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||||
|
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||||
|
var entries: [SimpleEntry] = []
|
||||||
|
|
||||||
|
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||||
|
let currentDate = Date()
|
||||||
|
for hourOffset in 0 ..< 5 {
|
||||||
|
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||||
|
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||||
|
entries.append(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||||
|
completion(timeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func relevances() async -> WidgetRelevances<Void> {
|
||||||
|
// // Generate a list containing the contexts this widget is relevant in.
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let emoji: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomePlayerWidgetEntryView : View {
|
||||||
|
var entry: Provider.Entry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Time:")
|
||||||
|
Text(entry.date, style: .time)
|
||||||
|
|
||||||
|
Text("Emoji:")
|
||||||
|
Text(entry.emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomePlayerWidget: Widget {
|
||||||
|
let kind: String = "HomePlayerWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
HomePlayerWidgetEntryView(entry: entry)
|
||||||
|
.containerBackground(.fill.tertiary, for: .widget)
|
||||||
|
} else {
|
||||||
|
HomePlayerWidgetEntryView(entry: entry)
|
||||||
|
.padding()
|
||||||
|
.background()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.configurationDisplayName("My Widget")
|
||||||
|
.description("This is an example widget.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemSmall) {
|
||||||
|
HomePlayerWidget()
|
||||||
|
} timeline: {
|
||||||
|
SimpleEntry(date: .now, emoji: "😀")
|
||||||
|
SimpleEntry(date: .now, emoji: "🤩")
|
||||||
|
}
|
16
ios/HomePlayerWidget/HomePlayerWidgetBundle.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// HomePlayerWidgetBundle.swift
|
||||||
|
// HomePlayerWidget
|
||||||
|
//
|
||||||
|
// Created by Kingkor Roy Tirtho on 15/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct HomePlayerWidgetBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
HomePlayerWidget()
|
||||||
|
}
|
||||||
|
}
|
11
ios/HomePlayerWidget/Info.plist
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
10
ios/HomePlayerWidgetExtension.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.spotube_home_player_widget</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -64,6 +64,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_sharing_intent (0.0.1):
|
- flutter_sharing_intent (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- home_widget (0.0.1):
|
||||||
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
@ -106,12 +108,15 @@ PODS:
|
|||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- sqlite3 (~> 3.47.0)
|
- FlutterMacOS
|
||||||
|
- sqlite3 (~> 3.47.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- SwiftyGif (5.4.4)
|
- SwiftyGif (5.4.4)
|
||||||
|
- system_theme (0.0.1):
|
||||||
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
@ -130,6 +135,7 @@ DEPENDENCIES:
|
|||||||
- 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`)
|
||||||
|
- home_widget (from `.symlinks/plugins/home_widget/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`)
|
||||||
@ -141,7 +147,8 @@ DEPENDENCIES:
|
|||||||
- 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_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
|
- system_theme (from `.symlinks/plugins/system_theme/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:
|
||||||
@ -182,6 +189,8 @@ EXTERNAL SOURCES:
|
|||||||
: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"
|
||||||
|
home_widget:
|
||||||
|
:path: ".symlinks/plugins/home_widget/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:
|
||||||
@ -205,7 +214,9 @@ EXTERNAL SOURCES:
|
|||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
|
system_theme:
|
||||||
|
:path: ".symlinks/plugins/system_theme/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
@ -226,6 +237,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
||||||
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||||
@ -240,8 +252,9 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: 1e522f0938463e44b7faf50393b40bdc1e1e456d
|
sqlite3: 1e522f0938463e44b7faf50393b40bdc1e1e456d
|
||||||
sqlite3_flutter_libs: b55ef23cfafea5318ae5081e0bf3fbbce8417c94
|
sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3
|
||||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||||
|
system_theme: bfc1b0913d08f38d8c6bbe94b202a58df599d9f7
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@main
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
// Add this to get Documents directory path
|
||||||
|
if let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path {
|
||||||
|
UserDefaults.standard.set(documentsPath, forKey: "download_path")
|
||||||
|
}
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-nightly-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-nightly-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-nightly-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-nightly-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
@ -72,5 +72,11 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>_spotube._tcp</string>
|
<string>_spotube._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
|
<key>UISupportsDocumentBrowser</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
10
ios/Runner/Runner.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.spotube_home_player_widget</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
10
ios/dev.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.spotube_home_player_widget</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
10
ios/nightly.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.spotube_home_player_widget</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
10
ios/stable.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.spotube_home_player_widget</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -9,6 +9,17 @@
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class $AssetsBackgroundsGen {
|
||||||
|
const $AssetsBackgroundsGen();
|
||||||
|
|
||||||
|
/// File path: assets/backgrounds/xmas-effect.png
|
||||||
|
AssetGenImage get xmasEffect =>
|
||||||
|
const AssetGenImage('assets/backgrounds/xmas-effect.png');
|
||||||
|
|
||||||
|
/// List of all assets
|
||||||
|
List<AssetGenImage> get values => [xmasEffect];
|
||||||
|
}
|
||||||
|
|
||||||
class $AssetsLogosGen {
|
class $AssetsLogosGen {
|
||||||
const $AssetsLogosGen();
|
const $AssetsLogosGen();
|
||||||
|
|
||||||
@ -24,6 +35,84 @@ class $AssetsLogosGen {
|
|||||||
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $AssetsPatternsGen {
|
||||||
|
const $AssetsPatternsGen();
|
||||||
|
|
||||||
|
/// File path: assets/patterns/black_white_visualized.jpg
|
||||||
|
AssetGenImage get blackWhiteVisualized =>
|
||||||
|
const AssetGenImage('assets/patterns/black_white_visualized.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/brazil_carnival.jpg
|
||||||
|
AssetGenImage get brazilCarnival =>
|
||||||
|
const AssetGenImage('assets/patterns/brazil_carnival.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/cotton_balls.jpg
|
||||||
|
AssetGenImage get cottonBalls =>
|
||||||
|
const AssetGenImage('assets/patterns/cotton_balls.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/cute_worms.jpg
|
||||||
|
AssetGenImage get cuteWorms =>
|
||||||
|
const AssetGenImage('assets/patterns/cute_worms.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/flash_cross_axis.jpg
|
||||||
|
AssetGenImage get flashCrossAxis =>
|
||||||
|
const AssetGenImage('assets/patterns/flash_cross_axis.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/memphis_shapes.jpg
|
||||||
|
AssetGenImage get memphisShapes =>
|
||||||
|
const AssetGenImage('assets/patterns/memphis_shapes.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/oval_gloomy.jpg
|
||||||
|
AssetGenImage get ovalGloomy =>
|
||||||
|
const AssetGenImage('assets/patterns/oval_gloomy.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/oval_sunny.jpg
|
||||||
|
AssetGenImage get ovalSunny =>
|
||||||
|
const AssetGenImage('assets/patterns/oval_sunny.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/red_nimbuses.jpg
|
||||||
|
AssetGenImage get redNimbuses =>
|
||||||
|
const AssetGenImage('assets/patterns/red_nimbuses.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/tree_bark.jpg
|
||||||
|
AssetGenImage get treeBark =>
|
||||||
|
const AssetGenImage('assets/patterns/tree_bark.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/vibrant_pentagons.jpg
|
||||||
|
AssetGenImage get vibrantPentagons =>
|
||||||
|
const AssetGenImage('assets/patterns/vibrant_pentagons.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/wiring_pattern.jpg
|
||||||
|
AssetGenImage get wiringPattern =>
|
||||||
|
const AssetGenImage('assets/patterns/wiring_pattern.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/zigzags_gloomy.jpg
|
||||||
|
AssetGenImage get zigzagsGloomy =>
|
||||||
|
const AssetGenImage('assets/patterns/zigzags_gloomy.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/zigzags_sunny.jpg
|
||||||
|
AssetGenImage get zigzagsSunny =>
|
||||||
|
const AssetGenImage('assets/patterns/zigzags_sunny.jpg');
|
||||||
|
|
||||||
|
/// List of all assets
|
||||||
|
List<AssetGenImage> get values => [
|
||||||
|
blackWhiteVisualized,
|
||||||
|
brazilCarnival,
|
||||||
|
cottonBalls,
|
||||||
|
cuteWorms,
|
||||||
|
flashCrossAxis,
|
||||||
|
memphisShapes,
|
||||||
|
ovalGloomy,
|
||||||
|
ovalSunny,
|
||||||
|
redNimbuses,
|
||||||
|
treeBark,
|
||||||
|
vibrantPentagons,
|
||||||
|
wiringPattern,
|
||||||
|
zigzagsGloomy,
|
||||||
|
zigzagsSunny
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
class $AssetsTutorialGen {
|
class $AssetsTutorialGen {
|
||||||
const $AssetsTutorialGen();
|
const $AssetsTutorialGen();
|
||||||
|
|
||||||
@ -46,6 +135,7 @@ class Assets {
|
|||||||
static const String license = 'LICENSE';
|
static const String license = 'LICENSE';
|
||||||
static const AssetGenImage albumPlaceholder =
|
static const AssetGenImage albumPlaceholder =
|
||||||
AssetGenImage('assets/album-placeholder.png');
|
AssetGenImage('assets/album-placeholder.png');
|
||||||
|
static const $AssetsBackgroundsGen backgrounds = $AssetsBackgroundsGen();
|
||||||
static const AssetGenImage bengaliPatternsBg =
|
static const AssetGenImage bengaliPatternsBg =
|
||||||
AssetGenImage('assets/bengali-patterns-bg.jpg');
|
AssetGenImage('assets/bengali-patterns-bg.jpg');
|
||||||
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
|
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
|
||||||
@ -55,12 +145,15 @@ class Assets {
|
|||||||
static const AssetGenImage likedTracks =
|
static const AssetGenImage likedTracks =
|
||||||
AssetGenImage('assets/liked-tracks.jpg');
|
AssetGenImage('assets/liked-tracks.jpg');
|
||||||
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||||
|
static const $AssetsPatternsGen patterns = $AssetsPatternsGen();
|
||||||
static const AssetGenImage placeholder =
|
static const AssetGenImage placeholder =
|
||||||
AssetGenImage('assets/placeholder.png');
|
AssetGenImage('assets/placeholder.png');
|
||||||
static const AssetGenImage spotubeHeroBanner =
|
static const AssetGenImage spotubeHeroBanner =
|
||||||
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 spotubeLogoMacos =
|
||||||
|
AssetGenImage('assets/spotube-logo-macos.png');
|
||||||
static const AssetGenImage spotubeLogoBmp =
|
static const AssetGenImage spotubeLogoBmp =
|
||||||
AssetGenImage('assets/spotube-logo.bmp');
|
AssetGenImage('assets/spotube-logo.bmp');
|
||||||
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
|
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
|
||||||
@ -104,6 +197,7 @@ class Assets {
|
|||||||
placeholder,
|
placeholder,
|
||||||
spotubeHeroBanner,
|
spotubeHeroBanner,
|
||||||
spotubeLogoForeground,
|
spotubeLogoForeground,
|
||||||
|
spotubeLogoMacos,
|
||||||
spotubeLogoBmp,
|
spotubeLogoBmp,
|
||||||
spotubeLogoIco,
|
spotubeLogoIco,
|
||||||
spotubeLogoPng,
|
spotubeLogoPng,
|
||||||
|
@ -38,6 +38,11 @@ abstract class Env {
|
|||||||
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
||||||
static final String _releaseChannel = _Env._releaseChannel;
|
static final String _releaseChannel = _Env._releaseChannel;
|
||||||
|
|
||||||
|
@EnviedField(varName: "DISABLE_SPOTIFY_IMAGES", defaultValue: "0")
|
||||||
|
static final String _disableSpotifyImages = _Env._disableSpotifyImages;
|
||||||
|
|
||||||
|
static bool get disableSpotifyImages => _disableSpotifyImages == "1";
|
||||||
|
|
||||||
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
||||||
? ReleaseChannel.stable
|
? ReleaseChannel.stable
|
||||||
: ReleaseChannel.nightly;
|
: ReleaseChannel.nightly;
|
||||||
|
24
lib/collections/fonts.gen.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
/// *****************************************************
|
||||||
|
/// FlutterGen
|
||||||
|
/// *****************************************************
|
||||||
|
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
|
||||||
|
|
||||||
|
class FontFamily {
|
||||||
|
FontFamily._();
|
||||||
|
|
||||||
|
/// Font family: BootstrapIcons
|
||||||
|
static const String bootstrapIcons = 'BootstrapIcons';
|
||||||
|
|
||||||
|
/// Font family: GeistMono
|
||||||
|
static const String geistMono = 'GeistMono';
|
||||||
|
|
||||||
|
/// Font family: GeistSans
|
||||||
|
static const String geistSans = 'GeistSans';
|
||||||
|
|
||||||
|
/// Font family: RadixIcons
|
||||||
|
static const String radixIcons = 'RadixIcons';
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
const gradients = [
|
const gradients = [
|
||||||
LinearGradient(colors: [
|
LinearGradient(colors: [
|
||||||
|
@ -3,13 +3,9 @@ import 'dart:io';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/services.dart';
|
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:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/modules/player/player_controls.dart';
|
import 'package:spotube/modules/player/player_controls.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
|
||||||
import 'package:spotube/pages/library/library.dart';
|
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
|
||||||
import 'package:spotube/pages/search/search.dart';
|
|
||||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
import 'package:spotube/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';
|
||||||
@ -36,7 +32,7 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NavigationIntent extends Intent {
|
class NavigationIntent extends Intent {
|
||||||
final GoRouter router;
|
final AppRouter router;
|
||||||
final String path;
|
final String path;
|
||||||
const NavigationIntent(this.router, this.path);
|
const NavigationIntent(this.router, this.path);
|
||||||
}
|
}
|
||||||
@ -44,7 +40,7 @@ class NavigationIntent extends Intent {
|
|||||||
class NavigationAction extends Action<NavigationIntent> {
|
class NavigationAction extends Action<NavigationIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) {
|
invoke(intent) {
|
||||||
intent.router.go(intent.path);
|
intent.router.navigateNamed(intent.path);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,32 +48,49 @@ class NavigationAction extends Action<NavigationIntent> {
|
|||||||
enum HomeTabs {
|
enum HomeTabs {
|
||||||
browse,
|
browse,
|
||||||
search,
|
search,
|
||||||
library,
|
|
||||||
lyrics,
|
lyrics,
|
||||||
|
userPlaylists,
|
||||||
|
userArtists,
|
||||||
|
userAlbums,
|
||||||
|
userLocalLibrary,
|
||||||
|
userDownloads,
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeTabIntent extends Intent {
|
class HomeTabIntent extends Intent {
|
||||||
final WidgetRef ref;
|
final AppRouter router;
|
||||||
final HomeTabs tab;
|
final HomeTabs tab;
|
||||||
const HomeTabIntent(this.ref, {required this.tab});
|
const HomeTabIntent(this.router, {required this.tab});
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeTabAction extends Action<HomeTabIntent> {
|
class HomeTabAction extends Action<HomeTabIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) {
|
invoke(intent) {
|
||||||
final router = intent.ref.read(routerProvider);
|
final router = intent.router;
|
||||||
switch (intent.tab) {
|
switch (intent.tab) {
|
||||||
case HomeTabs.browse:
|
case HomeTabs.browse:
|
||||||
router.goNamed(HomePage.name);
|
router.navigate(const HomeRoute());
|
||||||
break;
|
break;
|
||||||
case HomeTabs.search:
|
case HomeTabs.search:
|
||||||
router.goNamed(SearchPage.name);
|
router.navigate(const SearchRoute());
|
||||||
break;
|
|
||||||
case HomeTabs.library:
|
|
||||||
router.goNamed(LibraryPage.name);
|
|
||||||
break;
|
break;
|
||||||
case HomeTabs.lyrics:
|
case HomeTabs.lyrics:
|
||||||
router.goNamed(LyricsPage.name);
|
router.navigate(LyricsRoute());
|
||||||
|
break;
|
||||||
|
case HomeTabs.userPlaylists:
|
||||||
|
router.navigate(const UserPlaylistsRoute());
|
||||||
|
break;
|
||||||
|
case HomeTabs.userArtists:
|
||||||
|
router.navigate(const UserArtistsRoute());
|
||||||
|
break;
|
||||||
|
case HomeTabs.userAlbums:
|
||||||
|
router.navigate(const UserAlbumsRoute());
|
||||||
|
break;
|
||||||
|
case HomeTabs.userLocalLibrary:
|
||||||
|
router.navigate(const UserLocalLibraryRoute());
|
||||||
|
break;
|
||||||
|
case HomeTabs.userDownloads:
|
||||||
|
router.navigate(const UserDownloadsRoute());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,329 +1,235 @@
|
|||||||
import 'package:flutter/foundation.dart' hide Category;
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Search;
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/models/spotify/recommendation_seeds.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/home/feed/feed_section.dart';
|
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
|
||||||
import 'package:spotube/pages/home/home.dart';
|
|
||||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
|
||||||
import 'package:spotube/pages/library/local_folder.dart';
|
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
|
||||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
|
||||||
import 'package:spotube/pages/playlist/liked_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/settings/blacklist.dart';
|
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
|
||||||
import 'package:spotube/pages/settings/logs.dart';
|
|
||||||
import 'package:spotube/pages/stats/albums/albums.dart';
|
|
||||||
import 'package:spotube/pages/stats/artists/artists.dart';
|
|
||||||
import 'package:spotube/pages/stats/fees/fees.dart';
|
|
||||||
import 'package:spotube/pages/stats/minutes/minutes.dart';
|
|
||||||
import 'package:spotube/pages/stats/playlists/playlists.dart';
|
|
||||||
import 'package:spotube/pages/stats/stats.dart';
|
|
||||||
import 'package:spotube/pages/stats/streams/streams.dart';
|
|
||||||
import 'package:spotube/pages/track/track.dart';
|
|
||||||
import 'package:spotube/provider/authentication/authentication.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/components/spotube_page_route.dart';
|
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
|
||||||
import 'package:spotube/pages/library/library.dart';
|
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
|
||||||
import 'package:spotube/pages/root/root_app.dart';
|
|
||||||
import 'package:spotube/pages/settings/settings.dart';
|
|
||||||
import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
|
||||||
|
|
||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
|
||||||
final routerProvider = Provider((ref) {
|
|
||||||
return GoRouter(
|
|
||||||
navigatorKey: rootNavigatorKey,
|
|
||||||
routes: [
|
|
||||||
ShellRoute(
|
|
||||||
navigatorKey: shellRouteNavigatorKey,
|
|
||||||
builder: (context, state, child) => RootApp(child: child),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "/",
|
|
||||||
name: HomePage.name,
|
|
||||||
redirect: (context, state) async {
|
|
||||||
final auth = await ref.read(authenticationProvider.future);
|
|
||||||
|
|
||||||
if (auth == null && !KVStoreService.doneGettingStarted) {
|
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
|
||||||
return "/getting-started";
|
class AppRouter extends RootStackRouter {
|
||||||
}
|
final WidgetRef ref;
|
||||||
|
|
||||||
return null;
|
AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
|
||||||
},
|
|
||||||
pageBuilder: (context, state) =>
|
@override
|
||||||
const SpotubePage(child: HomePage()),
|
List<AutoRoute> get routes => [
|
||||||
routes: [
|
AutoRoute(
|
||||||
GoRoute(
|
page: RootAppRoute.page,
|
||||||
path: "genres",
|
path: "/",
|
||||||
name: GenrePage.name,
|
initial: true,
|
||||||
pageBuilder: (context, state) =>
|
children: [
|
||||||
const SpotubePage(child: GenrePage()),
|
AutoRoute(
|
||||||
),
|
path: "home",
|
||||||
GoRoute(
|
page: HomeRoute.page,
|
||||||
path: "genre/:categoryId",
|
initial: true,
|
||||||
name: GenrePlaylistsPage.name,
|
guards: [
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
AutoRouteGuardCallback(
|
||||||
child: GenrePlaylistsPage(
|
(resolver, router) async {
|
||||||
category: state.extra as Category,
|
final auth = await ref.read(authenticationProvider.future);
|
||||||
),
|
|
||||||
),
|
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||||
),
|
resolver.redirect(const GettingStartedRoute());
|
||||||
GoRoute(
|
} else {
|
||||||
path: "feeds/:feedId",
|
resolver.next(true);
|
||||||
name: HomeFeedSectionPage.name,
|
}
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: HomeFeedSectionPage(
|
|
||||||
sectionUri: state.pathParameters["feedId"] as String,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/search",
|
|
||||||
name: SearchPage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: SearchPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/library",
|
|
||||||
name: LibraryPage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: LibraryPage()),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "generate",
|
|
||||||
name: PlaylistGeneratorPage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "result",
|
|
||||||
name: PlaylistGenerateResultPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: PlaylistGenerateResultPage(
|
|
||||||
state: state.extra as GeneratePlaylistProviderInput,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "local",
|
|
||||||
name: LocalLibraryPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.extra is String);
|
|
||||||
return SpotubePage(
|
|
||||||
child: LocalLibraryPage(
|
|
||||||
state.extra as String,
|
|
||||||
isDownloads:
|
|
||||||
state.uri.queryParameters["downloads"] != null,
|
|
||||||
isCache: state.uri.queryParameters["cache"] != null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
GoRoute(
|
|
||||||
path: "/lyrics",
|
|
||||||
name: LyricsPage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: LyricsPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/settings",
|
|
||||||
name: SettingsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: SettingsPage(),
|
|
||||||
),
|
),
|
||||||
routes: [
|
AutoRoute(
|
||||||
GoRoute(
|
path: "home/genres",
|
||||||
path: "blacklist",
|
page: GenreRoute.page,
|
||||||
name: BlackListPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
|
||||||
child: const BlackListPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!kIsWeb)
|
|
||||||
GoRoute(
|
|
||||||
path: "logs",
|
|
||||||
name: LogsPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
|
||||||
child: const LogsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "about",
|
|
||||||
name: AboutSpotube.name,
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
|
||||||
child: const AboutSpotube(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/album/:id",
|
|
||||||
name: AlbumPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.extra is AlbumSimple);
|
|
||||||
return SpotubePage(
|
|
||||||
child: AlbumPage(album: state.extra as AlbumSimple),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/artist/:id",
|
|
||||||
name: ArtistPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.pathParameters["id"] != null);
|
|
||||||
return SpotubePage(
|
|
||||||
child: ArtistPage(state.pathParameters["id"]!));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/playlist/:id",
|
|
||||||
name: PlaylistPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.extra is PlaylistSimple);
|
|
||||||
return SpotubePage(
|
|
||||||
child: state.pathParameters["id"] == "user-liked-tracks"
|
|
||||||
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
|
|
||||||
: PlaylistPage(playlist: state.extra as PlaylistSimple),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/track/:id",
|
|
||||||
name: TrackPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
final id = state.pathParameters["id"]!;
|
|
||||||
return SpotubePage(
|
|
||||||
child: TrackPage(trackId: id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/connect",
|
|
||||||
name: ConnectPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: ConnectPage(),
|
|
||||||
),
|
),
|
||||||
routes: [
|
AutoRoute(
|
||||||
GoRoute(
|
path: "home/genre/:categoryId",
|
||||||
path: "control",
|
page: GenrePlaylistsRoute.page,
|
||||||
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: [
|
AutoRoute(
|
||||||
GoRoute(
|
path: "home/feeds/:feedId",
|
||||||
path: "minutes",
|
page: HomeFeedSectionRoute.page,
|
||||||
name: StatsMinutesPage.name,
|
),
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
AutoRoute(
|
||||||
child: StatsMinutesPage(),
|
path: "search",
|
||||||
|
page: SearchRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "library",
|
||||||
|
page: LibraryRoute.page,
|
||||||
|
children: [
|
||||||
|
AutoRoute(
|
||||||
|
path: "playlists",
|
||||||
|
page: UserPlaylistsRoute.page,
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
path: "artists",
|
||||||
path: "streams",
|
page: UserArtistsRoute.page,
|
||||||
name: StatsStreamsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsStreamsPage(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
path: "albums",
|
||||||
path: "fees",
|
page: UserAlbumsRoute.page,
|
||||||
name: StatsStreamFeesPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsStreamFeesPage(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
path: "local",
|
||||||
path: "artists",
|
page: UserLocalLibraryRoute.page,
|
||||||
name: StatsArtistsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsArtistsPage(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
path: "local/folder",
|
||||||
path: "albums",
|
page: LocalLibraryRoute.page,
|
||||||
name: StatsAlbumsPage.name,
|
// parentNavigatorKey: shellRouteNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsAlbumsPage(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
path: "downloads",
|
||||||
path: "playlists",
|
page: UserDownloadsRoute.page,
|
||||||
name: StatsPlaylistsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsPlaylistsPage(),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "library/generate",
|
||||||
|
page: PlaylistGeneratorRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "library/generate/result",
|
||||||
|
page: PlaylistGenerateResultRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "lyrics",
|
||||||
|
page: LyricsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "settings",
|
||||||
|
page: SettingsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "settings/blacklist",
|
||||||
|
page: BlackListRoute.page,
|
||||||
|
),
|
||||||
|
if (!kIsWeb)
|
||||||
|
AutoRoute(
|
||||||
|
path: "settings/logs",
|
||||||
|
page: LogsRoute.page,
|
||||||
),
|
),
|
||||||
],
|
AutoRoute(
|
||||||
)
|
path: "settings/about",
|
||||||
],
|
page: AboutSpotubeRoute.page,
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/mini-player",
|
path: "album/:id",
|
||||||
name: MiniLyricsPage.name,
|
page: AlbumRoute.page,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
),
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
AutoRoute(
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
path: "artist/:id",
|
||||||
|
page: ArtistRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "liked-tracks",
|
||||||
|
page: LikedPlaylistRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "playlist/:id",
|
||||||
|
page: PlaylistRoute.page,
|
||||||
|
guards: [
|
||||||
|
AutoRouteGuard.redirect(
|
||||||
|
(resolver) {
|
||||||
|
final PlaylistRouteArgs(:id, :playlist) =
|
||||||
|
resolver.route.args as PlaylistRouteArgs;
|
||||||
|
if (id == "user-liked-tracks") {
|
||||||
|
return LikedPlaylistRoute(playlist: playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "track/:id",
|
||||||
|
page: TrackRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "connect",
|
||||||
|
page: ConnectRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "connect/control",
|
||||||
|
page: ConnectControlRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "profile",
|
||||||
|
page: ProfileRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats",
|
||||||
|
page: StatsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/minutes",
|
||||||
|
page: StatsMinutesRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/streams",
|
||||||
|
page: StatsStreamsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/fees",
|
||||||
|
page: StatsStreamFeesRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/artists",
|
||||||
|
page: StatsArtistsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/albums",
|
||||||
|
page: StatsAlbumsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/playlists",
|
||||||
|
page: StatsPlaylistsRoute.page,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
CustomRoute(
|
||||||
GoRoute(
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
path: "/getting-started",
|
durationInMilliseconds: 200,
|
||||||
name: GettingStarting.name,
|
reverseDurationInMilliseconds: 200,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
path: "/player/queue",
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
page: PlayerQueueRoute.page,
|
||||||
child: GettingStarting(),
|
|
||||||
),
|
),
|
||||||
),
|
CustomRoute(
|
||||||
GoRoute(
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
path: "/login",
|
durationInMilliseconds: 200,
|
||||||
name: WebViewLogin.name,
|
reverseDurationInMilliseconds: 200,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
path: "/player/sources",
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
page: PlayerTrackSourcesRoute.page,
|
||||||
child: WebViewLogin(),
|
|
||||||
),
|
),
|
||||||
),
|
CustomRoute(
|
||||||
GoRoute(
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
path: "/lastfm-login",
|
durationInMilliseconds: 200,
|
||||||
name: LastFMLoginPage.name,
|
reverseDurationInMilliseconds: 200,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
path: "/player/lyrics",
|
||||||
pageBuilder: (context, state) =>
|
page: PlayerLyricsRoute.page,
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
],
|
path: "/mini-player",
|
||||||
);
|
page: MiniLyricsRoute.page,
|
||||||
});
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "/getting-started",
|
||||||
|
page: GettingStartedRoute.page,
|
||||||
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "/login",
|
||||||
|
page: WebViewLoginRoute.page,
|
||||||
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "/lastfm-login",
|
||||||
|
page: LastFMLoginRoute.page,
|
||||||
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
1174
lib/collections/routes.gr.dart
Normal file
@ -1,81 +1,113 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
|
||||||
import 'package:spotube/pages/library/library.dart';
|
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
|
||||||
import 'package:spotube/pages/search/search.dart';
|
|
||||||
import 'package:spotube/pages/stats/stats.dart';
|
|
||||||
|
|
||||||
class SideBarTiles {
|
class SideBarTiles {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String pathPrefix;
|
||||||
|
final PageRouteInfo route;
|
||||||
|
|
||||||
SideBarTiles({
|
SideBarTiles({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.route,
|
||||||
|
required this.pathPrefix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "browse",
|
id: "home",
|
||||||
name: HomePage.name,
|
pathPrefix: "/home",
|
||||||
|
route: const HomeRoute(),
|
||||||
icon: SpotubeIcons.home,
|
icon: SpotubeIcons.home,
|
||||||
title: l10n.browse,
|
title: l10n.browse,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "search",
|
id: "search",
|
||||||
name: SearchPage.name,
|
pathPrefix: "/search",
|
||||||
|
route: const SearchRoute(),
|
||||||
icon: SpotubeIcons.search,
|
icon: SpotubeIcons.search,
|
||||||
title: l10n.search,
|
title: l10n.search,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
|
||||||
id: "library",
|
|
||||||
name: LibraryPage.name,
|
|
||||||
icon: SpotubeIcons.library,
|
|
||||||
title: l10n.library,
|
|
||||||
),
|
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "lyrics",
|
id: "lyrics",
|
||||||
name: LyricsPage.name,
|
pathPrefix: "/lyrics",
|
||||||
|
route: LyricsRoute(),
|
||||||
icon: SpotubeIcons.music,
|
icon: SpotubeIcons.music,
|
||||||
title: l10n.lyrics,
|
title: l10n.lyrics,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "stats",
|
id: "stats",
|
||||||
name: StatsPage.name,
|
pathPrefix: "/stats",
|
||||||
|
route: const StatsRoute(),
|
||||||
icon: SpotubeIcons.chart,
|
icon: SpotubeIcons.chart,
|
||||||
title: l10n.stats,
|
title: l10n.stats,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
List<SideBarTiles> getSidebarLibraryTileList(AppLocalizations l10n) => [
|
||||||
|
SideBarTiles(
|
||||||
|
id: "playlists",
|
||||||
|
pathPrefix: "/library/playlists",
|
||||||
|
title: l10n.playlists,
|
||||||
|
route: const UserPlaylistsRoute(),
|
||||||
|
icon: SpotubeIcons.playlist,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "artists",
|
||||||
|
pathPrefix: "/library/artists",
|
||||||
|
title: l10n.artists,
|
||||||
|
route: const UserArtistsRoute(),
|
||||||
|
icon: SpotubeIcons.artist,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "albums",
|
||||||
|
pathPrefix: "/library/albums",
|
||||||
|
title: l10n.albums,
|
||||||
|
route: const UserAlbumsRoute(),
|
||||||
|
icon: SpotubeIcons.album,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "local_library",
|
||||||
|
pathPrefix: "/library/local",
|
||||||
|
title: l10n.local_library,
|
||||||
|
route: const UserLocalLibraryRoute(),
|
||||||
|
icon: SpotubeIcons.device,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "browse",
|
id: "home",
|
||||||
name: HomePage.name,
|
pathPrefix: "/home",
|
||||||
|
route: const HomeRoute(),
|
||||||
icon: SpotubeIcons.home,
|
icon: SpotubeIcons.home,
|
||||||
title: l10n.browse,
|
title: l10n.browse,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "search",
|
id: "search",
|
||||||
name: SearchPage.name,
|
pathPrefix: "/search",
|
||||||
|
route: const SearchRoute(),
|
||||||
icon: SpotubeIcons.search,
|
icon: SpotubeIcons.search,
|
||||||
title: l10n.search,
|
title: l10n.search,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library",
|
id: "library",
|
||||||
name: LibraryPage.name,
|
pathPrefix: "/library",
|
||||||
|
route: const UserPlaylistsRoute(),
|
||||||
icon: SpotubeIcons.library,
|
icon: SpotubeIcons.library,
|
||||||
title: l10n.library,
|
title: l10n.library,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "stats",
|
id: "stats",
|
||||||
name: StatsPage.name,
|
pathPrefix: "/stats",
|
||||||
|
route: const StatsRoute(),
|
||||||
icon: SpotubeIcons.chart,
|
icon: SpotubeIcons.chart,
|
||||||
title: l10n.stats,
|
title: l10n.stats,
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||||
import 'package:simple_icons/simple_icons.dart';
|
import 'package:simple_icons/simple_icons.dart';
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ abstract class SpotubeIcons {
|
|||||||
static const share = FeatherIcons.share2;
|
static const share = FeatherIcons.share2;
|
||||||
static const playlistAdd = Icons.playlist_add_rounded;
|
static const playlistAdd = Icons.playlist_add_rounded;
|
||||||
static const playlistRemove = Icons.playlist_remove_rounded;
|
static const playlistRemove = Icons.playlist_remove_rounded;
|
||||||
|
static const playlist = Icons.playlist_play_rounded;
|
||||||
static const trash = FeatherIcons.trash2;
|
static const trash = FeatherIcons.trash2;
|
||||||
static const clock = FeatherIcons.clock;
|
static const clock = FeatherIcons.clock;
|
||||||
static const lyrics = Icons.lyrics_rounded;
|
static const lyrics = Icons.lyrics_rounded;
|
||||||
@ -127,4 +128,11 @@ abstract class SpotubeIcons {
|
|||||||
static const cache = FeatherIcons.hardDrive;
|
static const cache = FeatherIcons.hardDrive;
|
||||||
static const export = Icons.file_open_outlined;
|
static const export = Icons.file_open_outlined;
|
||||||
static const delete = FeatherIcons.trash2;
|
static const delete = FeatherIcons.trash2;
|
||||||
|
static const open = FeatherIcons.externalLink;
|
||||||
|
static const radioChecked = Icons.radio_button_on_rounded;
|
||||||
|
static const radioUnchecked = Icons.radio_button_off_rounded;
|
||||||
|
static const grid = FeatherIcons.grid;
|
||||||
|
static const list = FeatherIcons.list;
|
||||||
|
static const device = FeatherIcons.smartphone;
|
||||||
|
static const engine = FeatherIcons.server;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
class AdaptiveListTile extends HookWidget {
|
class AdaptiveListTile extends HookWidget {
|
||||||
@ -24,41 +25,39 @@ class AdaptiveListTile extends HookWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
return ListTile(
|
return ButtonTile(
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
trailing: breakOn ?? mediaQuery.smAndDown
|
trailing: breakOn ?? mediaQuery.smAndDown
|
||||||
? null
|
? null
|
||||||
: trailing?.call(context, null),
|
: trailing?.call(context, null),
|
||||||
leading: leading,
|
leading: leading,
|
||||||
onTap: breakOn ?? mediaQuery.smAndDown
|
enabled: breakOn ?? mediaQuery.smAndDown,
|
||||||
? () {
|
onPressed: () {
|
||||||
onTap?.call();
|
onTap?.call();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StatefulBuilder(builder: (context, update) {
|
return StatefulBuilder(builder: (context, update) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: title != null
|
title: title != null
|
||||||
? Row(
|
? Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
spacing: 5,
|
||||||
if (leading != null) ...[
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
leading!,
|
children: [
|
||||||
const SizedBox(width: 5)
|
if (leading != null) leading!,
|
||||||
],
|
Flexible(child: title!),
|
||||||
Flexible(child: title!),
|
],
|
||||||
],
|
)
|
||||||
)
|
: const SizedBox.shrink(),
|
||||||
: Container(),
|
content: Center(child: trailing?.call(context, update)),
|
||||||
content: trailing?.call(context, update),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
: null,
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' show showModalBottomSheet;
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
_emptyCB() {}
|
class AdaptiveMenuButton<T> extends MenuButton {
|
||||||
|
|
||||||
class PopSheetEntry<T> extends ListTile {
|
|
||||||
final T? value;
|
final T? value;
|
||||||
const PopSheetEntry({
|
const AdaptiveMenuButton({
|
||||||
this.value,
|
|
||||||
super.key,
|
super.key,
|
||||||
super.leading,
|
this.value,
|
||||||
super.title,
|
required super.child,
|
||||||
super.subtitle,
|
super.subMenu,
|
||||||
|
super.onPressed,
|
||||||
super.trailing,
|
super.trailing,
|
||||||
super.isThreeLine = false,
|
super.leading,
|
||||||
super.dense,
|
|
||||||
super.visualDensity,
|
|
||||||
super.shape,
|
|
||||||
super.style,
|
|
||||||
super.selectedColor,
|
|
||||||
super.iconColor,
|
|
||||||
super.textColor,
|
|
||||||
super.titleTextStyle,
|
|
||||||
super.subtitleTextStyle,
|
|
||||||
super.leadingAndTrailingTextStyle,
|
|
||||||
super.contentPadding,
|
|
||||||
super.enabled = true,
|
super.enabled = true,
|
||||||
super.onTap = _emptyCB,
|
|
||||||
super.onLongPress,
|
|
||||||
super.onFocusChange,
|
|
||||||
super.mouseCursor,
|
|
||||||
super.selected = false,
|
|
||||||
super.focusColor,
|
|
||||||
super.hoverColor,
|
|
||||||
super.splashColor,
|
|
||||||
super.focusNode,
|
super.focusNode,
|
||||||
super.autofocus = false,
|
super.autoClose = true,
|
||||||
super.tileColor,
|
super.popoverController,
|
||||||
super.selectedTileColor,
|
}) : assert(
|
||||||
super.enableFeedback,
|
value != null || onPressed != null,
|
||||||
super.horizontalTitleGap,
|
'Either value or onPressed must be provided',
|
||||||
super.minVerticalPadding,
|
);
|
||||||
super.minLeadingWidth,
|
|
||||||
super.titleAlignment,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above
|
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above
|
||||||
/// or equal to 640px
|
/// or equal to 640px
|
||||||
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
|
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
|
||||||
class AdaptivePopSheetList<T> extends StatelessWidget {
|
class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||||
final List<PopSheetEntry<T>> children;
|
final List<AdaptiveMenuButton<T>> children;
|
||||||
final Widget? icon;
|
final Widget? icon;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final bool useRootNavigator;
|
final bool useRootNavigator;
|
||||||
|
|
||||||
final List<Widget>? headings;
|
final List<Widget>? headings;
|
||||||
final String? tooltip;
|
final String tooltip;
|
||||||
final ValueChanged<T>? onSelected;
|
final ValueChanged<T>? onSelected;
|
||||||
|
|
||||||
final BorderRadius borderRadius;
|
|
||||||
final Offset offset;
|
final Offset offset;
|
||||||
|
|
||||||
|
final ButtonVariance variance;
|
||||||
|
|
||||||
const AdaptivePopSheetList({
|
const AdaptivePopSheetList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.children,
|
required this.children,
|
||||||
@ -70,166 +49,141 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
this.useRootNavigator = true,
|
this.useRootNavigator = true,
|
||||||
this.headings,
|
this.headings,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
this.borderRadius = const BorderRadius.all(Radius.circular(999)),
|
required this.tooltip,
|
||||||
this.tooltip,
|
|
||||||
this.offset = Offset.zero,
|
this.offset = Offset.zero,
|
||||||
|
this.variance = ButtonVariance.ghost,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
!(icon != null && child != null),
|
!(icon != null && child != null),
|
||||||
'Either icon or child must be provided',
|
'Either icon or child must be provided',
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<T?> showPopupMenu(BuildContext context, RelativeRect position) {
|
Future<void> showDropdownMenu(BuildContext context, Offset position) async {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final childrenModified = children.map((s) {
|
||||||
|
if (s.onPressed == null) {
|
||||||
|
return MenuButton(
|
||||||
|
key: s.key,
|
||||||
|
autoClose: s.autoClose,
|
||||||
|
enabled: s.enabled,
|
||||||
|
leading: s.leading,
|
||||||
|
focusNode: s.focusNode,
|
||||||
|
onPressed: (context) {
|
||||||
|
if (s.value != null) {
|
||||||
|
onSelected?.call(s.value as T);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popoverController: s.popoverController,
|
||||||
|
subMenu: s.subMenu,
|
||||||
|
trailing: s.trailing,
|
||||||
|
child: s.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
return showMenu<T>(
|
if (mediaQuery.mdAndUp) {
|
||||||
|
await showDropdown<T?>(
|
||||||
|
context: context,
|
||||||
|
rootOverlay: useRootNavigator,
|
||||||
|
// heightConstraint: PopoverConstraint.anchorFixedSize,
|
||||||
|
// constraints: BoxConstraints(
|
||||||
|
// maxHeight: mediaQuery.size.height * 0.6,
|
||||||
|
// ),
|
||||||
|
position: position,
|
||||||
|
builder: (context) {
|
||||||
|
return DropdownMenu(
|
||||||
|
children: childrenModified,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).future;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: useRootNavigator,
|
enableDrag: true,
|
||||||
constraints: BoxConstraints(
|
showDragHandle: true,
|
||||||
maxHeight: mediaQuery.size.height * 0.6,
|
useRootNavigator: true,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
),
|
),
|
||||||
position: position,
|
backgroundColor: context.theme.colorScheme.card,
|
||||||
items: children
|
builder: (context) {
|
||||||
.map(
|
return ListView.builder(
|
||||||
(item) => PopupMenuItem<T>(
|
itemCount: childrenModified.length,
|
||||||
padding: EdgeInsets.zero,
|
shrinkWrap: true,
|
||||||
enabled: false,
|
itemBuilder: (context, index) {
|
||||||
child: _AdaptivePopSheetListItem<T>(
|
final data = childrenModified[index];
|
||||||
item: item,
|
|
||||||
onSelected: onSelected,
|
return Button(
|
||||||
|
enabled: data.enabled,
|
||||||
|
style: ButtonVariance.ghost.copyWith(
|
||||||
|
padding: (context, state, value) => const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
),
|
onPressed: () {
|
||||||
)
|
data.onPressed?.call(context);
|
||||||
.toList(),
|
if (data.autoClose) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leading: data.leading,
|
||||||
|
trailing: data.trailing,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: data.child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (mediaQuery.mdAndUp) {
|
if (mediaQuery.mdAndUp) {
|
||||||
return PopupMenuButton(
|
return Tooltip(
|
||||||
icon: icon,
|
tooltip: TooltipContainer(
|
||||||
tooltip: tooltip,
|
child: Text(tooltip),
|
||||||
offset: offset,
|
|
||||||
child: child == null ? null : IgnorePointer(child: child),
|
|
||||||
itemBuilder: (context) => children
|
|
||||||
.map(
|
|
||||||
(item) => PopupMenuItem(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
enabled: false,
|
|
||||||
child: _AdaptivePopSheetListItem(
|
|
||||||
item: item,
|
|
||||||
onSelected: onSelected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
useRootNavigator: useRootNavigator,
|
|
||||||
isScrollControlled: true,
|
|
||||||
showDragHandle: true,
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: mediaQuery.size.height * 0.6,
|
|
||||||
),
|
),
|
||||||
builder: (context) {
|
child: IconButton(
|
||||||
return Padding(
|
variance: variance,
|
||||||
padding: const EdgeInsets.all(8.0).copyWith(top: 0),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
child: DefaultTextStyle(
|
onPressed: () {
|
||||||
style: theme.textTheme.titleMedium!,
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
child: SingleChildScrollView(
|
final position = RelativeRect.fromRect(
|
||||||
child: Column(
|
Rect.fromPoints(
|
||||||
mainAxisSize: MainAxisSize.min,
|
renderBox.localToGlobal(Offset.zero,
|
||||||
children: [
|
ancestor: context.findRenderObject()),
|
||||||
if (headings != null) ...[
|
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero),
|
||||||
...headings!,
|
ancestor: context.findRenderObject()),
|
||||||
const SizedBox(height: 8),
|
|
||||||
Divider(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
thickness: 0.3,
|
|
||||||
endIndent: 16,
|
|
||||||
indent: 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
...children.map(
|
|
||||||
(item) => _AdaptivePopSheetListItem(
|
|
||||||
item: item,
|
|
||||||
onSelected: onSelected,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
Offset.zero & mediaQuery.size,
|
||||||
);
|
);
|
||||||
},
|
final offset = Offset(position.left, position.top);
|
||||||
|
showDropdownMenu(context, offset);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: tooltip ?? '',
|
tooltip: TooltipContainer(child: Text(tooltip)),
|
||||||
child: InkWell(
|
child: Button(
|
||||||
onTap: showSheet,
|
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||||
borderRadius: borderRadius,
|
style: variance,
|
||||||
child: IgnorePointer(child: child),
|
child: IgnorePointer(child: child),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IconButton(
|
return Tooltip(
|
||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
tooltip: TooltipContainer(child: Text(tooltip)),
|
||||||
tooltip: tooltip,
|
child: IconButton(
|
||||||
style: theme.iconButtonTheme.style?.copyWith(
|
variance: variance,
|
||||||
shape: WidgetStatePropertyAll(
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
RoundedRectangleBorder(
|
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||||
borderRadius: borderRadius,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: showSheet,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AdaptivePopSheetListItem<T> extends StatelessWidget {
|
|
||||||
final PopSheetEntry<T> item;
|
|
||||||
final ValueChanged<T>? onSelected;
|
|
||||||
const _AdaptivePopSheetListItem({
|
|
||||||
super.key,
|
|
||||||
required this.item,
|
|
||||||
this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?)
|
|
||||||
?.borderRadius as BorderRadius? ??
|
|
||||||
const BorderRadius.all(Radius.circular(10)),
|
|
||||||
onTap: !item.enabled
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
item.onTap?.call();
|
|
||||||
if (item.value != null) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
onSelected?.call(item.value as T);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: IconTheme.merge(
|
|
||||||
data: const IconThemeData(opacity: 1),
|
|
||||||
child: IgnorePointer(child: item),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
|
|
||||||
import 'package:popover/popover.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
|
||||||
|
|
||||||
class Action extends StatelessWidget {
|
|
||||||
final Widget text;
|
|
||||||
final Widget icon;
|
|
||||||
final void Function() onPressed;
|
|
||||||
final bool isExpanded;
|
|
||||||
final Color? backgroundColor;
|
|
||||||
const Action({
|
|
||||||
super.key,
|
|
||||||
required this.icon,
|
|
||||||
required this.text,
|
|
||||||
required this.onPressed,
|
|
||||||
this.isExpanded = true,
|
|
||||||
this.backgroundColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (isExpanded != true) {
|
|
||||||
return IconButton(
|
|
||||||
icon: icon,
|
|
||||||
onPressed: onPressed,
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
),
|
|
||||||
tooltip: text is Text
|
|
||||||
? (text as Text).data
|
|
||||||
: text.toStringShallow().split(",").last.replaceAll(
|
|
||||||
"\"",
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
tileColor: backgroundColor,
|
|
||||||
onTap: onPressed,
|
|
||||||
leading: icon,
|
|
||||||
title: text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdaptiveActions extends HookWidget {
|
|
||||||
final List<Action> actions;
|
|
||||||
final bool? breakOn;
|
|
||||||
const AdaptiveActions({
|
|
||||||
required this.actions,
|
|
||||||
this.breakOn,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
|
||||||
|
|
||||||
if (breakOn ?? mediaQuery.lgAndUp) {
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
|
||||||
onPressed: () {
|
|
||||||
showPopover(
|
|
||||||
context: context,
|
|
||||||
direction: PopoverDirection.left,
|
|
||||||
bodyBuilder: (context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions
|
|
||||||
.map(
|
|
||||||
(action) => SizedBox(
|
|
||||||
width: 200,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: action),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
backgroundColor: Theme.of(context).cardColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: actions.map((action) {
|
|
||||||
return Action(
|
|
||||||
icon: action.icon,
|
|
||||||
onPressed: action.onPressed,
|
|
||||||
text: action.text,
|
|
||||||
backgroundColor: action.backgroundColor,
|
|
||||||
isExpanded: false,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' show ListTile, ListTileControlAffinity;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
@ -7,11 +8,12 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
final Widget title;
|
final Widget title;
|
||||||
final Widget? subtitle;
|
final Widget? subtitle;
|
||||||
final Widget? secondary;
|
final Widget? secondary;
|
||||||
|
final List<Widget>? trailing;
|
||||||
final ListTileControlAffinity? controlAffinity;
|
final ListTileControlAffinity? controlAffinity;
|
||||||
final T value;
|
final T value;
|
||||||
final ValueChanged<T?>? onChanged;
|
final ValueChanged<T?>? onChanged;
|
||||||
|
|
||||||
final List<DropdownMenuItem<T>> options;
|
final List<SelectItemButton<T>> options;
|
||||||
|
|
||||||
/// Show the smaller value when the breakpoint is reached
|
/// Show the smaller value when the breakpoint is reached
|
||||||
///
|
///
|
||||||
@ -22,6 +24,9 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
|
|
||||||
final bool? breakLayout;
|
final bool? breakLayout;
|
||||||
|
|
||||||
|
final BoxConstraints? popupConstraints;
|
||||||
|
final PopoverConstraint? popupWidthConstraint;
|
||||||
|
|
||||||
const AdaptiveSelectTile({
|
const AdaptiveSelectTile({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.value,
|
required this.value,
|
||||||
@ -30,64 +35,50 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
this.controlAffinity = ListTileControlAffinity.trailing,
|
this.controlAffinity = ListTileControlAffinity.trailing,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.secondary,
|
this.secondary,
|
||||||
|
this.trailing,
|
||||||
this.breakLayout,
|
this.breakLayout,
|
||||||
this.showValueWhenUnfolded = true,
|
this.showValueWhenUnfolded = true,
|
||||||
super.key,
|
super.key,
|
||||||
|
this.popupConstraints,
|
||||||
|
this.popupWidthConstraint,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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.sizeOf(context);
|
||||||
final rawControl = DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.secondaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: DropdownButton<T>(
|
|
||||||
items: options,
|
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
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(
|
|
||||||
() => options
|
|
||||||
.firstWhere(
|
|
||||||
(element) => element.value == value,
|
|
||||||
orElse: () => DropdownMenuItem<T>(
|
|
||||||
value: null,
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child,
|
|
||||||
[value, options]);
|
|
||||||
|
|
||||||
final control = breakLayout ?? mediaQuery.mdAndUp
|
Widget? control = Select<T>(
|
||||||
? rawControl
|
itemBuilder: (context, item) {
|
||||||
: showValueWhenUnfolded
|
return options.firstWhere((element) => element.value == item).child;
|
||||||
? Container(
|
},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
value: value,
|
||||||
decoration: BoxDecoration(
|
onChanged: onChanged,
|
||||||
border: Border.all(
|
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
|
||||||
color: theme.colorScheme.primary,
|
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
|
||||||
width: 2,
|
autoClosePopover: true,
|
||||||
),
|
popup: (context) {
|
||||||
borderRadius: BorderRadius.circular(10),
|
return SelectPopup(
|
||||||
),
|
autoClose: true,
|
||||||
child: DefaultTextStyle(
|
items: SelectItemBuilder(
|
||||||
style: TextStyle(
|
childCount: options.length,
|
||||||
color: theme.colorScheme.primary,
|
builder: (context, index) {
|
||||||
),
|
return options[index];
|
||||||
child: controlPlaceholder,
|
},
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: const SizedBox.shrink();
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mediaQuery.smAndDown) {
|
||||||
|
if (showValueWhenUnfolded) {
|
||||||
|
control = OutlineBadge(
|
||||||
|
child: options.firstWhere((element) => element.value == value).child,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
control = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: title,
|
title: title,
|
||||||
@ -95,29 +86,48 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
leading: controlAffinity != ListTileControlAffinity.leading
|
leading: controlAffinity != ListTileControlAffinity.leading
|
||||||
? secondary
|
? secondary
|
||||||
: control,
|
: control,
|
||||||
trailing: controlAffinity == ListTileControlAffinity.leading
|
trailing: Row(
|
||||||
? secondary
|
mainAxisSize: MainAxisSize.min,
|
||||||
: control,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
...?trailing,
|
||||||
|
if (controlAffinity == ListTileControlAffinity.leading &&
|
||||||
|
secondary != null)
|
||||||
|
secondary!
|
||||||
|
else if (controlAffinity == ListTileControlAffinity.trailing &&
|
||||||
|
control != null)
|
||||||
|
control,
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: breakLayout ?? mediaQuery.mdAndUp
|
onTap: breakLayout ?? mediaQuery.mdAndUp
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SimpleDialog(
|
return AlertDialog(
|
||||||
title: title,
|
content: Flexible(
|
||||||
children: [
|
child: ListView.builder(
|
||||||
for (final option in options)
|
shrinkWrap: true,
|
||||||
RadioListTile<T>(
|
itemCount: options.length,
|
||||||
title: option.child,
|
itemBuilder: (context, index) {
|
||||||
value: option.value as T,
|
final item = options[index];
|
||||||
groupValue: value,
|
|
||||||
onChanged: (v) {
|
return ListTile(
|
||||||
Navigator.pop(context);
|
iconColor: theme.colorScheme.primary,
|
||||||
onChanged?.call(v);
|
leading: item.value == value
|
||||||
},
|
? const Icon(SpotubeIcons.radioChecked)
|
||||||
),
|
: const Icon(SpotubeIcons.radioUnchecked),
|
||||||
],
|
title: item.child,
|
||||||
|
onTap: () {
|
||||||
|
onChanged?.call(item.value);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
class AnimateGradient extends HookWidget {
|
class AnimateGradient extends HookWidget {
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
library bordered_text;
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
/// Adds stroke to text widget
|
|
||||||
/// We can apply a very thin and subtle stroke to a [Text]
|
|
||||||
/// ```dart
|
|
||||||
/// BorderedText(
|
|
||||||
/// strokeWidth: 1.0,
|
|
||||||
/// text: Text(
|
|
||||||
/// 'Bordered Text',
|
|
||||||
/// style: TextStyle(
|
|
||||||
/// decoration: TextDecoration.none,
|
|
||||||
/// decorationStyle: TextDecorationStyle.wavy,
|
|
||||||
/// decorationColor: Colors.red,
|
|
||||||
/// ),
|
|
||||||
/// ),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
class BorderedText extends StatelessWidget {
|
|
||||||
const BorderedText({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
this.strokeCap = StrokeCap.round,
|
|
||||||
this.strokeJoin = StrokeJoin.round,
|
|
||||||
this.strokeWidth = 6.0,
|
|
||||||
this.strokeColor = const Color.fromRGBO(53, 0, 71, 1),
|
|
||||||
});
|
|
||||||
|
|
||||||
/// the stroke cap style
|
|
||||||
final StrokeCap strokeCap;
|
|
||||||
|
|
||||||
/// the stroke joint style
|
|
||||||
final StrokeJoin strokeJoin;
|
|
||||||
|
|
||||||
/// the stroke width
|
|
||||||
final double strokeWidth;
|
|
||||||
|
|
||||||
/// the stroke color
|
|
||||||
final Color strokeColor;
|
|
||||||
|
|
||||||
/// the [Text] widget to apply stroke on
|
|
||||||
final Text child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
TextStyle style;
|
|
||||||
if (child.style != null) {
|
|
||||||
style = child.style!.copyWith(
|
|
||||||
foreground: Paint()
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeCap = strokeCap
|
|
||||||
..strokeJoin = strokeJoin
|
|
||||||
..strokeWidth = strokeWidth
|
|
||||||
..color = strokeColor,
|
|
||||||
color: null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
style = TextStyle(
|
|
||||||
foreground: Paint()
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeCap = strokeCap
|
|
||||||
..strokeJoin = strokeJoin
|
|
||||||
..strokeWidth = strokeWidth
|
|
||||||
..color = strokeColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
textDirection: child.textDirection,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
child.data!,
|
|
||||||
style: style,
|
|
||||||
maxLines: child.maxLines,
|
|
||||||
overflow: child.overflow,
|
|
||||||
semanticsLabel: child.semanticsLabel,
|
|
||||||
softWrap: child.softWrap,
|
|
||||||
strutStyle: child.strutStyle,
|
|
||||||
textAlign: child.textAlign,
|
|
||||||
textDirection: child.textDirection,
|
|
||||||
textScaler: child.textScaler,
|
|
||||||
),
|
|
||||||
child,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
21
lib/components/button/back_button.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|
||||||
|
class BackButton extends StatelessWidget {
|
||||||
|
final Color? color;
|
||||||
|
final IconData icon;
|
||||||
|
const BackButton({
|
||||||
|
super.key,
|
||||||
|
this.color,
|
||||||
|
this.icon = SpotubeIcons.angleLeft,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton.ghost(
|
||||||
|
size: const ButtonSize(.9),
|
||||||
|
icon: Icon(icon, color: color),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
|
|
||||||
import 'package:popover/popover.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
|
|
||||||
class CompactSearch extends HookWidget {
|
|
||||||
final ValueChanged<String>? onChanged;
|
|
||||||
final String placeholder;
|
|
||||||
final IconData icon;
|
|
||||||
final Color? iconColor;
|
|
||||||
|
|
||||||
const CompactSearch({
|
|
||||||
super.key,
|
|
||||||
this.onChanged,
|
|
||||||
this.placeholder = "Search...",
|
|
||||||
this.icon = SpotubeIcons.search,
|
|
||||||
this.iconColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showPopover(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: Theme.of(context).cardColor,
|
|
||||||
transitionDuration: const Duration(milliseconds: 100),
|
|
||||||
barrierColor: Colors.transparent,
|
|
||||||
arrowDxOffset: -6,
|
|
||||||
bodyBuilder: (context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
width: 300,
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
onChanged: onChanged,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: placeholder,
|
|
||||||
prefixIcon: Icon(icon),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
height: 60,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
tooltip: placeholder,
|
|
||||||
icon: Icon(icon, color: iconColor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/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';
|
||||||
@ -9,13 +8,15 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
final screenSize = MediaQuery.sizeOf(context);
|
||||||
title: Padding(
|
|
||||||
padding: const EdgeInsets.all(15),
|
return ConstrainedBox(
|
||||||
child: Row(
|
constraints: BoxConstraints(maxWidth: Breakpoints.sm),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
spacing: 10,
|
||||||
children: [
|
children: [
|
||||||
Text(context.l10n.are_you_sure),
|
Text(context.l10n.are_you_sure),
|
||||||
const SizedBox(width: 10),
|
|
||||||
const UniversalImage(
|
const UniversalImage(
|
||||||
path:
|
path:
|
||||||
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
||||||
@ -24,58 +25,53 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
content: Expanded(
|
||||||
content: Container(
|
flex: screenSize.smAndUp ? 0 : 1,
|
||||||
padding: const EdgeInsets.all(15),
|
child: SingleChildScrollView(
|
||||||
constraints: BoxConstraints(maxWidth: Breakpoints.sm),
|
child: Column(
|
||||||
child: SingleChildScrollView(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
context.l10n.download_warning,
|
||||||
Text(
|
textAlign: TextAlign.justify,
|
||||||
context.l10n.download_warning,
|
|
||||||
textAlign: TextAlign.justify,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Text(
|
|
||||||
context.l10n.download_ip_ban_warning,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.justify,
|
const SizedBox(height: 10),
|
||||||
),
|
Text(
|
||||||
const SizedBox(height: 10),
|
context.l10n.download_ip_ban_warning,
|
||||||
Text(
|
style: const TextStyle(
|
||||||
context.l10n.by_clicking_accept_terms,
|
color: Colors.red,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
BulletPoint(context.l10n.download_agreement_1),
|
textAlign: TextAlign.justify,
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
BulletPoint(context.l10n.download_agreement_2),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
Text(
|
||||||
BulletPoint(context.l10n.download_agreement_3),
|
context.l10n.by_clicking_accept_terms,
|
||||||
],
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
BulletPoint(context.l10n.download_agreement_1),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
BulletPoint(context.l10n.download_agreement_2),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
BulletPoint(context.l10n.download_agreement_3),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
Button.outline(
|
||||||
|
child: Text(context.l10n.decline),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Button.destructive(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text(context.l10n.accept),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
OutlinedButton(
|
|
||||||
child: Text(context.l10n.decline),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(context.l10n.accept),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|