Merge branch 'dev' into website

This commit is contained in:
Kingkor Roy Tirtho 2025-03-07 20:08:09 +06:00
commit 894b0d7e5e
407 changed files with 22215 additions and 16625 deletions

View File

@ -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

View File

@ -1,3 +1,3 @@
{ {
"flutterSdkVersion": "3.24.5" "flutterSdkVersion": "3.29.0"
} }

2
.fvmrc
View File

@ -1,4 +1,4 @@
{ {
"flutter": "3.24.5", "flutter": "3.29.0",
"flavors": {} "flavors": {}
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -80,3 +80,6 @@ tm.json
# FVM Version Cache # FVM Version Cache
.fvm/ .fvm/
android/build
android/app/.cxx

11
.vscode/launch.json vendored
View File

@ -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": []

View File

@ -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"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
BSD-4-Clause License BSD-4-Clause License
Copyright (c) 2023 Kingkor Roy Tirtho. All rights reserved. Copyright (c) 2025 Kingkor Roy Tirtho. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must display the following acknowledgement: 3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
This product includes software developed by Kingkor Roy Tirtho. This product includes software developed by Kingkor Roy Tirtho.
4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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'
} }

View File

@ -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.**

View File

@ -1,7 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="oss.krtirtho.spotube"> <!-- Flutter needs it to communicate with the running application
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -17,38 +17,36 @@
</queries> </queries>
<application <application
android:name="${applicationName}"
android:allowBackup="false" android:allowBackup="false"
android:fullBackupContent="false" android:fullBackupContent="false"
android:label="@string/app_name_en"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true" android:label="@string/app_name_en"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
> android:usesCleartextTraffic="true">
<!-- Enable Impeller --> <!-- Enable Impeller -->
<!-- <meta-data <!-- <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"
android:value="true" /> --> android:value="false" /> -->
<activity <activity
android:name="com.ryanheise.audioservice.AudioServiceActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:windowSoftInputMode="adjustResize">
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
>
<!-- <!--
Specifies an Android theme to apply to this Activity as soon as Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. to determine the Window background behind the Flutter UI.
--> -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme" />
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -56,12 +54,13 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:scheme="https"
android:host="open.spotify.com" android:host="open.spotify.com"
/> android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
@ -72,23 +71,30 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "spotify:// --> <!-- Accepts URIs that begin with "spotify:// -->
<data android:scheme="spotify" /> <data android:scheme="spotify" />
<data android:scheme="spotube" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
</intent-filter>
</activity> </activity>
<!-- AudioService Config --> <!-- AudioService Config -->
<service android:name="com.ryanheise.audioservice.AudioService" <service
android:foregroundServiceType="mediaPlayback" android:name="com.ryanheise.audioservice.AudioService"
android:exported="true"> android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" <receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
@ -96,11 +102,40 @@
</receiver> </receiver>
<!-- =================== --> <!-- =================== -->
<meta-data android:name="com.google.android.gms.car.application" <meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" /> android:resource="@xml/automotive_app_desc" />
<!-- Home Widget config -->
<receiver
android:name=".glance.HomePlayerWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/home_player_widget_config" />
</receiver>
<receiver
android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver"
android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service
android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- =================== -->
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2" /> <meta-data
android:name="flutterEmbedding"
android:value="2" />
</application> </application>
</manifest> </manifest>

View File

@ -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()
}
}

View File

@ -0,0 +1,7 @@
package oss.krtirtho.spotube.glance
import HomeWidgetGlanceWidgetReceiver
class HomePlayerWidgetReceiver : HomeWidgetGlanceWidgetReceiver<HomePlayerWidget>() {
override val glanceAppWidget = HomePlayerWidget()
}

View File

@ -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
}

View File

@ -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?
)

View File

@ -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,
)

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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)
)
}

View File

@ -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
),
)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View File

@ -1,7 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="oss.krtirtho.spotube"> <!-- Flutter needs it to communicate with the running application
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -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

