Merge branch 'dev' into website
@ -14,3 +14,4 @@ LASTFM_API_SECRET=$LASTFM_API_SECRET
|
||||
RELEASE_CHANNEL=$RELEASE_CHANNEL
|
||||
|
||||
HIDE_DONATIONS=$HIDE_DONATIONS
|
||||
DISABLE_SPOTIFY_IMAGES=$DISABLE_SPOTIFY_IMAGES
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.24.5"
|
||||
"flutterSdkVersion": "3.29.0"
|
||||
}
|
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -9,7 +9,8 @@ body:
|
||||
attributes:
|
||||
label: Is there an existing issue for this? (Please read the 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.
|
||||
|
||||
Try with multiple similar keywords, and check the closed issues too.
|
||||
@ -60,7 +61,7 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
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.
|
||||
validations:
|
||||
required: true
|
||||
@ -96,7 +97,10 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
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:
|
||||
- label: I'm ready to work on this issue!
|
||||
required: false
|
||||
|
3
.github/workflows/pr-lint.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: 3.24.5
|
||||
FLUTTER_VERSION: 3.29.0
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@ -28,7 +28,6 @@ jobs:
|
||||
RELEASE_CHANNEL: nightly
|
||||
HIDE_DONATIONS: 0
|
||||
|
||||
|
||||
- name: Configure repo
|
||||
run: |
|
||||
flutter pub get
|
||||
|
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to publish (x.x.x)
|
||||
default: 3.8.3
|
||||
default: 4.0.0
|
||||
required: true
|
||||
dry_run:
|
||||
description: Dry run
|
||||
|
87
.github/workflows/spotube-release-binary.yml
vendored
@ -20,7 +20,8 @@ on:
|
||||
description: Dry run without uploading to release
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: 3.24.5
|
||||
FLUTTER_VERSION: 3.29.0
|
||||
FLUTTER_CHANNEL: master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -30,64 +31,72 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04
|
||||
platform: linux
|
||||
arch: x86
|
||||
files: |
|
||||
dist/Spotube-linux-x86_64.deb
|
||||
dist/Spotube-linux-x86_64.rpm
|
||||
dist/spotube-linux-*-x86_64.tar.xz
|
||||
- os: ubuntu-latest
|
||||
platform: linux_arm
|
||||
- os: ubuntu-22.04-arm
|
||||
platform: linux
|
||||
arch: arm64
|
||||
files: |
|
||||
dist/Spotube-linux-aarch64.deb
|
||||
dist/spotube-linux-*-aarch64.tar.xz
|
||||
- os: ubuntu-latest
|
||||
- os: ubuntu-22.04
|
||||
platform: android
|
||||
arch: all
|
||||
files: |
|
||||
build/Spotube-android-all-arch.apk
|
||||
build/Spotube-playstore-all-arch.aab
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
arch: x86
|
||||
files: |
|
||||
dist/Spotube-windows-x86_64.nupkg
|
||||
dist/Spotube-windows-x86_64-setup.exe
|
||||
- os: macos-latest
|
||||
platform: ios
|
||||
arch: all
|
||||
files: |
|
||||
Spotube-iOS.ipa
|
||||
- os: macos-14
|
||||
platform: macos
|
||||
arch: all
|
||||
files: |
|
||||
build/Spotube-macos-universal.dmg
|
||||
build/Spotube-macos-universal.pkg
|
||||
runs-on: ${{matrix.os}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2.12.0
|
||||
- uses: subosito/flutter-action@v2.18.0
|
||||
with:
|
||||
cache: true
|
||||
cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: ${{ env.FLUTTER_CHANNEL }}
|
||||
cache: true
|
||||
git-source: https://github.com/flutter/flutter.git
|
||||
|
||||
- name: Setup Java
|
||||
if: ${{matrix.platform == 'android'}}
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
cache: "gradle"
|
||||
check-latest: true
|
||||
- name: Set up QEMU
|
||||
if: ${{matrix.platform == 'linux_arm'}}
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{matrix.platform == 'linux_arm'}}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
if: ${{matrix.platform != 'linux_arm'}}
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
flutter pub get
|
||||
@ -99,28 +108,16 @@ jobs:
|
||||
echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
|
||||
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
|
||||
run: dart cli/cli.dart build ${{matrix.platform}}
|
||||
run: dart cli/cli.dart build --arch=${{matrix.arch}} ${{matrix.platform}}
|
||||
env:
|
||||
CHANNEL: ${{inputs.channel}}
|
||||
DOTENV: ${{secrets.DOTENV_RELEASE}}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
name: ${{matrix.platform}}-${{matrix.arch}}
|
||||
path: ${{matrix.files}}
|
||||
|
||||
- name: Debug With SSH When fails
|
||||
@ -130,14 +127,13 @@ jobs:
|
||||
limit-access-to-actor: true
|
||||
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build_platform
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Spotube-Release-Binaries
|
||||
path: ./Spotube-Release-Binaries
|
||||
|
||||
- name: Install dependencies
|
||||
@ -146,18 +142,19 @@ jobs:
|
||||
- name: Generate Checksums
|
||||
run: |
|
||||
tree .
|
||||
md5sum Spotube-Release-Binaries/* >> RELEASE.md5sum
|
||||
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
|
||||
find Spotube-Release-Binaries -type f -exec md5sum {} \; >> RELEASE.md5sum
|
||||
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
|
||||
|
||||
- name: Extract pubspec version
|
||||
run: |
|
||||
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
name: sums
|
||||
path: |
|
||||
RELEASE.md5sum
|
||||
RELEASE.sha256sum
|
||||
@ -172,7 +169,7 @@ jobs:
|
||||
omitNameDuringUpdate: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
allowUpdates: true
|
||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||
artifacts: Spotube-Release-Binaries/**/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||
|
||||
- name: Upload Release Binaries (nightly)
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'nightly' }}
|
||||
@ -184,9 +181,15 @@ jobs:
|
||||
omitNameDuringUpdate: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
allowUpdates: true
|
||||
artifacts: Spotube-Release-Binaries/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||
artifacts: Spotube-Release-Binaries/**/*,RELEASE.sha256sum,RELEASE.md5sum
|
||||
body: |
|
||||
Build Number: ${{github.run_number}}
|
||||
|
||||
Nightly release includes newest features but may contain bugs
|
||||
It is preferred to use the stable version unless you know what you're doing
|
||||
|
||||
- name: Debug With SSH When fails
|
||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
|
3
.gitignore
vendored
@ -80,3 +80,6 @@ tm.json
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
|
||||
android/build
|
||||
android/app/.cxx
|
||||
|
11
.vscode/launch.json
vendored
@ -30,6 +30,17 @@
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "spotube (mobile) (release)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "release",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev"
|
||||
]
|
||||
}
|
||||
],
|
||||
"compounds": []
|
||||
|
3
.vscode/settings.json
vendored
@ -13,6 +13,7 @@
|
||||
"RGBO",
|
||||
"riverpod",
|
||||
"Scrobblenaut",
|
||||
"shadcn",
|
||||
"skeletonizer",
|
||||
"songlink",
|
||||
"speechiness",
|
||||
@ -27,5 +28,5 @@
|
||||
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
||||
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
|
||||
},
|
||||
"dart.flutterSdkPath": ".fvm/flutter_sdk"
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.29.0"
|
||||
}
|
1144
CHANGELOG.md
6
LICENSE
@ -1,12 +1,12 @@
|
||||
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:
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
7
Makefile
@ -46,3 +46,10 @@ gensums:
|
||||
|
||||
migrate:
|
||||
dart run drift_dev make-migrations
|
||||
|
||||
dmg:
|
||||
flutter build macos &&\
|
||||
if [ -f dist/Spotube-macos-universal.dmg ];\
|
||||
then rm dist/Spotube-macos-universal.dmg;\
|
||||
fi &&\
|
||||
appdmg appdmg.json dist/Spotube-macos-universal.dmg
|
38
README.md
@ -110,7 +110,7 @@ This handy table lists all the methods you can use to install Spotube:
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td>Debian/Ubuntu</td>
|
||||
@ -207,10 +207,15 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
</summary>
|
||||
|
||||
### 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. [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. [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. [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. [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
|
||||
@ -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.
|
||||
|
||||
### Dependencies
|
||||
|
||||
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
||||
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
|
||||
1. [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_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. [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. [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. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
||||
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. [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. [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. [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. [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.
|
||||
@ -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_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. [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_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_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_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_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_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. [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. [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. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
||||
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
||||
1. [home_widget](https://pub.dev/packages/home_widget) - A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS.
|
||||
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_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. [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. [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. [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.
|
||||
@ -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. [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. [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. [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. [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_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. [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. [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.
|
||||
@ -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. [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. [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. [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. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
|
||||
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
|
||||
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
|
||||
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
|
||||
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
||||
1. [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. [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. [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. [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. [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. [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. [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>
|
||||
|
||||
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
||||
|
@ -32,8 +32,6 @@ linter:
|
||||
analyzer:
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
plugins:
|
||||
- custom_lint
|
||||
exclude:
|
||||
- "**.freezed.dart"
|
||||
- "**.g.dart"
|
||||
|
@ -28,12 +28,17 @@ if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
def composeVersion = "1.4.8"
|
||||
|
||||
ndkVersion "25.1.8937393"
|
||||
android {
|
||||
namespace "oss.krtirtho.spotube"
|
||||
|
||||
compileSdkVersion 35
|
||||
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@ -46,10 +51,18 @@ android {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion "$composeVersion" // Correlates with org.jetbrains.kotlin.android plugin in settings.gradle
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "oss.krtirtho.spotube"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
@ -63,6 +76,7 @@ android {
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
@ -96,15 +110,30 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
def glanceVersion = "1.1.1"
|
||||
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
|
||||
implementation 'com.android.support:multidex:2.0.1'
|
||||
|
||||
implementation "androidx.glance:glance-appwidget:$glanceVersion"
|
||||
implementation "androidx.glance:glance-appwidget-preview:$glanceVersion"
|
||||
implementation "androidx.glance:glance-preview:$glanceVersion"
|
||||
implementation "androidx.glance:glance-material3:$glanceVersion"
|
||||
implementation "androidx.glance:glance-material:$glanceVersion"
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
}
|
20
android/app/proguard-rules.pro
vendored
@ -1 +1,21 @@
|
||||
-keep class androidx.lifecycle.DefaultLifecycleObserver
|
||||
|
||||
-keepnames class kotlinx.serialization.** { *; }
|
||||
-keepnames class oss.krtirtho.spotube.glance.models.** { *; }
|
||||
-keep @kotlinx.serialization.Serializable class *
|
||||
-keepclassmembers class ** {
|
||||
@kotlinx.serialization.* <fields>;
|
||||
}
|
||||
|
||||
## We don't need beans
|
||||
-dontwarn java.beans.BeanDescriptor
|
||||
-dontwarn java.beans.BeanInfo
|
||||
-dontwarn java.beans.IntrospectionException
|
||||
-dontwarn java.beans.Introspector
|
||||
-dontwarn java.beans.PropertyDescriptor
|
||||
|
||||
## Rules for NewPipeExtractor
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
@ -1,7 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="oss.krtirtho.spotube">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
@ -1,4 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@ -17,38 +17,36 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:label="@string/app_name_en"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="@string/app_name_en"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
>
|
||||
android:usesCleartextTraffic="true">
|
||||
<!-- Enable Impeller -->
|
||||
<!-- <meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="true" /> -->
|
||||
android:value="false" /> -->
|
||||
|
||||
<activity
|
||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
>
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!--
|
||||
Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI.
|
||||
-->
|
||||
Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ -56,12 +54,13 @@
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="open.spotify.com"
|
||||
/>
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
@ -72,23 +71,30 @@
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with "spotify:// -->
|
||||
<data android:scheme="spotify" />
|
||||
<data android:scheme="spotube" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="es.antonborri.home_widget.action.LAUNCH" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- AudioService Config -->
|
||||
<service android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
@ -96,11 +102,40 @@
|
||||
</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" />
|
||||
|
||||
<!-- 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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,207 @@
|
||||
package oss.krtirtho.spotube.glance
|
||||
|
||||
import HomeWidgetGlanceState
|
||||
import HomeWidgetGlanceStateDefinition
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.action.ActionParameters
|
||||
import androidx.glance.action.actionParametersOf
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.background
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.action.ActionCallback
|
||||
import androidx.glance.appwidget.action.actionRunCallback
|
||||
import androidx.glance.appwidget.background
|
||||
import androidx.glance.appwidget.components.CircleIconButton
|
||||
import androidx.glance.appwidget.components.Scaffold
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
||||
import androidx.glance.preview.Preview
|
||||
import androidx.glance.state.GlanceStateDefinition
|
||||
import com.google.gson.Gson
|
||||
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
|
||||
import es.antonborri.home_widget.actionStartActivity
|
||||
import oss.krtirtho.spotube.MainActivity
|
||||
import oss.krtirtho.spotube.glance.models.Track
|
||||
import oss.krtirtho.spotube.glance.widgets.FlutterAssetImageProvider
|
||||
import oss.krtirtho.spotube.glance.widgets.TrackDetailsView
|
||||
import oss.krtirtho.spotube.glance.widgets.TrackProgress
|
||||
|
||||
val gson = Gson()
|
||||
val serverAddressKey = ActionParameters.Key<String>("serverAddress")
|
||||
|
||||
class Breakpoints {
|
||||
companion object {
|
||||
val SMALL_SQUARE = DpSize(100.dp, 100.dp)
|
||||
val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
|
||||
val BIG_SQUARE = DpSize(250.dp, 250.dp)
|
||||
}
|
||||
}
|
||||
|
||||
class HomePlayerWidget : GlanceAppWidget() {
|
||||
|
||||
override val sizeMode = SizeMode.Responsive(
|
||||
setOf(
|
||||
Breakpoints.SMALL_SQUARE,
|
||||
Breakpoints.HORIZONTAL_RECTANGLE,
|
||||
Breakpoints.BIG_SQUARE
|
||||
)
|
||||
)
|
||||
|
||||
override val stateDefinition: GlanceStateDefinition<*>?
|
||||
get() = HomeWidgetGlanceStateDefinition()
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
GlanceContent(context, currentState())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||
@Preview(widthDp = 100, heightDp = 100)
|
||||
@Composable
|
||||
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
||||
val prefs = currentState.preferences
|
||||
val size = LocalSize.current
|
||||
|
||||
val activeTrackStr = prefs.getString("activeTrack", null)
|
||||
|
||||
val isPlaying = prefs.getBoolean("isPlaying", false)
|
||||
val playbackServerAddress = prefs.getString("playbackServerAddress", null) ?: ""
|
||||
|
||||
var activeTrack: Track? = null
|
||||
if (activeTrackStr != null) {
|
||||
activeTrack = gson.fromJson(activeTrackStr, Track::class.java)
|
||||
}
|
||||
|
||||
|
||||
val playIcon = Icon.createWithResource(context, R.drawable.ic_media_play);
|
||||
val pauseIcon = Icon.createWithResource(context, R.drawable.ic_media_pause);
|
||||
val previousIcon = Icon.createWithResource(context, R.drawable.ic_media_previous);
|
||||
val nextIcon = Icon.createWithResource(context, R.drawable.ic_media_next);
|
||||
|
||||
GlanceTheme {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.cornerRadius(8.dp)
|
||||
.background(
|
||||
color = GlanceTheme.colors.surface.getColor(context)
|
||||
)
|
||||
.clickable {
|
||||
actionStartActivity<MainActivity>(context)
|
||||
}
|
||||
,
|
||||
) {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.background(
|
||||
color =
|
||||
GlanceTheme.colors.surface.getColor(context)
|
||||
.copy(alpha = 0.5f),
|
||||
)
|
||||
.fillMaxSize(),
|
||||
) {}
|
||||
Column(
|
||||
modifier = GlanceModifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
|
||||
TrackDetailsView(activeTrack)
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
if (size != Breakpoints.SMALL_SQUARE) {
|
||||
TrackProgress(prefs)
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
Row(
|
||||
modifier = GlanceModifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
|
||||
) {
|
||||
CircleIconButton(
|
||||
imageProvider = ImageProvider(previousIcon),
|
||||
contentDescription = "Previous",
|
||||
onClick = actionRunCallback<PreviousAction>(
|
||||
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
CircleIconButton(
|
||||
imageProvider =
|
||||
if (isPlaying) ImageProvider(pauseIcon)
|
||||
else ImageProvider(playIcon),
|
||||
contentDescription = "Play/Pause",
|
||||
onClick = actionRunCallback<PlayPauseAction>(
|
||||
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
CircleIconButton(
|
||||
imageProvider = ImageProvider(nextIcon),
|
||||
contentDescription = "Previous",
|
||||
onClick = actionRunCallback<NextAction>(
|
||||
parameters = actionParametersOf(
|
||||
serverAddressKey to playbackServerAddress
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlayPauseAction : InteractiveAction("toggle-playback")
|
||||
class NextAction : InteractiveAction("next")
|
||||
class PreviousAction : InteractiveAction("previous")
|
||||
|
||||
|
||||
abstract class InteractiveAction(val command: String) : ActionCallback {
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
val serverAddress = parameters[serverAddressKey] ?: ""
|
||||
|
||||
Log.d("HomePlayerWidget", "Sending command $command to $serverAddress")
|
||||
|
||||
if (serverAddress == null || serverAddress.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(
|
||||
context,
|
||||
Uri.parse("spotube://playback/$command?serverAddress=$serverAddress")
|
||||
)
|
||||
backgroundIntent.send()
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package oss.krtirtho.spotube.glance
|
||||
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
|
||||
class HomePlayerWidgetReceiver : HomeWidgetGlanceWidgetReceiver<HomePlayerWidget>() {
|
||||
override val glanceAppWidget = HomePlayerWidget()
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package oss.krtirtho.spotube.glance.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AlbumSimple(
|
||||
@SerializedName("album_type")
|
||||
val albumType: AlbumType?,
|
||||
|
||||
@SerializedName("available_markets")
|
||||
val availableMarkets: List<Market>?,
|
||||
|
||||
val href: String?,
|
||||
val id: String?,
|
||||
val images: List<Image>?,
|
||||
val name: String?,
|
||||
|
||||
@SerializedName("release_date")
|
||||
val releaseDate: String?,
|
||||
|
||||
@SerializedName("release_date_precision")
|
||||
val releaseDatePrecision: DatePrecision?,
|
||||
|
||||
val type: String?,
|
||||
val uri: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class AlbumType {
|
||||
album,
|
||||
single,
|
||||
compilation
|
||||
}
|
||||
|
||||
enum class DatePrecision {
|
||||
year,
|
||||
month,
|
||||
day
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package oss.krtirtho.spotube.glance.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Artist(
|
||||
val href: String?,
|
||||
val id: String?,
|
||||
val name: String?,
|
||||
val type: String?,
|
||||
val uri: String?,
|
||||
|
||||
val followers: Followers?,
|
||||
val genres: List<String>?,
|
||||
val images: List<Image>?,
|
||||
|
||||
@SerializedName("popularity")
|
||||
val popularity: Int?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Followers(
|
||||
val total: Int?
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
package oss.krtirtho.spotube.glance.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
val height: Int?,
|
||||
val width: Int?,
|
||||
val path: String,
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package oss.krtirtho.spotube.glance.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Serializable
|
||||
data class Track(
|
||||
val album: AlbumSimple?, val artists: List<Artist>?,
|
||||
|
||||
@SerializedName("available_markets") val availableMarkets: List<Market>?,
|
||||
|
||||
@SerializedName("disc_number") val discNumber: Int?,
|
||||
|
||||
@SerializedName("duration_ms") val durationMs: Int,
|
||||
|
||||
val explicit: Boolean?, val href: String?, val id: String?,
|
||||
|
||||
@SerializedName("is_playable") val isPlayable: Boolean?,
|
||||
|
||||
val name: String?,
|
||||
|
||||
@SerializedName("popularity") val popularity: Int?,
|
||||
|
||||
@SerializedName("preview_url") val previewUrl: String?,
|
||||
|
||||
@SerializedName("track_number") val trackNumber: Int?,
|
||||
|
||||
val type: String?, val uri: String?
|
||||
) {
|
||||
val duration: kotlin.time.Duration
|
||||
get() = durationMs.toLong().milliseconds
|
||||
}
|
||||
|
||||
enum class Market {
|
||||
AD, AE, AF, AG, AI, AL, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BL, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, SS, ST, SV, SX, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, XK, YE, YT, ZA, ZM, ZW,
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package oss.krtirtho.spotube.glance.widgets
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import androidx.glance.ImageProvider
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun Base64ImageProvider(base64: String): ImageProvider {
|
||||
var bytes = Base64.decode(base64, Base64.DEFAULT);
|
||||
|
||||
var bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size);
|
||||
|
||||
return ImageProvider(bitmap)
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package oss.krtirtho.spotube.glance.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.glance.ImageProvider
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun FlutterAssetImageProvider(context: Context, path: String): ImageProvider {
|
||||
var inputStream = context.assets.open("flutter_assets/$path")
|
||||
|
||||
return ImageProvider(
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
)
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package oss.krtirtho.spotube.glance.widgets
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import oss.krtirtho.spotube.glance.Breakpoints
|
||||
import oss.krtirtho.spotube.glance.models.Track
|
||||
|
||||
@Composable
|
||||
fun TrackDetailsView(activeTrack: Track?) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val size = LocalSize.current
|
||||
|
||||
val artistStr = activeTrack?.artists?.map { it.name }?.joinToString(", ") ?: "<No Artist>"
|
||||
val imgLocalPath = activeTrack?.album?.images?.get(0)?.path;
|
||||
val title = activeTrack?.name ?: "<No Track>"
|
||||
|
||||
|
||||
Image(
|
||||
provider =
|
||||
if (imgLocalPath == null)
|
||||
ImageProvider(
|
||||
BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
android.R.drawable.ic_delete
|
||||
)
|
||||
)
|
||||
else ImageProvider(BitmapFactory.decodeFile(imgLocalPath)),
|
||||
contentDescription = "Album Art",
|
||||
modifier = GlanceModifier.cornerRadius(8.dp)
|
||||
.size(
|
||||
if (size.height < 200.dp) 50.dp
|
||||
else 100.dp
|
||||
),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = GlanceTheme.colors.onBackground
|
||||
),
|
||||
)
|
||||
if (size != Breakpoints.SMALL_SQUARE) {
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
Text(
|
||||
text = artistStr,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
color = GlanceTheme.colors.onBackground
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package oss.krtirtho.spotube.glance.widgets
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.appwidget.LinearProgressIndicator
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import oss.krtirtho.spotube.glance.Breakpoints
|
||||
|
||||
fun Duration.format(): String {
|
||||
return this.toComponents { hour, minutes, seconds, nanoseconds ->
|
||||
var paddedSeconds = seconds.toString().padStart(2, '0')
|
||||
var paddedMinutes = minutes.toString().padStart(2, '0')
|
||||
var paddedHour = hour.toString().padStart(2, '0')
|
||||
if (hour == 0L) {
|
||||
"$paddedMinutes:$paddedSeconds"
|
||||
} else {
|
||||
"$paddedHour:$paddedMinutes:$paddedSeconds"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrackProgress(prefs: SharedPreferences) {
|
||||
val size = LocalSize.current
|
||||
val position = prefs.getInt("position", 0).seconds
|
||||
var duration = prefs.getInt("duration", 0).seconds
|
||||
|
||||
var progress = position.inWholeSeconds.toFloat() / max(duration.inWholeSeconds.toFloat(), 1.0f)
|
||||
|
||||
var textStyle =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
)
|
||||
|
||||
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
Text(text = position.format(), style = textStyle)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = GlanceModifier.defaultWeight(),
|
||||
color = GlanceTheme.colors.primary,
|
||||
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
Text(text = duration.format(), style = textStyle)
|
||||
}
|
||||
} else {
|
||||
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
LinearProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = GlanceModifier.fillMaxWidth(),
|
||||
color = GlanceTheme.colors.primary,
|
||||
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
Text(text = position.format(), style = textStyle)
|
||||
Spacer(modifier = GlanceModifier.defaultWeight())
|
||||
Text(text = duration.format(), style = textStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="100dp"
|
||||
android:minHeight="100dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="10000">
|
||||
</appwidget-provider>
|
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="762"
|
||||
android:viewportHeight="762">
|
||||
<path
|
||||
android:pathData="M309.08,370.99L309.08,479.87C309.08,486.36 314.33,491.6 320.83,491.6C327.31,491.6 332.58,486.36 332.58,479.87L332.58,370.99C332.58,364.51 327.31,359.26 320.83,359.26C314.33,359.26 309.08,364.51 309.08,370.99Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="14"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M254.59,491.73L280.46,491.73L280.46,362.47C280.53,361.85 280.64,361.23 280.64,360.6C280.64,304.83 325.72,259.46 381.12,259.46C436.51,259.46 481.59,304.83 481.59,360.6C481.59,361.45 481.71,362.27 481.84,363.1L481.84,491.73L507.71,491.73C525.72,491.73 540.33,476.65 540.33,458.03L540.33,390.62C540.33,375.26 530.37,362.33 516.78,358.26C515.53,284.17 455.17,224.26 381.12,224.26C307.05,224.26 246.69,284.18 245.45,358.29C231.88,362.36 221.96,375.29 221.96,390.63L221.96,458.03C221.96,476.64 236.56,491.73 254.59,491.73Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="20"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M431.08,370.99L431.08,479.87C431.08,486.36 436.33,491.6 442.83,491.6C449.31,491.6 454.58,486.36 454.58,479.87L454.58,370.99C454.58,364.51 449.31,359.26 442.83,359.26C436.33,359.26 431.08,364.51 431.08,370.99Z"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="14"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
@ -1,7 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="oss.krtirtho.spotube">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
@ -1,6 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
#Fri Dec 13 21:53:13 BDT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
||||
|
@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
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
|
||||
}
|
||||
|
||||
include ":app"
|
||||
include ':app'
|
BIN
assets/backgrounds/xmas-effect.png
Normal file
After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 39 KiB |
BIN
assets/mobile-screenshots/android-6.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 1.6 MiB |
BIN
assets/patterns/black_white_visualized.jpg
Normal file
After Width: | Height: | Size: 336 KiB |
BIN
assets/patterns/brazil_carnival.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
assets/patterns/cotton_balls.jpg
Normal file
After Width: | Height: | Size: 498 KiB |
BIN
assets/patterns/cute_worms.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
assets/patterns/flash_cross_axis.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
assets/patterns/memphis_shapes.jpg
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
assets/patterns/oval_gloomy.jpg
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
assets/patterns/oval_sunny.jpg
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
assets/patterns/red_nimbuses.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
assets/patterns/tree_bark.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
assets/patterns/vibrant_pentagons.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
assets/patterns/wiring_pattern.jpg
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
assets/patterns/zigzags_gloomy.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
assets/patterns/zigzags_sunny.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 750 KiB After Width: | Height: | Size: 1006 KiB |
@ -1,8 +1,8 @@
|
||||
pkgbase = spotube-bin
|
||||
pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
|
||||
pkgver = 3.7.1
|
||||
pkgrel = 2
|
||||
url = https://github.com/KRTirtho/spotube/
|
||||
pkgver = 4.0.0
|
||||
pkgrel = 1
|
||||
url = https://spotube.krtirtho.dev
|
||||
arch = x86_64
|
||||
license = BSD-4-Clause
|
||||
depends = mpv
|
||||
@ -12,6 +12,7 @@ depends = jsoncpp
|
||||
depends = libnotify
|
||||
depends = xdg-user-dirs
|
||||
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
|
||||
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
||||
|
||||
|
@ -5,13 +5,13 @@ pkgrel=%{{PKGREL}}%
|
||||
epoch=
|
||||
pkgdesc="Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!"
|
||||
arch=(x86_64)
|
||||
url="https://github.com/KRTirtho/spotube/"
|
||||
url="https://spotube.krtirtho.dev"
|
||||
license=('BSD-4-Clause')
|
||||
groups=()
|
||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||
makedepends=()
|
||||
checkdepends=()
|
||||
optdepends=()
|
||||
optdepends=('yt-dlp-git')
|
||||
provides=()
|
||||
conflicts=()
|
||||
replaces=()
|
||||
|
10
build.yaml
@ -4,6 +4,16 @@ targets:
|
||||
exclude:
|
||||
- bin/*.dart
|
||||
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:
|
||||
options:
|
||||
any_map: true
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter
|
||||
enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<!-- == PACKAGE SPECIFIC SECTION == -->
|
||||
@ -12,34 +13,39 @@
|
||||
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
||||
<title>spotube (Install)</title>
|
||||
<authors>Kingkor Roy Tirtho</authors>
|
||||
<projectUrl>https://github.com/KRTirtho/spotube/</projectUrl>
|
||||
<iconUrl>https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
||||
<projectUrl>https://spotube.krtirtho.dev</projectUrl>
|
||||
<iconUrl>
|
||||
https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
||||
<copyright>2022 Spotube</copyright>
|
||||
<!-- If there is a license Url available, it is required for the community feed -->
|
||||
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<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>
|
||||
<tags>spotube music audio spotify youtube flutter</tags>
|
||||
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile! </summary>
|
||||
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available
|
||||
for both desktop & mobile! </summary>
|
||||
<description>
|
||||
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
||||
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
||||
friendly User Experience
|
||||
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
||||
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
||||
friendly User Experience
|
||||
|
||||
# Features
|
||||
- Open source/libre software
|
||||
- Anonymous/guest login
|
||||
- Cross platform support
|
||||
- No telemetry, diagnostics or user data collection
|
||||
- Lightweight & resource-friendly
|
||||
- Native performance (Thanks to Flutter+Skia)
|
||||
- Playback control is done locally instead of on the server
|
||||
- Small size & less data usage
|
||||
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support their valuable creations.)
|
||||
- Time synced lyrics
|
||||
- Downloadable tracks
|
||||
# Features
|
||||
- Open source/libre software
|
||||
- Anonymous/guest login
|
||||
- Cross platform support
|
||||
- No telemetry, diagnostics or user data collection
|
||||
- Lightweight & resource-friendly
|
||||
- Native performance (Thanks to Flutter+Skia)
|
||||
- Playback control is done locally instead of on the server
|
||||
- Small size & less data usage
|
||||
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended
|
||||
to support the creators by watching/liking/subscribing to the artists' YouTube channels or
|
||||
liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support
|
||||
their valuable creations.)
|
||||
- Time synced lyrics
|
||||
- Downloadable tracks
|
||||
</description>
|
||||
<releaseNotes>https://github.com/KRTirtho/spotube/releases/tag/v%{{SPOTUBE_VERSION}}%</releaseNotes>
|
||||
</metadata>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
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:
|
||||
|
||||
|
@ -3,7 +3,6 @@ import 'package:args/command_runner.dart';
|
||||
import 'build/android.dart';
|
||||
import 'build/ios.dart';
|
||||
import 'build/linux.dart';
|
||||
import 'build/linux_arm.dart';
|
||||
import 'build/macos.dart';
|
||||
import 'build/windows.dart';
|
||||
|
||||
@ -18,8 +17,13 @@ class BuildCommand extends Command {
|
||||
addSubcommand(AndroidBuildCommand());
|
||||
addSubcommand(IosBuildCommand());
|
||||
addSubcommand(LinuxBuildCommand());
|
||||
addSubcommand(LinuxArmBuildCommand());
|
||||
addSubcommand(MacosBuildCommand());
|
||||
addSubcommand(WindowsBuildCommand());
|
||||
argParser.addOption(
|
||||
"arch",
|
||||
abbr: "a",
|
||||
defaultsTo: "x86",
|
||||
allowed: ["x86", "arm64", "all"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -63,4 +63,6 @@ mixin BuildCommandCommonSteps on Command {
|
||||
""",
|
||||
);
|
||||
}
|
||||
|
||||
String get architecture => parent?.argResults?.option("arch") as String;
|
||||
}
|
||||
|
@ -37,23 +37,32 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
await bootstrap();
|
||||
|
||||
await shell.run(
|
||||
"""
|
||||
flutter_distributor package --platform=linux --targets=deb
|
||||
flutter_distributor package --platform=linux --targets=rpm
|
||||
""",
|
||||
"flutter_distributor package --platform=linux --targets=deb",
|
||||
);
|
||||
|
||||
final tempDir = join(Directory.systemTemp.path, "spotube-tar");
|
||||
if (architecture == "x86") {
|
||||
await shell.run(
|
||||
"flutter_distributor package --platform=linux --targets=rpm",
|
||||
);
|
||||
}
|
||||
|
||||
final bundleDirPath =
|
||||
join(cwd.path, "build", "linux", "x64", "release", "bundle");
|
||||
final tempDir = join(Directory.systemTemp.path, "spotube-tar");
|
||||
final bundleArchName = architecture == "x86" ? "x86_64" : "aarch64";
|
||||
final bundleDirPath = join(
|
||||
cwd.path,
|
||||
"build",
|
||||
"linux",
|
||||
architecture == "x86" ? "x64" : architecture,
|
||||
"release",
|
||||
"bundle",
|
||||
);
|
||||
|
||||
final tarFile = File(join(
|
||||
cwd.path,
|
||||
"dist",
|
||||
"spotube-linux-"
|
||||
"${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber}"
|
||||
"-x86_64.tar.xz",
|
||||
"-$bundleArchName.tar.xz",
|
||||
));
|
||||
|
||||
await copyPath(bundleDirPath, tempDir);
|
||||
@ -81,25 +90,31 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
"spotube-${pubspec.version}-linux.deb",
|
||||
),
|
||||
);
|
||||
|
||||
final ogRpm = File(
|
||||
await ogDeb.copy(
|
||||
join(
|
||||
cwd.path,
|
||||
"dist",
|
||||
pubspec.version.toString(),
|
||||
"spotube-${pubspec.version}-linux.rpm",
|
||||
"Spotube-linux-$bundleArchName.deb",
|
||||
),
|
||||
);
|
||||
|
||||
await ogDeb.copy(
|
||||
join(cwd.path, "dist", "Spotube-linux-x86_64.deb"),
|
||||
);
|
||||
await ogRpm.copy(
|
||||
join(cwd.path, "dist", "Spotube-linux-x86_64.rpm"),
|
||||
);
|
||||
|
||||
await ogDeb.delete();
|
||||
await ogRpm.delete();
|
||||
|
||||
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");
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
|
||||
class LinuxArmBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
@override
|
||||
String get description => "Build Linux Arm";
|
||||
|
||||
@override
|
||||
String get name => "linux_arm";
|
||||
|
||||
@override
|
||||
FutureOr? run() async {
|
||||
await bootstrap();
|
||||
|
||||
await shell.run(
|
||||
"docker buildx build --platform=linux/arm64 "
|
||||
"-f ${join(cwd.path, ".github", "Dockerfile")} ${cwd.path} "
|
||||
"--build-arg FLUTTER_VERSION=${CliEnv.flutterVersion} "
|
||||
"--build-arg BUILD_VERSION=${CliEnv.channel == BuildChannel.nightly ? "nightly" : versionWithoutBuildNumber} "
|
||||
"-t krtirtho/spotube_linux_arm:latest "
|
||||
"--load",
|
||||
);
|
||||
|
||||
await shell.run(
|
||||
"""
|
||||
docker images ls
|
||||
docker create --name spotube_linux_arm krtirtho/spotube_linux_arm:latest
|
||||
docker cp spotube_linux_arm:/app/dist/ dist/
|
||||
""",
|
||||
);
|
||||
}
|
||||
}
|
@ -24,6 +24,13 @@ class InstallDependenciesCommand extends Command {
|
||||
],
|
||||
mandatory: true,
|
||||
);
|
||||
|
||||
argParser.addOption(
|
||||
"arch",
|
||||
abbr: "a",
|
||||
allowed: ["x86", "arm64", "all"],
|
||||
defaultsTo: "x86",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -41,14 +48,6 @@ class InstallDependenciesCommand extends Command {
|
||||
""",
|
||||
);
|
||||
break;
|
||||
case "linux_arm":
|
||||
await shell.run(
|
||||
"""
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y pkg-config make python3-pip python3-setuptools
|
||||
""",
|
||||
);
|
||||
break;
|
||||
case "macos":
|
||||
await shell.run(
|
||||
"""
|
||||
|
1
drift_schemas/app_db/drift_schema_v4.json
Normal file
@ -1,13 +0,0 @@
|
||||
flutter_launcher_icons:
|
||||
ios: true
|
||||
android: true
|
||||
image_path: "assets/spotube-logo.png"
|
||||
adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg"
|
||||
adaptive_icon_background: "#242832"
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "assets/spotube-logo.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "assets/spotube-logo-macos.png"
|
29
flutter_launcher_icons.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
# flutter pub run flutter_launcher_icons
|
||||
flutter_launcher_icons:
|
||||
image_path: "assets/spotube-logo.png"
|
||||
|
||||
android: true
|
||||
# image_path_android: "assets/icon/icon.png"
|
||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||
adaptive_icon_background: "#242832"
|
||||
adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg"
|
||||
# adaptive_icon_monochrome: "assets/icon/monochrome.png"
|
||||
|
||||
ios: true
|
||||
# image_path_ios: "assets/icon/icon.png"
|
||||
remove_alpha_channel_ios: true
|
||||
# image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
|
||||
# image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
|
||||
# desaturate_tinted_to_grayscale_ios: true
|
||||
|
||||
web:
|
||||
generate: false
|
||||
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "assets/spotube-logo.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "assets/spotube-logo-macos.png"
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
ios/HomePlayerWidget/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
86
ios/HomePlayerWidget/HomePlayerWidget.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// HomePlayerWidget.swift
|
||||
// HomePlayerWidget
|
||||
//
|
||||
// Created by Kingkor Roy Tirtho on 15/12/24.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
private let widgetGroupId = "group.spotube_home_player_widget"
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct HomePlayerWidgetEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomePlayerWidget: Widget {
|
||||
let kind: String = "HomePlayerWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
HomePlayerWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
HomePlayerWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
HomePlayerWidget()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
16
ios/HomePlayerWidget/HomePlayerWidgetBundle.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// HomePlayerWidgetBundle.swift
|
||||
// HomePlayerWidget
|
||||
//
|
||||
// Created by Kingkor Roy Tirtho on 15/12/24.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct HomePlayerWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
HomePlayerWidget()
|
||||
}
|
||||
}
|
11
ios/HomePlayerWidget/Info.plist
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
10
ios/HomePlayerWidgetExtension.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.spotube_home_player_widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
@ -64,6 +64,8 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_sharing_intent (0.0.1):
|
||||
- Flutter
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
@ -106,12 +108,15 @@ PODS:
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- sqlite3 (~> 3.47.0)
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.47.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.4)
|
||||
- system_theme (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
@ -130,6 +135,7 @@ DEPENDENCIES:
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/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`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/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`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/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`)
|
||||
|
||||
SPEC REPOS:
|
||||
@ -182,6 +189,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_sharing_intent:
|
||||
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
integration_test:
|
||||
@ -205,7 +214,9 @@ EXTERNAL SOURCES:
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
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:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
@ -226,6 +237,7 @@ SPEC CHECKSUMS:
|
||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
|
||||
@ -240,8 +252,9 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: 1e522f0938463e44b7faf50393b40bdc1e1e456d
|
||||
sqlite3_flutter_libs: b55ef23cfafea5318ae5081e0bf3fbbce8417c94
|
||||
sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
system_theme: bfc1b0913d08f38d8c6bbe94b202a58df599d9f7
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
||||
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
||||
|
@ -48,6 +48,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
@ -1,12 +1,17 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@main
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> 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)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-nightly-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-nightly-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-nightly-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-nightly-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-nightly-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-nightly-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-nightly-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-nightly-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-nightly-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-nightly-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-nightly-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-nightly-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-nightly-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-nightly-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-nightly-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
@ -72,5 +72,11 @@
|
||||
<array>
|
||||
<string>_spotube._tcp</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
10
ios/Runner/Runner.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.spotube_home_player_widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
10
ios/dev.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.spotube_home_player_widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
10
ios/nightly.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.spotube_home_player_widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
10
ios/stable.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.spotube_home_player_widget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
@ -9,6 +9,17 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
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 {
|
||||
const $AssetsLogosGen();
|
||||
|
||||
@ -24,6 +35,84 @@ class $AssetsLogosGen {
|
||||
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 {
|
||||
const $AssetsTutorialGen();
|
||||
|
||||
@ -46,6 +135,7 @@ class Assets {
|
||||
static const String license = 'LICENSE';
|
||||
static const AssetGenImage albumPlaceholder =
|
||||
AssetGenImage('assets/album-placeholder.png');
|
||||
static const $AssetsBackgroundsGen backgrounds = $AssetsBackgroundsGen();
|
||||
static const AssetGenImage bengaliPatternsBg =
|
||||
AssetGenImage('assets/bengali-patterns-bg.jpg');
|
||||
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
|
||||
@ -55,12 +145,15 @@ class Assets {
|
||||
static const AssetGenImage likedTracks =
|
||||
AssetGenImage('assets/liked-tracks.jpg');
|
||||
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||
static const $AssetsPatternsGen patterns = $AssetsPatternsGen();
|
||||
static const AssetGenImage placeholder =
|
||||
AssetGenImage('assets/placeholder.png');
|
||||
static const AssetGenImage spotubeHeroBanner =
|
||||
AssetGenImage('assets/spotube-hero-banner.png');
|
||||
static const AssetGenImage spotubeLogoForeground =
|
||||
AssetGenImage('assets/spotube-logo-foreground.jpg');
|
||||
static const AssetGenImage spotubeLogoMacos =
|
||||
AssetGenImage('assets/spotube-logo-macos.png');
|
||||
static const AssetGenImage spotubeLogoBmp =
|
||||
AssetGenImage('assets/spotube-logo.bmp');
|
||||
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
|
||||
@ -104,6 +197,7 @@ class Assets {
|
||||
placeholder,
|
||||
spotubeHeroBanner,
|
||||
spotubeLogoForeground,
|
||||
spotubeLogoMacos,
|
||||
spotubeLogoBmp,
|
||||
spotubeLogoIco,
|
||||
spotubeLogoPng,
|
||||
|
@ -38,6 +38,11 @@ abstract class Env {
|
||||
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
||||
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"
|
||||
? ReleaseChannel.stable
|
||||
: ReleaseChannel.nightly;
|
||||
|
24
lib/collections/fonts.gen.dart
Normal file
@ -0,0 +1,24 @@
|
||||
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
/// *****************************************************
|
||||
/// FlutterGen
|
||||
/// *****************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
|
||||
|
||||
class FontFamily {
|
||||
FontFamily._();
|
||||
|
||||
/// Font family: BootstrapIcons
|
||||
static const String bootstrapIcons = 'BootstrapIcons';
|
||||
|
||||
/// Font family: GeistMono
|
||||
static const String geistMono = 'GeistMono';
|
||||
|
||||
/// Font family: GeistSans
|
||||
static const String geistSans = 'GeistSans';
|
||||
|
||||
/// Font family: RadixIcons
|
||||
static const String radixIcons = 'RadixIcons';
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
const gradients = [
|
||||
LinearGradient(colors: [
|
||||
|
@ -3,13 +3,9 @@ import 'dart:io';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.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.gr.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/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
@ -36,7 +32,7 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
|
||||
}
|
||||
|
||||
class NavigationIntent extends Intent {
|
||||
final GoRouter router;
|
||||
final AppRouter router;
|
||||
final String path;
|
||||
const NavigationIntent(this.router, this.path);
|
||||
}
|
||||
@ -44,7 +40,7 @@ class NavigationIntent extends Intent {
|
||||
class NavigationAction extends Action<NavigationIntent> {
|
||||
@override
|
||||
invoke(intent) {
|
||||
intent.router.go(intent.path);
|
||||
intent.router.navigateNamed(intent.path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -52,32 +48,49 @@ class NavigationAction extends Action<NavigationIntent> {
|
||||
enum HomeTabs {
|
||||
browse,
|
||||
search,
|
||||
library,
|
||||
|
||||
lyrics,
|
||||
userPlaylists,
|
||||
userArtists,
|
||||
userAlbums,
|
||||
userLocalLibrary,
|
||||
userDownloads,
|
||||
}
|
||||
|
||||
class HomeTabIntent extends Intent {
|
||||
final WidgetRef ref;
|
||||
final AppRouter router;
|
||||
final HomeTabs tab;
|
||||
const HomeTabIntent(this.ref, {required this.tab});
|
||||
const HomeTabIntent(this.router, {required this.tab});
|
||||
}
|
||||
|
||||
class HomeTabAction extends Action<HomeTabIntent> {
|
||||
@override
|
||||
invoke(intent) {
|
||||
final router = intent.ref.read(routerProvider);
|
||||
final router = intent.router;
|
||||
switch (intent.tab) {
|
||||
case HomeTabs.browse:
|
||||
router.goNamed(HomePage.name);
|
||||
router.navigate(const HomeRoute());
|
||||
break;
|
||||
case HomeTabs.search:
|
||||
router.goNamed(SearchPage.name);
|
||||
break;
|
||||
case HomeTabs.library:
|
||||
router.goNamed(LibraryPage.name);
|
||||
router.navigate(const SearchRoute());
|
||||
break;
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
|
@ -1,329 +1,235 @@
|
||||
import 'package:flutter/foundation.dart' hide Category;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Search;
|
||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||
import 'package:spotube/pages/album/album.dart';
|
||||
import 'package:spotube/pages/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/collections/routes.gr.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.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 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) {
|
||||
return "/getting-started";
|
||||
}
|
||||
@AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
|
||||
class AppRouter extends RootStackRouter {
|
||||
final WidgetRef ref;
|
||||
|
||||
return null;
|
||||
},
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: HomePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "genres",
|
||||
name: GenrePage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: GenrePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "genre/:categoryId",
|
||||
name: GenrePlaylistsPage.name,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: GenrePlaylistsPage(
|
||||
category: state.extra as Category,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "feeds/:feedId",
|
||||
name: HomeFeedSectionPage.name,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: HomeFeedSectionPage(
|
||||
sectionUri: state.pathParameters["feedId"] as String,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/search",
|
||||
name: SearchPage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: SearchPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/library",
|
||||
name: LibraryPage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LibraryPage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "generate",
|
||||
name: PlaylistGeneratorPage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "result",
|
||||
name: PlaylistGenerateResultPage.name,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: PlaylistGenerateResultPage(
|
||||
state: state.extra as GeneratePlaylistProviderInput,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "local",
|
||||
name: LocalLibraryPage.name,
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is String);
|
||||
return SpotubePage(
|
||||
child: LocalLibraryPage(
|
||||
state.extra as String,
|
||||
isDownloads:
|
||||
state.uri.queryParameters["downloads"] != null,
|
||||
isCache: state.uri.queryParameters["cache"] != null,
|
||||
),
|
||||
);
|
||||
AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
|
||||
|
||||
@override
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(
|
||||
page: RootAppRoute.page,
|
||||
path: "/",
|
||||
initial: true,
|
||||
children: [
|
||||
AutoRoute(
|
||||
path: "home",
|
||||
page: HomeRoute.page,
|
||||
initial: true,
|
||||
guards: [
|
||||
AutoRouteGuardCallback(
|
||||
(resolver, router) async {
|
||||
final auth = await ref.read(authenticationProvider.future);
|
||||
|
||||
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||
resolver.redirect(const GettingStartedRoute());
|
||||
} else {
|
||||
resolver.next(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: "/lyrics",
|
||||
name: LyricsPage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LyricsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/settings",
|
||||
name: SettingsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: SettingsPage(),
|
||||
],
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "blacklist",
|
||||
name: BlackListPage.name,
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const BlackListPage(),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
GoRoute(
|
||||
path: "logs",
|
||||
name: LogsPage.name,
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const LogsPage(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "about",
|
||||
name: AboutSpotube.name,
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const AboutSpotube(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
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(),
|
||||
AutoRoute(
|
||||
path: "home/genres",
|
||||
page: GenreRoute.page,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "control",
|
||||
name: ConnectControlPage.name,
|
||||
pageBuilder: (context, state) {
|
||||
return const SpotubePage(
|
||||
child: ConnectControlPage(),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/profile",
|
||||
name: ProfilePage.name,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: ProfilePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/stats",
|
||||
name: StatsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsPage(),
|
||||
AutoRoute(
|
||||
path: "home/genre/:categoryId",
|
||||
page: GenrePlaylistsRoute.page,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "minutes",
|
||||
name: StatsMinutesPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsMinutesPage(),
|
||||
AutoRoute(
|
||||
path: "home/feeds/:feedId",
|
||||
page: HomeFeedSectionRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "search",
|
||||
page: SearchRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "library",
|
||||
page: LibraryRoute.page,
|
||||
children: [
|
||||
AutoRoute(
|
||||
path: "playlists",
|
||||
page: UserPlaylistsRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "streams",
|
||||
name: StatsStreamsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsStreamsPage(),
|
||||
AutoRoute(
|
||||
path: "artists",
|
||||
page: UserArtistsRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "fees",
|
||||
name: StatsStreamFeesPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsStreamFeesPage(),
|
||||
AutoRoute(
|
||||
path: "albums",
|
||||
page: UserAlbumsRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "artists",
|
||||
name: StatsArtistsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsArtistsPage(),
|
||||
AutoRoute(
|
||||
path: "local",
|
||||
page: UserLocalLibraryRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "albums",
|
||||
name: StatsAlbumsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsAlbumsPage(),
|
||||
AutoRoute(
|
||||
path: "local/folder",
|
||||
page: LocalLibraryRoute.page,
|
||||
// parentNavigatorKey: shellRouteNavigatorKey,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "playlists",
|
||||
name: StatsPlaylistsPage.name,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: StatsPlaylistsPage(),
|
||||
AutoRoute(
|
||||
path: "downloads",
|
||||
page: UserDownloadsRoute.page,
|
||||
),
|
||||
],
|
||||
),
|
||||
AutoRoute(
|
||||
path: "library/generate",
|
||||
page: PlaylistGeneratorRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "library/generate/result",
|
||||
page: PlaylistGenerateResultRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "lyrics",
|
||||
page: LyricsRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "settings",
|
||||
page: SettingsRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "settings/blacklist",
|
||||
page: BlackListRoute.page,
|
||||
),
|
||||
if (!kIsWeb)
|
||||
AutoRoute(
|
||||
path: "settings/logs",
|
||||
page: LogsRoute.page,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/mini-player",
|
||||
name: MiniLyricsPage.name,
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||
AutoRoute(
|
||||
path: "settings/about",
|
||||
page: AboutSpotubeRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "album/:id",
|
||||
page: AlbumRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "artist/:id",
|
||||
page: ArtistRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "liked-tracks",
|
||||
page: LikedPlaylistRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "playlist/:id",
|
||||
page: PlaylistRoute.page,
|
||||
guards: [
|
||||
AutoRouteGuard.redirect(
|
||||
(resolver) {
|
||||
final PlaylistRouteArgs(:id, :playlist) =
|
||||
resolver.route.args as PlaylistRouteArgs;
|
||||
if (id == "user-liked-tracks") {
|
||||
return LikedPlaylistRoute(playlist: playlist);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/getting-started",
|
||||
name: GettingStarting.name,
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: GettingStarting(),
|
||||
CustomRoute(
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 200,
|
||||
path: "/player/queue",
|
||||
page: PlayerQueueRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
name: WebViewLogin.name,
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: WebViewLogin(),
|
||||
CustomRoute(
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 200,
|
||||
path: "/player/sources",
|
||||
page: PlayerTrackSourcesRoute.page,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/lastfm-login",
|
||||
name: LastFMLoginPage.name,
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LastFMLoginPage()),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
CustomRoute(
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 200,
|
||||
path: "/player/lyrics",
|
||||
page: PlayerLyricsRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "/mini-player",
|
||||
page: MiniLyricsRoute.page,
|
||||
// parentNavigatorKey: rootNavigatorKey,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "/getting-started",
|
||||
page: GettingStartedRoute.page,
|
||||
// parentNavigatorKey: rootNavigatorKey,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "/login",
|
||||
page: WebViewLoginRoute.page,
|
||||
// parentNavigatorKey: rootNavigatorKey,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "/lastfm-login",
|
||||
page: LastFMLoginRoute.page,
|
||||
// parentNavigatorKey: rootNavigatorKey,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
1174
lib/collections/routes.gr.dart
Normal file
@ -1,81 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package: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 {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String id;
|
||||
final String name;
|
||||
final String pathPrefix;
|
||||
final PageRouteInfo route;
|
||||
|
||||
SideBarTiles({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.route,
|
||||
required this.pathPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
||||
SideBarTiles(
|
||||
id: "browse",
|
||||
name: HomePage.name,
|
||||
id: "home",
|
||||
pathPrefix: "/home",
|
||||
route: const HomeRoute(),
|
||||
icon: SpotubeIcons.home,
|
||||
title: l10n.browse,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "search",
|
||||
name: SearchPage.name,
|
||||
pathPrefix: "/search",
|
||||
route: const SearchRoute(),
|
||||
icon: SpotubeIcons.search,
|
||||
title: l10n.search,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "library",
|
||||
name: LibraryPage.name,
|
||||
icon: SpotubeIcons.library,
|
||||
title: l10n.library,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "lyrics",
|
||||
name: LyricsPage.name,
|
||||
pathPrefix: "/lyrics",
|
||||
route: LyricsRoute(),
|
||||
icon: SpotubeIcons.music,
|
||||
title: l10n.lyrics,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "stats",
|
||||
name: StatsPage.name,
|
||||
pathPrefix: "/stats",
|
||||
route: const StatsRoute(),
|
||||
icon: SpotubeIcons.chart,
|
||||
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) => [
|
||||
SideBarTiles(
|
||||
id: "browse",
|
||||
name: HomePage.name,
|
||||
id: "home",
|
||||
pathPrefix: "/home",
|
||||
route: const HomeRoute(),
|
||||
icon: SpotubeIcons.home,
|
||||
title: l10n.browse,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "search",
|
||||
name: SearchPage.name,
|
||||
pathPrefix: "/search",
|
||||
route: const SearchRoute(),
|
||||
icon: SpotubeIcons.search,
|
||||
title: l10n.search,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "library",
|
||||
name: LibraryPage.name,
|
||||
pathPrefix: "/library",
|
||||
route: const UserPlaylistsRoute(),
|
||||
icon: SpotubeIcons.library,
|
||||
title: l10n.library,
|
||||
),
|
||||
SideBarTiles(
|
||||
id: "stats",
|
||||
name: StatsPage.name,
|
||||
pathPrefix: "/stats",
|
||||
route: const StatsRoute(),
|
||||
icon: SpotubeIcons.chart,
|
||||
title: l10n.stats,
|
||||
),
|
||||
|
@ -1,5 +1,5 @@
|
||||
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:simple_icons/simple_icons.dart';
|
||||
|
||||
@ -37,6 +37,7 @@ abstract class SpotubeIcons {
|
||||
static const share = FeatherIcons.share2;
|
||||
static const playlistAdd = Icons.playlist_add_rounded;
|
||||
static const playlistRemove = Icons.playlist_remove_rounded;
|
||||
static const playlist = Icons.playlist_play_rounded;
|
||||
static const trash = FeatherIcons.trash2;
|
||||
static const clock = FeatherIcons.clock;
|
||||
static const lyrics = Icons.lyrics_rounded;
|
||||
@ -127,4 +128,11 @@ abstract class SpotubeIcons {
|
||||
static const cache = FeatherIcons.hardDrive;
|
||||
static const export = Icons.file_open_outlined;
|
||||
static const delete = FeatherIcons.trash2;
|
||||
static const open = FeatherIcons.externalLink;
|
||||
static const radioChecked = Icons.radio_button_on_rounded;
|
||||
static const radioUnchecked = Icons.radio_button_off_rounded;
|
||||
static const grid = FeatherIcons.grid;
|
||||
static const list = FeatherIcons.list;
|
||||
static const device = FeatherIcons.smartphone;
|
||||
static const engine = FeatherIcons.server;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
|
||||
class AdaptiveListTile extends HookWidget {
|
||||
@ -24,41 +25,39 @@ class AdaptiveListTile extends HookWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return ListTile(
|
||||
return ButtonTile(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: breakOn ?? mediaQuery.smAndDown
|
||||
? null
|
||||
: trailing?.call(context, null),
|
||||
leading: leading,
|
||||
onTap: breakOn ?? mediaQuery.smAndDown
|
||||
? () {
|
||||
onTap?.call();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, update) {
|
||||
return AlertDialog(
|
||||
title: title != null
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (leading != null) ...[
|
||||
leading!,
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
Flexible(child: title!),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
content: trailing?.call(context, update),
|
||||
);
|
||||
});
|
||||
},
|
||||
enabled: breakOn ?? mediaQuery.smAndDown,
|
||||
onPressed: () {
|
||||
onTap?.call();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, update) {
|
||||
return AlertDialog(
|
||||
title: title != null
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 5,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (leading != null) leading!,
|
||||
Flexible(child: title!),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
content: Center(child: trailing?.call(context, update)),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' show showModalBottomSheet;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
|
||||
_emptyCB() {}
|
||||
|
||||
class PopSheetEntry<T> extends ListTile {
|
||||
class AdaptiveMenuButton<T> extends MenuButton {
|
||||
final T? value;
|
||||
const PopSheetEntry({
|
||||
this.value,
|
||||
const AdaptiveMenuButton({
|
||||
super.key,
|
||||
super.leading,
|
||||
super.title,
|
||||
super.subtitle,
|
||||
this.value,
|
||||
required super.child,
|
||||
super.subMenu,
|
||||
super.onPressed,
|
||||
super.trailing,
|
||||
super.isThreeLine = false,
|
||||
super.dense,
|
||||
super.visualDensity,
|
||||
super.shape,
|
||||
super.style,
|
||||
super.selectedColor,
|
||||
super.iconColor,
|
||||
super.textColor,
|
||||
super.titleTextStyle,
|
||||
super.subtitleTextStyle,
|
||||
super.leadingAndTrailingTextStyle,
|
||||
super.contentPadding,
|
||||
super.leading,
|
||||
super.enabled = true,
|
||||
super.onTap = _emptyCB,
|
||||
super.onLongPress,
|
||||
super.onFocusChange,
|
||||
super.mouseCursor,
|
||||
super.selected = false,
|
||||
super.focusColor,
|
||||
super.hoverColor,
|
||||
super.splashColor,
|
||||
super.focusNode,
|
||||
super.autofocus = false,
|
||||
super.tileColor,
|
||||
super.selectedTileColor,
|
||||
super.enableFeedback,
|
||||
super.horizontalTitleGap,
|
||||
super.minVerticalPadding,
|
||||
super.minLeadingWidth,
|
||||
super.titleAlignment,
|
||||
});
|
||||
super.autoClose = true,
|
||||
super.popoverController,
|
||||
}) : assert(
|
||||
value != null || onPressed != null,
|
||||
'Either value or onPressed must be provided',
|
||||
);
|
||||
}
|
||||
|
||||
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above
|
||||
/// or equal to 640px
|
||||
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
|
||||
class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
final List<PopSheetEntry<T>> children;
|
||||
final List<AdaptiveMenuButton<T>> children;
|
||||
final Widget? icon;
|
||||
final Widget? child;
|
||||
final bool useRootNavigator;
|
||||
|
||||
final List<Widget>? headings;
|
||||
final String? tooltip;
|
||||
final String tooltip;
|
||||
final ValueChanged<T>? onSelected;
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
final Offset offset;
|
||||
|
||||
final ButtonVariance variance;
|
||||
|
||||
const AdaptivePopSheetList({
|
||||
super.key,
|
||||
required this.children,
|
||||
@ -70,166 +49,141 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
this.useRootNavigator = true,
|
||||
this.headings,
|
||||
this.onSelected,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(999)),
|
||||
this.tooltip,
|
||||
required this.tooltip,
|
||||
this.offset = Offset.zero,
|
||||
this.variance = ButtonVariance.ghost,
|
||||
}) : assert(
|
||||
!(icon != null && child != null),
|
||||
'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 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,
|
||||
useRootNavigator: useRootNavigator,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: mediaQuery.size.height * 0.6,
|
||||
enableDrag: true,
|
||||
showDragHandle: true,
|
||||
useRootNavigator: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: context.theme.borderRadiusMd,
|
||||
),
|
||||
position: position,
|
||||
items: children
|
||||
.map(
|
||||
(item) => PopupMenuItem<T>(
|
||||
padding: EdgeInsets.zero,
|
||||
enabled: false,
|
||||
child: _AdaptivePopSheetListItem<T>(
|
||||
item: item,
|
||||
onSelected: onSelected,
|
||||
backgroundColor: context.theme.colorScheme.card,
|
||||
builder: (context) {
|
||||
return ListView.builder(
|
||||
itemCount: childrenModified.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final data = childrenModified[index];
|
||||
|
||||
return Button(
|
||||
enabled: data.enabled,
|
||||
style: ButtonVariance.ghost.copyWith(
|
||||
padding: (context, state, value) => const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: () {
|
||||
data.onPressed?.call(context);
|
||||
if (data.autoClose) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
leading: data.leading,
|
||||
trailing: data.trailing,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: data.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (mediaQuery.mdAndUp) {
|
||||
return PopupMenuButton(
|
||||
icon: icon,
|
||||
tooltip: 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,
|
||||
return Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(tooltip),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0).copyWith(top: 0),
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.titleMedium!,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (headings != null) ...[
|
||||
...headings!,
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
color: theme.colorScheme.primary,
|
||||
thickness: 0.3,
|
||||
endIndent: 16,
|
||||
indent: 16,
|
||||
),
|
||||
],
|
||||
...children.map(
|
||||
(item) => _AdaptivePopSheetListItem(
|
||||
item: item,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
variance: variance,
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||
onPressed: () {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final position = RelativeRect.fromRect(
|
||||
Rect.fromPoints(
|
||||
renderBox.localToGlobal(Offset.zero,
|
||||
ancestor: context.findRenderObject()),
|
||||
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero),
|
||||
ancestor: context.findRenderObject()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
Offset.zero & mediaQuery.size,
|
||||
);
|
||||
final offset = Offset(position.left, position.top);
|
||||
showDropdownMenu(context, offset);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (child != null) {
|
||||
return Tooltip(
|
||||
message: tooltip ?? '',
|
||||
child: InkWell(
|
||||
onTap: showSheet,
|
||||
borderRadius: borderRadius,
|
||||
tooltip: TooltipContainer(child: Text(tooltip)),
|
||||
child: Button(
|
||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||
style: variance,
|
||||
child: IgnorePointer(child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||
tooltip: tooltip,
|
||||
style: theme.iconButtonTheme.style?.copyWith(
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: showSheet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdaptivePopSheetListItem<T> extends StatelessWidget {
|
||||
final PopSheetEntry<T> item;
|
||||
final ValueChanged<T>? onSelected;
|
||||
const _AdaptivePopSheetListItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InkWell(
|
||||
borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?)
|
||||
?.borderRadius as BorderRadius? ??
|
||||
const BorderRadius.all(Radius.circular(10)),
|
||||
onTap: !item.enabled
|
||||
? null
|
||||
: () {
|
||||
item.onTap?.call();
|
||||
if (item.value != null) {
|
||||
Navigator.pop(context);
|
||||
onSelected?.call(item.value as T);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(opacity: 1),
|
||||
child: IgnorePointer(child: item),
|
||||
),
|
||||
return Tooltip(
|
||||
tooltip: TooltipContainer(child: Text(tooltip)),
|
||||
child: IconButton(
|
||||
variance: variance,
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||
),
|
||||
);
|
||||
}
|
||||
|