@ -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": {}
|
||||||
}
|
}
|
10
.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.
|
||||||
@ -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
|
||||||
|
3
.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
|
||||||
|
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version to publish (x.x.x)
|
description: Version to publish (x.x.x)
|
||||||
default: 3.8.3
|
default: 4.0.0
|
||||||
required: true
|
required: true
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
description: Dry run
|
||||||
|
87
.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
|
||||||
@ -99,28 +108,16 @@ jobs:
|
|||||||
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
|
||||||
|
3
.gitignore
vendored
@ -80,3 +80,6 @@ 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"
|
||||||
}
|
}
|
1142
CHANGELOG.md
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
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:
|
||||||
|
|
||||||
|
7
Makefile
@ -46,3 +46,10 @@ gensums:
|
|||||||
|
|
||||||
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
|
38
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
|
||||||
@ -223,21 +228,20 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
1. [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'
|
||||||
}
|
}
|
20
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,5 +1,4 @@
|
|||||||
<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.
|
||||||
-->
|
-->
|
||||||
|
@ -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,28 +17,26 @@
|
|||||||
</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
|
||||||
@ -47,8 +45,8 @@
|
|||||||
-->
|
-->
|
||||||
<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>
|
@ -2,4 +2,5 @@
|
|||||||
<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" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,5 +1,4 @@
|
|||||||
<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.
|
||||||
-->
|
-->
|
||||||
|
@ -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,17 +13,19 @@
|
|||||||
<!-- == 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
|
||||||
@ -37,7 +40,10 @@
|
|||||||
- 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
|
||||||
|
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.)
|
||||||
- Time synced lyrics
|
- Time synced lyrics
|
||||||
- Downloadable tracks
|
- Downloadable tracks
|
||||||
</description>
|
</description>
|
||||||
|
@ -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,7 +90,16 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
|||||||
"spotube-${pubspec.version}-linux.deb",
|
"spotube-${pubspec.version}-linux.deb",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await ogDeb.copy(
|
||||||
|
join(
|
||||||
|
cwd.path,
|
||||||
|
"dist",
|
||||||
|
"Spotube-linux-$bundleArchName.deb",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await ogDeb.delete();
|
||||||
|
|
||||||
|
if (architecture == "x86") {
|
||||||
final ogRpm = File(
|
final ogRpm = File(
|
||||||
join(
|
join(
|
||||||
cwd.path,
|
cwd.path,
|
||||||
@ -91,15 +109,12 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ogDeb.copy(
|
|
||||||
join(cwd.path, "dist", "Spotube-linux-x86_64.deb"),
|
|
||||||
);
|
|
||||||
await ogRpm.copy(
|
await ogRpm.copy(
|
||||||
join(cwd.path, "dist", "Spotube-linux-x86_64.rpm"),
|
join(cwd.path, "dist", "Spotube-linux-$bundleArchName.rpm"),
|
||||||
);
|
);
|
||||||
|
|
||||||
await ogDeb.delete();
|
|
||||||
await ogRpm.delete();
|
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) {
|
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
|
||||||
return GoRouter(
|
class AppRouter extends RootStackRouter {
|
||||||
navigatorKey: rootNavigatorKey,
|
final WidgetRef ref;
|
||||||
routes: [
|
|
||||||
ShellRoute(
|
AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
|
||||||
navigatorKey: shellRouteNavigatorKey,
|
|
||||||
builder: (context, state, child) => RootApp(child: child),
|
@override
|
||||||
routes: [
|
List<AutoRoute> get routes => [
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
|
page: RootAppRoute.page,
|
||||||
path: "/",
|
path: "/",
|
||||||
name: HomePage.name,
|
initial: true,
|
||||||
redirect: (context, state) async {
|
children: [
|
||||||
|
AutoRoute(
|
||||||
|
path: "home",
|
||||||
|
page: HomeRoute.page,
|
||||||
|
initial: true,
|
||||||
|
guards: [
|
||||||
|
AutoRouteGuardCallback(
|
||||||
|
(resolver, router) async {
|
||||||
final auth = await ref.read(authenticationProvider.future);
|
final auth = await ref.read(authenticationProvider.future);
|
||||||
|
|
||||||
if (auth == null && !KVStoreService.doneGettingStarted) {
|
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||||
return "/getting-started";
|
resolver.redirect(const GettingStartedRoute());
|
||||||
|
} else {
|
||||||
|
resolver.next(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "home/genres",
|
||||||
|
page: GenreRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "home/genre/:categoryId",
|
||||||
|
page: GenrePlaylistsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "home/feeds/:feedId",
|
||||||
|
page: HomeFeedSectionRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "search",
|
||||||
|
page: SearchRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "library",
|
||||||
|
page: LibraryRoute.page,
|
||||||
|
children: [
|
||||||
|
AutoRoute(
|
||||||
|
path: "playlists",
|
||||||
|
page: UserPlaylistsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "artists",
|
||||||
|
page: UserArtistsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "albums",
|
||||||
|
page: UserAlbumsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "local",
|
||||||
|
page: UserLocalLibraryRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "local/folder",
|
||||||
|
page: LocalLibraryRoute.page,
|
||||||
|
// parentNavigatorKey: shellRouteNavigatorKey,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "downloads",
|
||||||
|
page: UserDownloadsRoute.page,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "album/:id",
|
||||||
|
page: AlbumRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: HomePage()),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "genres",
|
|
||||||
name: GenrePage.name,
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: GenrePage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "genre/:categoryId",
|
|
||||||
name: GenrePlaylistsPage.name,
|
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: GenrePlaylistsPage(
|
|
||||||
category: state.extra as Category,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "feeds/:feedId",
|
|
||||||
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: [
|
|
||||||
GoRoute(
|
|
||||||
path: "blacklist",
|
|
||||||
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(
|
AutoRoute(
|
||||||
path: "/album/:id",
|
path: "track/:id",
|
||||||
name: AlbumPage.name,
|
page: TrackRoute.page,
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.extra is AlbumSimple);
|
|
||||||
return SpotubePage(
|
|
||||||
child: AlbumPage(album: state.extra as AlbumSimple),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/artist/:id",
|
path: "connect",
|
||||||
name: ArtistPage.name,
|
page: ConnectRoute.page,
|
||||||
pageBuilder: (context, state) {
|
|
||||||
assert(state.pathParameters["id"] != null);
|
|
||||||
return SpotubePage(
|
|
||||||
child: ArtistPage(state.pathParameters["id"]!));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/playlist/:id",
|
path: "connect/control",
|
||||||
name: PlaylistPage.name,
|
page: ConnectControlRoute.page,
|
||||||
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(
|
AutoRoute(
|
||||||
path: "/track/:id",
|
path: "profile",
|
||||||
name: TrackPage.name,
|
page: ProfileRoute.page,
|
||||||
pageBuilder: (context, state) {
|
|
||||||
final id = state.pathParameters["id"]!;
|
|
||||||
return SpotubePage(
|
|
||||||
child: TrackPage(trackId: id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/connect",
|
path: "stats",
|
||||||
name: ConnectPage.name,
|
page: StatsRoute.page,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: ConnectPage(),
|
|
||||||
),
|
),
|
||||||
routes: [
|
AutoRoute(
|
||||||
GoRoute(
|
path: "stats/minutes",
|
||||||
path: "control",
|
page: StatsMinutesRoute.page,
|
||||||
name: ConnectControlPage.name,
|
|
||||||
pageBuilder: (context, state) {
|
|
||||||
return const SpotubePage(
|
|
||||||
child: ConnectControlPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/profile",
|
path: "stats/streams",
|
||||||
name: ProfilePage.name,
|
page: StatsStreamsRoute.page,
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: ProfilePage()),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "/stats",
|
path: "stats/fees",
|
||||||
name: StatsPage.name,
|
page: StatsStreamFeesRoute.page,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsPage(),
|
|
||||||
),
|
),
|
||||||
routes: [
|
AutoRoute(
|
||||||
GoRoute(
|
path: "stats/artists",
|
||||||
path: "minutes",
|
page: StatsArtistsRoute.page,
|
||||||
name: StatsMinutesPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsMinutesPage(),
|
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "stats/albums",
|
||||||
|
page: StatsAlbumsRoute.page,
|
||||||
),
|
),
|
||||||
GoRoute(
|
AutoRoute(
|
||||||
path: "streams",
|
path: "stats/playlists",
|
||||||
name: StatsStreamsPage.name,
|
page: StatsPlaylistsRoute.page,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsStreamsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "fees",
|
|
||||||
name: StatsStreamFeesPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsStreamFeesPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "artists",
|
|
||||||
name: StatsArtistsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsArtistsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "albums",
|
|
||||||
name: StatsAlbumsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsAlbumsPage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "playlists",
|
|
||||||
name: StatsPlaylistsPage.name,
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: StatsPlaylistsPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/queue",
|
||||||
|
page: PlayerQueueRoute.page,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/sources",
|
||||||
|
page: PlayerTrackSourcesRoute.page,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/lyrics",
|
||||||
|
page: PlayerLyricsRoute.page,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
name: MiniLyricsPage.name,
|
page: MiniLyricsRoute.page,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
|
||||||
path: "/getting-started",
|
path: "/getting-started",
|
||||||
name: GettingStarting.name,
|
page: GettingStartedRoute.page,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: GettingStarting(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: WebViewLogin.name,
|
page: WebViewLoginRoute.page,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: WebViewLogin(),
|
|
||||||
),
|
),
|
||||||
),
|
AutoRoute(
|
||||||
GoRoute(
|
|
||||||
path: "/lastfm-login",
|
path: "/lastfm-login",
|
||||||
name: LastFMLoginPage.name,
|
page: LastFMLoginRoute.page,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
// parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
|
||||||
),
|
),
|
||||||
],
|
];
|
||||||
);
|
}
|
||||||
});
|
|
||||||
|
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,15 +25,15 @@ 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,
|
||||||
@ -43,22 +44,20 @@ class AdaptiveListTile extends HookWidget {
|
|||||||
title: title != null
|
title: title != null
|
||||||
? Row(
|
? Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 5,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (leading != null) ...[
|
if (leading != null) leading!,
|
||||||
leading!,
|
|
||||||
const SizedBox(width: 5)
|
|
||||||
],
|
|
||||||
Flexible(child: title!),
|
Flexible(child: title!),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Container(),
|
: const SizedBox.shrink(),
|
||||||
content: trailing?.call(context, update),
|
content: Center(child: 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,
|
context: context,
|
||||||
useRootNavigator: useRootNavigator,
|
rootOverlay: useRootNavigator,
|
||||||
constraints: BoxConstraints(
|
// heightConstraint: PopoverConstraint.anchorFixedSize,
|
||||||
maxHeight: mediaQuery.size.height * 0.6,
|
// constraints: BoxConstraints(
|
||||||
),
|
// maxHeight: mediaQuery.size.height * 0.6,
|
||||||
|
// ),
|
||||||
position: position,
|
position: position,
|
||||||
items: children
|
builder: (context) {
|
||||||
.map(
|
return DropdownMenu(
|
||||||
(item) => PopupMenuItem<T>(
|
children: childrenModified,
|
||||||
padding: EdgeInsets.zero,
|
);
|
||||||
enabled: false,
|
},
|
||||||
child: _AdaptivePopSheetListItem<T>(
|
).future;
|
||||||
item: item,
|
return;
|
||||||
onSelected: onSelected,
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
enableDrag: true,
|
||||||
|
showDragHandle: true,
|
||||||
|
useRootNavigator: true,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
),
|
),
|
||||||
|
backgroundColor: context.theme.colorScheme.card,
|
||||||
|
builder: (context) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: childrenModified.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final data = childrenModified[index];
|
||||||
|
|
||||||
|
return Button(
|
||||||
|
enabled: data.enabled,
|
||||||
|
style: ButtonVariance.ghost.copyWith(
|
||||||
|
padding: (context, state, value) => const EdgeInsets.all(16),
|
||||||
),
|
),
|
||||||
)
|
onPressed: () {
|
||||||
.toList(),
|
data.onPressed?.call(context);
|
||||||
|
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) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0).copyWith(top: 0),
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style: theme.textTheme.titleMedium!,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (headings != null) ...[
|
|
||||||
...headings!,
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Divider(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
thickness: 0.3,
|
|
||||||
endIndent: 16,
|
|
||||||
indent: 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
...children.map(
|
|
||||||
(item) => _AdaptivePopSheetListItem(
|
|
||||||
item: item,
|
|
||||||
onSelected: onSelected,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: IconButton(
|
||||||
|
variance: variance,
|
||||||
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
|
onPressed: () {
|
||||||
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
final position = RelativeRect.fromRect(
|
||||||
|
Rect.fromPoints(
|
||||||
|
renderBox.localToGlobal(Offset.zero,
|
||||||
|
ancestor: context.findRenderObject()),
|
||||||
|
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero),
|
||||||
|
ancestor: context.findRenderObject()),
|
||||||
),
|
),
|
||||||
|
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(
|
||||||
|
tooltip: TooltipContainer(child: Text(tooltip)),
|
||||||
|
child: IconButton(
|
||||||
|
variance: variance,
|
||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
tooltip: tooltip,
|
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||||
style: theme.iconButtonTheme.style?.copyWith(
|
|
||||||
shape: WidgetStatePropertyAll(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
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(
|
Widget? control = Select<T>(
|
||||||
color: theme.colorScheme.secondaryContainer,
|
itemBuilder: (context, item) {
|
||||||
borderRadius: BorderRadius.circular(10),
|
return options.firstWhere((element) => element.value == item).child;
|
||||||
),
|
},
|
||||||
child: DropdownButton<T>(
|
|
||||||
items: options,
|
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
menuMaxHeight: mediaQuery.size.height * 0.6,
|
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
|
||||||
underline: const SizedBox.shrink(),
|
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
autoClosePopover: true,
|
||||||
borderRadius: BorderRadius.circular(10),
|
popup: (context) {
|
||||||
icon: const Icon(SpotubeIcons.angleDown),
|
return SelectPopup(
|
||||||
dropdownColor: theme.colorScheme.secondaryContainer,
|
autoClose: true,
|
||||||
|
items: SelectItemBuilder(
|
||||||
|
childCount: options.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
return options[index];
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final controlPlaceholder = useMemoized(
|
},
|
||||||
() => options
|
);
|
||||||
.firstWhere(
|
|
||||||
(element) => element.value == value,
|
|
||||||
orElse: () => DropdownMenuItem<T>(
|
|
||||||
value: null,
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child,
|
|
||||||
[value, options]);
|
|
||||||
|
|
||||||
final control = breakLayout ?? mediaQuery.mdAndUp
|
if (mediaQuery.smAndDown) {
|
||||||
? rawControl
|
if (showValueWhenUnfolded) {
|
||||||
: showValueWhenUnfolded
|
control = OutlineBadge(
|
||||||
? Container(
|
child: options.firstWhere((element) => element.value == value).child,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
);
|
||||||
decoration: BoxDecoration(
|
} else {
|
||||||
border: Border.all(
|
control = null;
|
||||||
color: theme.colorScheme.primary,
|
}
|
||||||
width: 2,
|
}
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
child: controlPlaceholder,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
|
|
||||||
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,10 +25,8 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
content: Expanded(
|
||||||
content: Container(
|
flex: screenSize.smAndUp ? 0 : 1,
|
||||||
padding: const EdgeInsets.all(15),
|
|
||||||
constraints: BoxConstraints(maxWidth: Breakpoints.sm),
|
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -61,21 +60,18 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
OutlinedButton(
|
Button.outline(
|
||||||
child: Text(context.l10n.decline),
|
child: Text(context.l10n.decline),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilledButton(
|
Button.destructive(
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
child: Text(context.l10n.accept),
|
child: Text(context.l10n.accept),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
||||||
|
|
||||||
class PipedDownDialog extends HookConsumerWidget {
|
|
||||||
const PipedDownDialog({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final pipedInstance =
|
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.pipedInstance));
|
|
||||||
final ThemeData(:colorScheme) = Theme.of(context);
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
insetPadding: const EdgeInsets.all(6),
|
|
||||||
contentPadding: const EdgeInsets.all(6),
|
|
||||||
icon: Icon(
|
|
||||||
SpotubeIcons.error,
|
|
||||||
color: colorScheme.error,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
context.l10n.piped_api_down,
|
|
||||||
style: TextStyle(color: colorScheme.error),
|
|
||||||
),
|
|
||||||
content: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child:
|
|
||||||
Text(context.l10n.piped_down_error_instructions(pipedInstance)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(context.l10n.ok),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(context.l10n.settings),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|