View File

@ -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'

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 KiB

After

Width:  |  Height:  |  Size: 1006 KiB

View File

@ -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

View File

@ -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=()

View File

@ -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

View File

@ -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 “Ω” doesnt 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 “Ω” doesnt appear as greek uppercase omega letter
enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata> <metadata>
<!-- == PACKAGE SPECIFIC SECTION == --> <!-- == PACKAGE SPECIFIC SECTION == -->
@ -12,34 +13,39 @@
<!-- == SOFTWARE SPECIFIC SECTION == --> <!-- == SOFTWARE SPECIFIC SECTION == -->
<title>spotube (Install)</title> <title>spotube (Install)</title>
<authors>Kingkor Roy Tirtho</authors> <authors>Kingkor Roy Tirtho</authors>
<projectUrl>https://github.com/KRTirtho/spotube/</projectUrl> <projectUrl>https://spotube.krtirtho.dev</projectUrl>
<iconUrl>https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl> <iconUrl>
https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
<copyright>2022 Spotube</copyright> <copyright>2022 Spotube</copyright>
<!-- If there is a license Url available, it is required for the community feed --> <!-- If there is a license Url available, it is required for the community feed -->
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl> <licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance> <requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl> <projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl>
<docsUrl>https://github.com/KRTirtho/spotube#readme</docsUrl> <docsUrl>https://spotube.krtirtho.dev</docsUrl>
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl> <bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
<tags>spotube music audio spotify youtube flutter</tags> <tags>spotube music audio spotify youtube flutter</tags>
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop &amp; mobile! </summary> <summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available
for both desktop &amp; 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 &amp; Youtube's public API &amp; creates a hazardless, performant &amp; resource of Spotify &amp; Youtube's public API &amp; creates a hazardless, performant &amp; resource
friendly User Experience friendly User Experience
# Features # Features
- Open source/libre software - Open source/libre software
- Anonymous/guest login - Anonymous/guest login
- Cross platform support - Cross platform support
- No telemetry, diagnostics or user data collection - No telemetry, diagnostics or user data collection
- Lightweight &amp; resource-friendly - Lightweight &amp; resource-friendly
- Native performance (Thanks to Flutter+Skia) - Native performance (Thanks to Flutter+Skia)
- Playback control is done locally instead of on the server - Playback control is done locally instead of on the server
- Small size &amp; less data usage - Small size &amp; less data usage
- No Spotify or YouTube ads since it uses all public &amp; 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 &amp; free APIs (It is still recommended
- Time synced lyrics to support the creators by watching/liking/subscribing to the artists' YouTube channels or
- Downloadable tracks liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support
their valuable creations.)
- Time synced lyrics
- Downloadable tracks
</description> </description>
<releaseNotes>https://github.com/KRTirtho/spotube/releases/tag/v%{{SPOTUBE_VERSION}}%</releaseNotes> <releaseNotes>https://github.com/KRTirtho/spotube/releases/tag/v%{{SPOTUBE_VERSION}}%</releaseNotes>
</metadata> </metadata>

View File

@ -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:

View File

@ -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"],
);
} }
} }

View File

@ -63,4 +63,6 @@ mixin BuildCommandCommonSteps on Command {
""", """,
); );
} }
String get architecture => parent?.argResults?.option("arch") as String;
} }

View File

@ -37,23 +37,32 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
await bootstrap(); await bootstrap();
await shell.run( await shell.run(
""" "flutter_distributor package --platform=linux --targets=deb",
flutter_distributor package --platform=linux --targets=deb
flutter_distributor package --platform=linux --targets=rpm
""",
); );
final tempDir = join(Directory.systemTemp.path, "spotube-tar"); if (architecture == "x86") {
await shell.run(
"flutter_distributor package --platform=linux --targets=rpm",
);
}
final bundleDirPath = final tempDir = join(Directory.systemTemp.path, "spotube-tar");
join(cwd.path, "build", "linux", "x64", "release", "bundle"); final bundleArchName = architecture == "x86" ? "x86_64" : "aarch64";
final bundleDirPath = join(
cwd.path,
"build",
"linux",
architecture == "x86" ? "x64" : architecture,
"release",
"bundle",
);
final tarFile = File(join( final tarFile = File(join(
cwd.path, cwd.path,
"dist", "dist",
"spotube-linux-" "spotube-linux-"
"${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}" "${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}"
"-x86_64.tar.xz", "-$bundleArchName.tar.xz",
)); ));
await copyPath(bundleDirPath, tempDir); await copyPath(bundleDirPath, tempDir);
@ -81,25 +90,31 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
"spotube-${pubspec.version}-linux.deb", "spotube-${pubspec.version}-linux.deb",
), ),
); );
await ogDeb.copy(
final ogRpm = File(
join( join(
cwd.path, cwd.path,
"dist", "dist",
pubspec.version.toString(), "Spotube-linux-$bundleArchName.deb",
"spotube-${pubspec.version}-linux.rpm",
), ),
); );
await ogDeb.copy(
join(cwd.path, "dist", "Spotube-linux-x86_64.deb"),
);
await ogRpm.copy(
join(cwd.path, "dist", "Spotube-linux-x86_64.rpm"),
);
await ogDeb.delete(); await ogDeb.delete();
await ogRpm.delete();
if (architecture == "x86") {
final ogRpm = File(
join(
cwd.path,
"dist",
pubspec.version.toString(),
"spotube-${pubspec.version}-linux.rpm",
),
);
await ogRpm.copy(
join(cwd.path, "dist", "Spotube-linux-$bundleArchName.rpm"),
);
await ogRpm.delete();
}
stdout.writeln("✅ Linux building done"); stdout.writeln("✅ Linux building done");
} }

View File

@ -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/
""",
);
}
}

View File

@ -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(
""" """

File diff suppressed because one or more lines are too long

View 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"

View 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"

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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: "🤩")
}

View 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()
}
}

View 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>

View 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>

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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">

View File

@ -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)
} }

View File

@ -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"}}

View File

@ -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>

View 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
View 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
View 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
View 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>

View File

@ -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,

View File

@ -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;

View 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';
}

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
const gradients = [ const gradients = [
LinearGradient(colors: [ LinearGradient(colors: [

View File

@ -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;

View File

@ -1,329 +1,235 @@
import 'package:flutter/foundation.dart' hide Category; import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/connect/connect.dart';
import 'package:spotube/pages/connect/control/control.dart';
import 'package:spotube/pages/getting_started/getting_started.dart';
import 'package:spotube/pages/home/feed/feed_section.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/pages/home/genres/genres.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/local_folder.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/playlist/liked_playlist.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/pages/stats/albums/albums.dart';
import 'package:spotube/pages/stats/artists/artists.dart';
import 'package:spotube/pages/stats/fees/fees.dart';
import 'package:spotube/pages/stats/minutes/minutes.dart';
import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/components/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final routerProvider = Provider((ref) {
return GoRouter(
navigatorKey: rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: shellRouteNavigatorKey,
builder: (context, state, child) => RootApp(child: child),
routes: [
GoRoute(
path: "/",
name: HomePage.name,
redirect: (context, state) async {
final auth = await ref.read(authenticationProvider.future);
if (auth == null && !KVStoreService.doneGettingStarted) { @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
return "/getting-started"; class AppRouter extends RootStackRouter {
} final WidgetRef ref;
return null; AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
},
pageBuilder: (context, state) => @override
const SpotubePage(child: HomePage()), List<AutoRoute> get routes => [
routes: [ AutoRoute(
GoRoute( page: RootAppRoute.page,
path: "genres", path: "/",
name: GenrePage.name, initial: true,
pageBuilder: (context, state) => children: [
const SpotubePage(child: GenrePage()), AutoRoute(
), path: "home",
GoRoute( page: HomeRoute.page,
path: "genre/:categoryId", initial: true,
name: GenrePlaylistsPage.name, guards: [
pageBuilder: (context, state) => SpotubePage( AutoRouteGuardCallback(
child: GenrePlaylistsPage( (resolver, router) async {
category: state.extra as Category, final auth = await ref.read(authenticationProvider.future);
),
), if (auth == null && !KVStoreService.doneGettingStarted) {
), resolver.redirect(const GettingStartedRoute());
GoRoute( } else {
path: "feeds/:feedId", resolver.next(true);
name: HomeFeedSectionPage.name, }
pageBuilder: (context, state) => SpotubePage(
child: HomeFeedSectionPage(
sectionUri: state.pathParameters["feedId"] as String,
),
),
)
],
),
GoRoute(
path: "/search",
name: SearchPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: SearchPage()),
),
GoRoute(
path: "/library",
name: LibraryPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: LibraryPage()),
routes: [
GoRoute(
path: "generate",
name: PlaylistGeneratorPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: PlaylistGeneratorPage()),
routes: [
GoRoute(
path: "result",
name: PlaylistGenerateResultPage.name,
pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage(
state: state.extra as GeneratePlaylistProviderInput,
),
),
)
],
),
GoRoute(
path: "local",
name: LocalLibraryPage.name,
pageBuilder: (context, state) {
assert(state.extra is String);
return SpotubePage(
child: LocalLibraryPage(
state.extra as String,
isDownloads:
state.uri.queryParameters["downloads"] != null,
isCache: state.uri.queryParameters["cache"] != null,
),
);
}, },
), ),
]), ],
GoRoute(
path: "/lyrics",
name: LyricsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: LyricsPage()),
),
GoRoute(
path: "/settings",
name: SettingsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: SettingsPage(),
), ),
routes: [ AutoRoute(
GoRoute( path: "home/genres",
path: "blacklist", page: GenreRoute.page,
name: BlackListPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const BlackListPage(),
),
),
if (!kIsWeb)
GoRoute(
path: "logs",
name: LogsPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const LogsPage(),
),
),
GoRoute(
path: "about",
name: AboutSpotube.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const AboutSpotube(),
),
),
],
),
GoRoute(
path: "/album/:id",
name: AlbumPage.name,
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
return SpotubePage(
child: AlbumPage(album: state.extra as AlbumSimple),
);
},
),
GoRoute(
path: "/artist/:id",
name: ArtistPage.name,
pageBuilder: (context, state) {
assert(state.pathParameters["id"] != null);
return SpotubePage(
child: ArtistPage(state.pathParameters["id"]!));
},
),
GoRoute(
path: "/playlist/:id",
name: PlaylistPage.name,
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
child: state.pathParameters["id"] == "user-liked-tracks"
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
: PlaylistPage(playlist: state.extra as PlaylistSimple),
);
},
),
GoRoute(
path: "/track/:id",
name: TrackPage.name,
pageBuilder: (context, state) {
final id = state.pathParameters["id"]!;
return SpotubePage(
child: TrackPage(trackId: id),
);
},
),
GoRoute(
path: "/connect",
name: ConnectPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: ConnectPage(),
), ),
routes: [ AutoRoute(
GoRoute( path: "home/genre/:categoryId",
path: "control", page: GenrePlaylistsRoute.page,
name: ConnectControlPage.name,
pageBuilder: (context, state) {
return const SpotubePage(
child: ConnectControlPage(),
);
},
)
],
),
GoRoute(
path: "/profile",
name: ProfilePage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: ProfilePage()),
),
GoRoute(
path: "/stats",
name: StatsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsPage(),
), ),
routes: [ AutoRoute(
GoRoute( path: "home/feeds/:feedId",
path: "minutes", page: HomeFeedSectionRoute.page,
name: StatsMinutesPage.name, ),
pageBuilder: (context, state) => const SpotubePage( AutoRoute(
child: StatsMinutesPage(), path: "search",
page: SearchRoute.page,
),
AutoRoute(
path: "library",
page: LibraryRoute.page,
children: [
AutoRoute(
path: "playlists",
page: UserPlaylistsRoute.page,
), ),
), AutoRoute(
GoRoute( path: "artists",
path: "streams", page: UserArtistsRoute.page,
name: StatsStreamsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamsPage(),
), ),
), AutoRoute(
GoRoute( path: "albums",
path: "fees", page: UserAlbumsRoute.page,
name: StatsStreamFeesPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamFeesPage(),
), ),
), AutoRoute(
GoRoute( path: "local",
path: "artists", page: UserLocalLibraryRoute.page,
name: StatsArtistsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsArtistsPage(),
), ),
), AutoRoute(
GoRoute( path: "local/folder",
path: "albums", page: LocalLibraryRoute.page,
name: StatsAlbumsPage.name, // parentNavigatorKey: shellRouteNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: StatsAlbumsPage(),
), ),
), AutoRoute(
GoRoute( path: "downloads",
path: "playlists", page: UserDownloadsRoute.page,
name: StatsPlaylistsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsPlaylistsPage(),
), ),
],
),
AutoRoute(
path: "library/generate",
page: PlaylistGeneratorRoute.page,
),
AutoRoute(
path: "library/generate/result",
page: PlaylistGenerateResultRoute.page,
),
AutoRoute(
path: "lyrics",
page: LyricsRoute.page,
),
AutoRoute(
path: "settings",
page: SettingsRoute.page,
),
AutoRoute(
path: "settings/blacklist",
page: BlackListRoute.page,
),
if (!kIsWeb)
AutoRoute(
path: "settings/logs",
page: LogsRoute.page,
), ),
], AutoRoute(
) path: "settings/about",
], page: AboutSpotubeRoute.page,
), ),
GoRoute( AutoRoute(
path: "/mini-player", path: "album/:id",
name: MiniLyricsPage.name, page: AlbumRoute.page,
parentNavigatorKey: rootNavigatorKey, ),
pageBuilder: (context, state) => SpotubePage( AutoRoute(
child: MiniLyricsPage(prevSize: state.extra as Size), path: "artist/:id",
page: ArtistRoute.page,
),
AutoRoute(
path: "liked-tracks",
page: LikedPlaylistRoute.page,
),
AutoRoute(
path: "playlist/:id",
page: PlaylistRoute.page,
guards: [
AutoRouteGuard.redirect(
(resolver) {
final PlaylistRouteArgs(:id, :playlist) =
resolver.route.args as PlaylistRouteArgs;
if (id == "user-liked-tracks") {
return LikedPlaylistRoute(playlist: playlist);
}
return null;
},
),
],
),
AutoRoute(
path: "track/:id",
page: TrackRoute.page,
),
AutoRoute(
path: "connect",
page: ConnectRoute.page,
),
AutoRoute(
path: "connect/control",
page: ConnectControlRoute.page,
),
AutoRoute(
path: "profile",
page: ProfileRoute.page,
),
AutoRoute(
path: "stats",
page: StatsRoute.page,
),
AutoRoute(
path: "stats/minutes",
page: StatsMinutesRoute.page,
),
AutoRoute(
path: "stats/streams",
page: StatsStreamsRoute.page,
),
AutoRoute(
path: "stats/fees",
page: StatsStreamFeesRoute.page,
),
AutoRoute(
path: "stats/artists",
page: StatsArtistsRoute.page,
),
AutoRoute(
path: "stats/albums",
page: StatsAlbumsRoute.page,
),
AutoRoute(
path: "stats/playlists",
page: StatsPlaylistsRoute.page,
),
],
), ),
), CustomRoute(
GoRoute( transitionsBuilder: TransitionsBuilders.slideBottom,
path: "/getting-started", durationInMilliseconds: 200,
name: GettingStarting.name, reverseDurationInMilliseconds: 200,
parentNavigatorKey: rootNavigatorKey, path: "/player/queue",
pageBuilder: (context, state) => const SpotubePage( page: PlayerQueueRoute.page,
child: GettingStarting(),
), ),
), CustomRoute(
GoRoute( transitionsBuilder: TransitionsBuilders.slideBottom,
path: "/login", durationInMilliseconds: 200,
name: WebViewLogin.name, reverseDurationInMilliseconds: 200,
parentNavigatorKey: rootNavigatorKey, path: "/player/sources",
pageBuilder: (context, state) => const SpotubePage( page: PlayerTrackSourcesRoute.page,
child: WebViewLogin(),
), ),
), CustomRoute(
GoRoute( transitionsBuilder: TransitionsBuilders.slideBottom,
path: "/lastfm-login", durationInMilliseconds: 200,
name: LastFMLoginPage.name, reverseDurationInMilliseconds: 200,
parentNavigatorKey: rootNavigatorKey, path: "/player/lyrics",
pageBuilder: (context, state) => page: PlayerLyricsRoute.page,
const SpotubePage(child: LastFMLoginPage()), ),
), AutoRoute(
], path: "/mini-player",
); page: MiniLyricsRoute.page,
}); // parentNavigatorKey: rootNavigatorKey,
),
AutoRoute(
path: "/getting-started",
page: GettingStartedRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
AutoRoute(
path: "/login",
page: WebViewLoginRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
AutoRoute(
path: "/lastfm-login",
page: LastFMLoginRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
];
}

File diff suppressed because it is too large Load Diff

View 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,
), ),

View File

@ -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;
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
class AdaptiveListTile extends HookWidget { class AdaptiveListTile extends HookWidget {
@ -24,41 +25,39 @@ class AdaptiveListTile extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
return ListTile( return ButtonTile(
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
trailing: breakOn ?? mediaQuery.smAndDown trailing: breakOn ?? mediaQuery.smAndDown
? null ? null
: trailing?.call(context, null), : trailing?.call(context, null),
leading: leading, leading: leading,
onTap: breakOn ?? mediaQuery.smAndDown enabled: breakOn ?? mediaQuery.smAndDown,
? () { onPressed: () {
onTap?.call(); onTap?.call();
showDialog( showDialog(
context: context, context: context,
barrierDismissible: true, barrierDismissible: true,
builder: (context) { builder: (context) {
return StatefulBuilder(builder: (context, update) { return StatefulBuilder(builder: (context, update) {
return AlertDialog( return AlertDialog(
title: title != null title: title != null
? Row( ? Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ spacing: 5,
if (leading != null) ...[ mainAxisAlignment: MainAxisAlignment.center,
leading!, children: [
const SizedBox(width: 5) if (leading != null) leading!,
], Flexible(child: title!),
Flexible(child: title!), ],
], )
) : const SizedBox.shrink(),
: Container(), content: Center(child: trailing?.call(context, update)),
content: trailing?.call(context, update),
);
});
},
); );
} });
: null, },
);
},
); );
} }
} }

View File

@ -1,67 +1,46 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' show showModalBottomSheet;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
_emptyCB() {} class AdaptiveMenuButton<T> extends MenuButton {
class PopSheetEntry<T> extends ListTile {
final T? value; final T? value;
const PopSheetEntry({ const AdaptiveMenuButton({
this.value,
super.key, super.key,
super.leading, this.value,
super.title, required super.child,
super.subtitle, super.subMenu,
super.onPressed,
super.trailing, super.trailing,
super.isThreeLine = false, super.leading,
super.dense,
super.visualDensity,
super.shape,
super.style,
super.selectedColor,
super.iconColor,
super.textColor,
super.titleTextStyle,
super.subtitleTextStyle,
super.leadingAndTrailingTextStyle,
super.contentPadding,
super.enabled = true, super.enabled = true,
super.onTap = _emptyCB,
super.onLongPress,
super.onFocusChange,
super.mouseCursor,
super.selected = false,
super.focusColor,
super.hoverColor,
super.splashColor,
super.focusNode, super.focusNode,
super.autofocus = false, super.autoClose = true,
super.tileColor, super.popoverController,
super.selectedTileColor, }) : assert(
super.enableFeedback, value != null || onPressed != null,
super.horizontalTitleGap, 'Either value or onPressed must be provided',
super.minVerticalPadding, );
super.minLeadingWidth,
super.titleAlignment,
});
} }
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above /// An adaptive widget that shows a [PopupMenuButton] when screen size is above
/// or equal to 640px /// or equal to 640px
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown /// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
class AdaptivePopSheetList<T> extends StatelessWidget { class AdaptivePopSheetList<T> extends StatelessWidget {
final List<PopSheetEntry<T>> children; final List<AdaptiveMenuButton<T>> children;
final Widget? icon; final Widget? icon;
final Widget? child; final Widget? child;
final bool useRootNavigator; final bool useRootNavigator;
final List<Widget>? headings; final List<Widget>? headings;
final String? tooltip; final String tooltip;
final ValueChanged<T>? onSelected; final ValueChanged<T>? onSelected;
final BorderRadius borderRadius;
final Offset offset; final Offset offset;
final ButtonVariance variance;
const AdaptivePopSheetList({ const AdaptivePopSheetList({
super.key, super.key,
required this.children, required this.children,
@ -70,166 +49,141 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
this.useRootNavigator = true, this.useRootNavigator = true,
this.headings, this.headings,
this.onSelected, this.onSelected,
this.borderRadius = const BorderRadius.all(Radius.circular(999)), required this.tooltip,
this.tooltip,
this.offset = Offset.zero, this.offset = Offset.zero,
this.variance = ButtonVariance.ghost,
}) : assert( }) : assert(
!(icon != null && child != null), !(icon != null && child != null),
'Either icon or child must be provided', 'Either icon or child must be provided',
); );
Future<T?> showPopupMenu(BuildContext context, RelativeRect position) { Future<void> showDropdownMenu(BuildContext context, Offset position) async {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final childrenModified = children.map((s) {
if (s.onPressed == null) {
return MenuButton(
key: s.key,
autoClose: s.autoClose,
enabled: s.enabled,
leading: s.leading,
focusNode: s.focusNode,
onPressed: (context) {
if (s.value != null) {
onSelected?.call(s.value as T);
}
},
popoverController: s.popoverController,
subMenu: s.subMenu,
trailing: s.trailing,
child: s.child,
);
}
return s;
}).toList();
return showMenu<T>( if (mediaQuery.mdAndUp) {
await showDropdown<T?>(
context: context,
rootOverlay: useRootNavigator,
// heightConstraint: PopoverConstraint.anchorFixedSize,
// constraints: BoxConstraints(
// maxHeight: mediaQuery.size.height * 0.6,
// ),
position: position,
builder: (context) {
return DropdownMenu(
children: childrenModified,
);
},
).future;
return;
}
showModalBottomSheet(
context: context, context: context,
useRootNavigator: useRootNavigator, enableDrag: true,
constraints: BoxConstraints( showDragHandle: true,
maxHeight: mediaQuery.size.height * 0.6, useRootNavigator: true,
shape: RoundedRectangleBorder(
borderRadius: context.theme.borderRadiusMd,
), ),
position: position, backgroundColor: context.theme.colorScheme.card,
items: children builder: (context) {
.map( return ListView.builder(
(item) => PopupMenuItem<T>( itemCount: childrenModified.length,
padding: EdgeInsets.zero, shrinkWrap: true,
enabled: false, itemBuilder: (context, index) {
child: _AdaptivePopSheetListItem<T>( final data = childrenModified[index];
item: item,
onSelected: onSelected, return Button(
enabled: data.enabled,
style: ButtonVariance.ghost.copyWith(
padding: (context, state, value) => const EdgeInsets.all(16),
), ),
), onPressed: () {
) data.onPressed?.call(context);
.toList(), if (data.autoClose) {
Navigator.of(context).pop();
}
},
leading: data.leading,
trailing: data.trailing,
alignment: Alignment.centerLeft,
child: data.child,
);
},
);
},
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
if (mediaQuery.mdAndUp) { if (mediaQuery.mdAndUp) {
return PopupMenuButton( return Tooltip(
icon: icon, tooltip: TooltipContainer(
tooltip: tooltip, child: Text(tooltip),
offset: offset,
child: child == null ? null : IgnorePointer(child: child),
itemBuilder: (context) => children
.map(
(item) => PopupMenuItem(
padding: EdgeInsets.zero,
enabled: false,
child: _AdaptivePopSheetListItem(
item: item,
onSelected: onSelected,
),
),
)
.toList(),
);
}
void showSheet() {
showModalBottomSheet(
context: context,
useRootNavigator: useRootNavigator,
isScrollControlled: true,
showDragHandle: true,
constraints: BoxConstraints(
maxHeight: mediaQuery.size.height * 0.6,
), ),
builder: (context) { child: IconButton(
return Padding( variance: variance,
padding: const EdgeInsets.all(8.0).copyWith(top: 0), icon: icon ?? const Icon(SpotubeIcons.moreVertical),
child: DefaultTextStyle( onPressed: () {
style: theme.textTheme.titleMedium!, final renderBox = context.findRenderObject() as RenderBox;
child: SingleChildScrollView( final position = RelativeRect.fromRect(
child: Column( Rect.fromPoints(
mainAxisSize: MainAxisSize.min, renderBox.localToGlobal(Offset.zero,
children: [ ancestor: context.findRenderObject()),
if (headings != null) ...[ renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero),
...headings!, ancestor: context.findRenderObject()),
const SizedBox(height: 8),
Divider(
color: theme.colorScheme.primary,
thickness: 0.3,
endIndent: 16,
indent: 16,
),
],
...children.map(
(item) => _AdaptivePopSheetListItem(
item: item,
onSelected: onSelected,
),
)
],
),
), ),
), Offset.zero & mediaQuery.size,
); );
}, final offset = Offset(position.left, position.top);
showDropdownMenu(context, offset);
},
),
); );
} }
if (child != null) { if (child != null) {
return Tooltip( return Tooltip(
message: tooltip ?? '', tooltip: TooltipContainer(child: Text(tooltip)),
child: InkWell( child: Button(
onTap: showSheet, onPressed: () => showDropdownMenu(context, Offset.zero),
borderRadius: borderRadius, style: variance,
child: IgnorePointer(child: child), child: IgnorePointer(child: child),
), ),
); );
} }
return IconButton( return Tooltip(
icon: icon ?? const Icon(SpotubeIcons.moreVertical), tooltip: TooltipContainer(child: Text(tooltip)),
tooltip: tooltip, child: IconButton(
style: theme.iconButtonTheme.style?.copyWith( variance: variance,
shape: WidgetStatePropertyAll( icon: icon ?? const Icon(SpotubeIcons.moreVertical),
RoundedRectangleBorder( onPressed: () => showDropdownMenu(context, Offset.zero),
borderRadius: borderRadius,
),
),
),
onPressed: showSheet,
);
}
}
class _AdaptivePopSheetListItem<T> extends StatelessWidget {
final PopSheetEntry<T> item;
final ValueChanged<T>? onSelected;
const _AdaptivePopSheetListItem({
super.key,
required this.item,
this.onSelected,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?)
?.borderRadius as BorderRadius? ??
const BorderRadius.all(Radius.circular(10)),
onTap: !item.enabled
? null
: () {
item.onTap?.call();
if (item.value != null) {
Navigator.pop(context);
onSelected?.call(item.value as T);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: IconTheme.merge(
data: const IconThemeData(opacity: 1),
child: IgnorePointer(child: item),
),
), ),
); );
} }

Some files were not shown because too many files have changed in this diff Show More