Merge branch 'master' into build

This commit is contained in:
Kingkor Roy Tirtho 2022-07-03 17:55:29 +06:00
commit c34ff50345
52 changed files with 1769 additions and 1014 deletions

View File

@ -1,89 +0,0 @@
name: Flutter Cross Build
on:
push:
branches:
- build
workflow_dispatch:
jobs:
build_ubuntu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2.2.0
with:
cache: true
- run: |
sudo apt-get update -y
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make libwebkit2gtk-4.0-dev keybinder-3.0 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
- run: flutter config --enable-linux-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter clean
- run: flutter build linux
- run: make deb
- run: make tar
- run: wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
- run: chmod +x /usr/local/bin/appimagetool
- run: pip3 install appimage-builder
- run: make appimage
- uses: actions/upload-artifact@v2
with:
name: Spotube-Linux-Bundle
path: |
build/Spotube-linux-x86_64.deb
build/Spotube-linux-x86_64.tar.xz
build/Spotube-*-x86_64.AppImage
# Building Android Application
- run: echo '${{ secrets.KEYSTORE }}' | base64 --decode > android/app/upload-keystore.jks
- run: echo '${{ secrets.KEY_PROPERTIES }}' > android/key.properties
- run: flutter build apk
- run: make apk
- uses: actions/upload-artifact@v2
with:
name: Spotube-Android-Bundle
path: |
build/Spotube-android-all-arch.apk
build_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
repository: KRTirtho/flutter_distributor
ref: deb-implementation
path: build/flutter_distributor
- uses: subosito/flutter-action@v2.2.0
with:
cache: true
- run: flutter config --enable-windows-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: choco install innosetup -y
- run: dart pub global activate melos
- run: cd build/flutter_distributor && melos bootstrap && cd ../..
- run: dart build/flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean
- uses: actions/upload-artifact@v3
with:
name: Spotube-Windows-Bundle
path: |
dist/**/*.exe
build_macos:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
cache: true
- run: flutter config --enable-macos-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter build macos
- run: du -sh build/macos/Build/Products/Release/spotube.app
- run: npm install -g appdmg
- run: appdmg appdmg.json build/Spotube-macos-x86_64.dmg
- uses: actions/upload-artifact@v2
with:
name: Spotube-Macos-Bundle
path: |
build/Spotube-macos-x86_64.dmg

134
.github/workflows/spotube-nightly.yml vendored Normal file
View File

@ -0,0 +1,134 @@
name: Spotube Nightly
on:
push:
branches:
- build
workflow_dispatch:
jobs:
build_ubuntu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get latest tag
id: tag
uses: dawidd6/action-get-tag@v1
with:
strip_v: true
- uses: subosito/flutter-action@v2
with:
cache: true
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- run: |
sudo apt-get update -y
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
- run: |
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.0.0-beta.1/appimage-builder-1.0.0-677acbd-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
# replacing & adding new release version with older version
- run: |
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${{ steps.tag.outputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
- run: |
flutter config --enable-linux-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate flutter_distributor
flutter_distributor package --platform=linux --targets=deb,appimage --skip-clean
make tar
- run: |
mv build/Spotube-linux-x86_64.tar.xz dist/
mv dist/**/spotube-*-linux.deb dist/Spotube-linux-x86_64.deb
mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage
- uses: actions/upload-artifact@v3
with:
name: Spotube-Linux-Bundle
path: dist/
build_android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get latest tag
id: tag
uses: dawidd6/action-get-tag@v1
with:
strip_v: true
- uses: subosito/flutter-action@v2
with:
cache: true
- run: |
sudo apt-get update -y
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
- run: |
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate flutter_distributor
flutter_distributor package --platform=android --targets=apk --skip-clean
- run: |
mv dist/**/spotube-*-android.apk dist/Spotube-android-all-arch.apk
- uses: actions/upload-artifact@v3
with:
name: Spotube-Android-Bundle
path: dist/
build_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Get latest tag
id: tag
uses: dawidd6/action-get-tag@v1
with:
# Optionally strip `v` prefix
strip_v: true
# Replace Version in files
- run: |
choco install sed make -y
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" windows/runner/Runner.rc
# Build Windows Executable
- uses: subosito/flutter-action@v2.2.0
with:
cache: true
- run: |
flutter config --enable-windows-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate flutter_distributor
make innoinstall
flutter_distributor package --platform=windows --targets=exe --skip-clean
# Create Chocolatey Package
# setting the sha256 hash for new bundle
- run: mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
# Upload artifacts
- uses: actions/upload-artifact@v3
with:
name: Spotube-Windows-Bundle
path: dist/
build_macos:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
cache: true
- run: flutter config --enable-macos-desktop
- run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter build macos
- run: du -sh build/macos/Build/Products/Release/spotube.app
- run: npm install -g appdmg
- run: appdmg appdmg.json build/Spotube-macos-x86_64.dmg
- uses: actions/upload-artifact@v2
with:
name: Spotube-Macos-Bundle
path: |
build/Spotube-macos-x86_64.dmg

View File

@ -1,4 +1,4 @@
name: Spotube Build & Release name: Spotube Release
on: on:
release: release:
types: types:
@ -36,9 +36,8 @@ jobs:
flutter pub get flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}' dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate melos dart pub global activate melos
cd build/flutter_distributor && melos bootstrap && cd ../..
make innoinstall make innoinstall
dart build/flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean flutter_distributor package --platform=windows --targets=exe --skip-clean
# Create Chocolatey Package # Create Chocolatey Package
# setting the sha256 hash for new bundle # setting the sha256 hash for new bundle
@ -93,11 +92,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
repository: KRTirtho/flutter_distributor
ref: deb-implementation
path: build/flutter_distributor
- name: Get latest tag - name: Get latest tag
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
@ -111,7 +105,7 @@ jobs:
run: echo "::set-output name=date::$(date +'%Y-%m-%d')" run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make libwebkit2gtk-4.0-dev keybinder-3.0 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
- run: | - run: |
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.0.0-beta.1/appimage-builder-1.0.0-677acbd-x86_64.AppImage wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.0.0-beta.1/appimage-builder-1.0.0-677acbd-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage chmod +x appimage-builder-x86_64.AppImage
@ -124,9 +118,8 @@ jobs:
flutter config --enable-linux-desktop flutter config --enable-linux-desktop
flutter pub get flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}' dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate melos dart pub global activate flutter_distributor
cd build/flutter_distributor && melos bootstrap && cd ../.. flutter_distributor package --platform=linux --targets=deb,appimage --skip-clean
dart build/flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=linux --targets=deb,appimage --skip-clean
make tar make tar
- run: | - run: |
mv build/Spotube-linux-x86_64.tar.xz dist/ mv build/Spotube-linux-x86_64.tar.xz dist/
@ -141,11 +134,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
repository: KRTirtho/flutter_distributor
ref: deb-implementation
path: build/flutter_distributor
- name: Get latest tag - name: Get latest tag
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
@ -156,13 +144,12 @@ jobs:
cache: true cache: true
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make libwebkit2gtk-4.0-dev keybinder-3.0 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
- run: | - run: |
flutter pub get flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}' dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
dart pub global activate melos dart pub global activate flutter_distributor
cd build/flutter_distributor && melos bootstrap && cd ../.. flutter_distributor package --platform=android --targets=apk --skip-clean
dart build/flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=android --targets=apk --skip-clean
- run: | - run: |
mv dist/**/spotube-*-android.apk dist/Spotube-android-all-arch.apk mv dist/**/spotube-*-android.apk dist/Spotube-android-all-arch.apk
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3

View File

@ -1,3 +1,6 @@
{ {
"cmake.configureOnOpen": false "cmake.configureOnOpen": false,
"cSpell.words": [
"Mpris"
]
} }

View File

@ -12,12 +12,16 @@ All types of contributions are encouraged and valued. See the [Table of Contents
## Table of Contents ## Table of Contents
- [Code of Conduct](#code-of-conduct) - [Contributing to Spotube](#contributing-to-spotube)
- [I Have a Question](#i-have-a-question) - [Table of Contents](#table-of-contents)
- [I Want To Contribute](#i-want-to-contribute) - [Code of Conduct](#code-of-conduct)
- [Reporting Bugs](#reporting-bugs) - [I Have a Question](#i-have-a-question)
- [Suggesting Enhancements](#suggesting-enhancements) - [I Want To Contribute](#i-want-to-contribute)
- [Your First Code Contribution](#your-first-code-contribution) - [Reporting Bugs](#reporting-bugs)
- [Before Submitting a Bug Report](#before-submitting-a-bug-report)
- [How Do I Submit a Good Bug Report?](#how-do-i-submit-a-good-bug-report)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
## Code of Conduct ## Code of Conduct
@ -109,11 +113,12 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt
### Your First Code Contribution ### Your First Code Contribution
<!-- Download -->
Do the following: Do the following:
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support - Download the latest Flutter SDK (>=2.15.1) & enable desktop support
- Install Development dependencies in linux - Install Development dependencies in linux
- `libwebkit2gtk-4.0-dev`, `libkeybinder-3.0-0` & `libkeybinder-3.0-dev` (for Debian/Ubuntu) - `libgstreamer1.0-dev` & `libgstreamer-plugins-base1.0-dev` (for Debian/Ubuntu)
- `webkit2gtk` & `libkeybinder3` (for Arch/Manjaro) - `gstreamer`, `gst-libav`, `gst-plugins-base` & `gst-plugins-good` (for Arch/Manjaro)
- Clone the Repo & Run `flutter pub get` in the Terminal - Clone the Repo & Run `flutter pub get` in the Terminal
- Create a `secrets.json` in root of the project. The structure should be similar to the following example: - Create a `secrets.json` in root of the project. The structure should be similar to the following example:
```jsoc name="secrets.json" ```jsoc name="secrets.json"
@ -137,7 +142,7 @@ Do the following:
> You can add more clientId/clientSecret/genius-access-token if you want. The credentials used in the example are dummy (fake). You've to use your own secrets > You can add more clientId/clientSecret/genius-access-token if you want. The credentials used in the example are dummy (fake). You've to use your own secrets
- Finally run these following commands in the root of the project to start the Spotube Locally - Finally run these following commands in the root of the project to start the Spotube Locally
```bash ```bash
$ dart create-secrets.dart --local $ dart bin/create-secrets.dart --local
$ flutter run -d <window|macos|linux|(<android-device-id>)> $ flutter run -d <window|macos|linux|(<android-device-id>)>
``` ```

View File

@ -58,7 +58,7 @@ I'm always releasing newer versions of binary of the software each 2-3 month wit
| Platform | Package/Installation Method | | Platform | Package/Installation Method |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Android | [<img width='240' alt='Android Download' src='https://www.remcsteuben.com/sites/default/files/images/apkdaddy%20download.png'/>][android-dlink] | | Android | [<img width='240' alt='Android Download' src='https://www.remcsteuben.com/sites/default/files/images/apkdaddy%20download.png'/>][android-dlink]<br/>[<img width='240' alt='Android Download' src='https://user-images.githubusercontent.com/61944859/174589876-bace24c0-b3fd-4c4a-bdb4-6fa82b5853ec.png'/>][fdroid-dlink]|
| Debian/Ubuntu | [<img width='240' alt='Linux Debian/Ubuntu Download' src='https://user-images.githubusercontent.com/61944859/169097994-e92aff78-fd75-4c93-b6e4-f072a4b5a7ed.png'/>][deb-dlink] <br/> Then run: `sudo apt install Spotube-linux-x86_64.deb` | | Debian/Ubuntu | [<img width='240' alt='Linux Debian/Ubuntu Download' src='https://user-images.githubusercontent.com/61944859/169097994-e92aff78-fd75-4c93-b6e4-f072a4b5a7ed.png'/>][deb-dlink] <br/> Then run: `sudo apt install Spotube-linux-x86_64.deb` |
| Flatpak | `flatpak install com.github.KRTirtho.Spotube` <br/> <a href='https://flathub.org/apps/details/com.github.KRTirtho.Spotube'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a> | | Flatpak | `flatpak install com.github.KRTirtho.Spotube` <br/> <a href='https://flathub.org/apps/details/com.github.KRTirtho.Spotube'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a> |
| Arch/Manjaro | pamac: `pamac install spotube-bin` <br/> yay: `yay -Sy spotube-bin` | | Arch/Manjaro | pamac: `pamac install spotube-bin` <br/> yay: `yay -Sy spotube-bin` |
@ -127,8 +127,7 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour
- [AUR](https://aur.archlinux.org/) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users - [AUR](https://aur.archlinux.org/) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
- [Flatpak](https://flatpak.org/) - Flatpak is a utility for software deployment and package management for Linux - [Flatpak](https://flatpak.org/) - Flatpak is a utility for software deployment and package management for Linux
- [spotify (dart)](https://github.com/rinukkusu/spotify-dart) - A dart library for interfacing with the Spotify API - [spotify (dart)](https://github.com/rinukkusu/spotify-dart) - A dart library for interfacing with the Spotify API
- [just_audio](https://github.com/ryanheise/just_audio/tree/master/just_audio) - A feature-rich cross-platform audio player for Flutter that supports network audio streams too - [audioplayers](https://github.com/bluefireteam/audioplayers) - A Flutter plugin to play multiple audio files simultaneously (Android/iOS)
- [libwinmedia](https://github.com/harmonoid/libwinmedia) - A cross-platform media playback library for C/C++ with good number of features (only Windows & Linux)
- [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - YoutubeExplode is a library that provides an interface to query metadata of YouTube videos, playlists and channels, as well as to resolve and download video streams and closed caption tracks - [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - YoutubeExplode is a library that provides an interface to query metadata of YouTube videos, playlists and channels, as well as to resolve and download video streams and closed caption tracks
- [infinite_scroll_pagination](https://github.com/EdsonBueno/infinite_scroll_pagination) - Flutter package to help you lazily load and display pages of items as the user scrolls down your screen - [infinite_scroll_pagination](https://github.com/EdsonBueno/infinite_scroll_pagination) - Flutter package to help you lazily load and display pages of items as the user scrolls down your screen
- [bitsdojo_window](https://github.com/bitsdojo/bitsdojo_window) - A Flutter package that makes it easy to customize and work with your Flutter desktop app window on Windows, macOS and Linux - [bitsdojo_window](https://github.com/bitsdojo/bitsdojo_window) - A Flutter package that makes it easy to customize and work with your Flutter desktop app window on Windows, macOS and Linux
@ -165,5 +164,6 @@ Follow me on [Twitter](https://twitter.com/@krtirtho) for newer updates about th
[appimage-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage [appimage-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage
[mac-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-macos-x86_64.dmg [mac-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-macos-x86_64.dmg
[android-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk [android-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk
[fdroid-dlink]: https://f-droid.org/packages/oss.krtirtho.spotube/
[wiki-installation-instructions]: https://github.com/KRTirtho/spotube/wiki/Installation-Instrcutions [wiki-installation-instructions]: https://github.com/KRTirtho/spotube/wiki/Installation-Instrcutions

View File

@ -18,15 +18,15 @@ class AlbumCard extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null && bool isPlaylistPlaying =
playback.currentPlaylist!.id == album.id; playback.playlist != null && playback.playlist!.id == album.id;
final int marginH = final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
return PlaybuttonCard( return PlaybuttonCard(
imageUrl: imageToUrlString(album.images), imageUrl: imageToUrlString(album.images),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: playback.currentPlaylist?.id != null && isPlaying:
playback.currentPlaylist?.id == album.id, playback.playlist?.id != null && playback.playlist?.id == album.id,
title: album.name!, title: album.name!,
description: description:
"Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}", "Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
@ -41,14 +41,12 @@ class AlbumCard extends HookConsumerWidget {
.toList(); .toList();
if (tracks.isEmpty) return; if (tracks.isEmpty) return;
playback.setCurrentPlaylist = CurrentPlaylist( await playback.playPlaylist(CurrentPlaylist(
tracks: tracks, tracks: tracks,
id: album.id!, id: album.id!,
name: album.name!, name: album.name!,
thumbnail: album.images!.first.url!, thumbnail: album.images!.first.url!,
); ));
playback.setCurrentTrack = tracks.first;
await playback.startPlaying();
}, },
); );
} }

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart';
@ -18,24 +17,25 @@ class AlbumView extends HookConsumerWidget {
final AlbumSimple album; final AlbumSimple album;
const AlbumView(this.album, {Key? key}) : super(key: key); const AlbumView(this.album, {Key? key}) : super(key: key);
playPlaylist(Playback playback, List<Track> tracks, Future<void> playPlaylist(Playback playback, List<Track> tracks,
{Track? currentTrack}) async { {Track? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; final isPlaylistPlaying = playback.playlist?.id == album.id;
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist( await playback.playPlaylist(
tracks: tracks, CurrentPlaylist(
id: album.id!, tracks: tracks,
name: album.name!, id: album.id!,
thumbnail: imageToUrlString(album.images), name: album.name!,
thumbnail: imageToUrlString(album.images),
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null && currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) { currentTrack.id != playback.track?.id) {
playback.setCurrentTrack = currentTrack; await playback.play(currentTrack);
} }
await playback.startPlaying();
} }
@override @override
@ -54,8 +54,8 @@ class AlbumView extends HookConsumerWidget {
return TrackCollectionView( return TrackCollectionView(
id: album.id!, id: album.id!,
isPlaying: playback.currentPlaylist?.id != null && isPlaying:
playback.currentPlaylist?.id == album.id, playback.playlist?.id != null && playback.playlist?.id == album.id,
title: album.name!, title: album.name!,
titleImage: albumArt, titleImage: albumArt,
tracksSnapshot: tracksSnapshot, tracksSnapshot: tracksSnapshot,

View File

@ -183,24 +183,25 @@ class ArtistProfile extends HookConsumerWidget {
topTracksSnapshot.when( topTracksSnapshot.when(
data: (topTracks) { data: (topTracks) {
final isPlaylistPlaying = final isPlaylistPlaying =
playback.currentPlaylist?.id == data.id; playback.playlist?.id == data.id;
playPlaylist(List<Track> tracks, playPlaylist(List<Track> tracks,
{Track? currentTrack}) async { {Track? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist( await playback.playPlaylist(
tracks: tracks, CurrentPlaylist(
id: data.id!, tracks: tracks,
name: "${data.name!} To Tracks", id: data.id!,
thumbnail: imageToUrlString(data.images), name: "${data.name!} To Tracks",
thumbnail: imageToUrlString(data.images),
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null && currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) { currentTrack.id != playback.track?.id) {
playback.setCurrentTrack = currentTrack; await playback.play(currentTrack);
} }
await playback.startPlaying();
} }
return Column(children: [ return Column(children: [

View File

@ -18,7 +18,6 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useHotKeys.dart';
import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
import 'package:spotube/hooks/useUpdateChecker.dart'; import 'package:spotube/hooks/useUpdateChecker.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
@ -54,8 +53,6 @@ class Home extends HookConsumerWidget {
final _selectedIndex = useState(0); final _selectedIndex = useState(0);
_onSelectedIndexChanged(int index) => _selectedIndex.value = index; _onSelectedIndexChanged(int index) => _selectedIndex.value = index;
// initializing global hot keys
useHotKeys(ref);
// checks for latest version of the application // checks for latest version of the application
useUpdateChecker(ref); useUpdateChecker(ref);

View File

@ -0,0 +1,67 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeleton_text/skeleton_text.dart';
import 'package:spotube/extensions/ShimmerColorTheme.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
const widths = [20, 56, 89, 60, 25, 69];
class ShimmerLyrics extends HookWidget {
const ShimmerLyrics({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
final breakpoint = useBreakpoints();
return ListView.builder(
itemCount: 20,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final widthsCp = [...widths];
if (breakpoint.isMd) {
widthsCp.removeLast();
}
if (breakpoint.isSm) {
widthsCp.removeLast();
widthsCp.removeLast();
}
widthsCp.shuffle();
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widthsCp.map(
(width) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SkeletonAnimation(
shimmerColor: shimmerColor,
shimmerDuration: 1000,
child: Container(
height: 10,
width: width.toDouble(),
decoration: BoxDecoration(
color: shimmerBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
margin: const EdgeInsets.only(top: 10),
),
),
);
},
).toList(),
),
);
},
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
@ -22,7 +23,7 @@ class Lyrics extends HookConsumerWidget {
children: [ children: [
Center( Center(
child: Text( child: Text(
playback.currentTrack?.name ?? "", playback.track?.name ?? "",
style: breakpoint >= Breakpoints.md style: breakpoint >= Breakpoints.md
? textTheme.headline3 ? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25), : textTheme.headline4?.copyWith(fontSize: 25),
@ -30,7 +31,7 @@ class Lyrics extends HookConsumerWidget {
), ),
Center( Center(
child: Text( child: Text(
artistsToString<Artist>(playback.currentTrack?.artists ?? []), artistsToString<Artist>(playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md style: breakpoint >= Breakpoints.md
? textTheme.headline5 ? textTheme.headline5
: textTheme.headline6, : textTheme.headline6,
@ -44,7 +45,7 @@ class Lyrics extends HookConsumerWidget {
child: geniusLyricsSnapshot.when( child: geniusLyricsSnapshot.when(
data: (lyrics) { data: (lyrics) {
return Text( return Text(
lyrics == null && playback.currentTrack == null lyrics == null && playback.track == null
? "No Track being played currently" ? "No Track being played currently"
: lyrics!, : lyrics!,
style: textTheme.headline6 style: textTheme.headline6
@ -52,8 +53,8 @@ class Lyrics extends HookConsumerWidget {
); );
}, },
error: (error, __) => Text( error: (error, __) => Text(
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("), "Sorry, no Lyrics were found for `${playback.track?.name}` :'("),
loading: () => const CircularProgressIndicator(), loading: () => const ShimmerLyrics(),
), ),
), ),
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
@ -42,7 +43,7 @@ class SyncedLyrics extends HookConsumerWidget {
controller.scrollToIndex(0); controller.scrollToIndex(0);
failed.value = false; failed.value = false;
return null; return null;
}, [playback.currentTrack]); }, [playback.track]);
useEffect(() { useEffect(() {
if (lyricValue != null && lyricValue.rating <= 2) { if (lyricValue != null && lyricValue.rating <= 2) {
@ -98,29 +99,30 @@ class SyncedLyrics extends HookConsumerWidget {
Center( Center(
child: SizedBox( child: SizedBox(
height: breakpoint >= Breakpoints.md ? 50 : 30, height: breakpoint >= Breakpoints.md ? 50 : 30,
child: playback.currentTrack?.name != null && child: playback.track?.name != null &&
playback.currentTrack!.name!.length > 29 playback.track!.name!.length > 29
? SpotubeMarqueeText( ? SpotubeMarqueeText(
text: playback.currentTrack?.name ?? "Not Playing", text: playback.track?.name ?? "Not Playing",
style: headlineTextStyle, style: headlineTextStyle,
) )
: Text( : Text(
playback.currentTrack?.name ?? "Not Playing", playback.track?.name ?? "Not Playing",
style: headlineTextStyle, style: headlineTextStyle,
), ),
)), )),
Center( Center(
child: Text( child: Text(
artistsToString<Artist>(playback.currentTrack?.artists ?? []), artistsToString<Artist>(playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md style: breakpoint >= Breakpoints.md
? textTheme.headline5 ? textTheme.headline5
: textTheme.headline6, : textTheme.headline6,
), ),
), ),
if (lyricValue != null) if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
controller: controller, controller: controller,
itemCount: lyricValue.lyrics.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final lyricSlice = lyricValue.lyrics[index]; final lyricSlice = lyricValue.lyrics[index];
final isActive = lyricSlice.time.inSeconds == currentTime; final isActive = lyricSlice.time.inSeconds == currentTime;
@ -153,9 +155,11 @@ class SyncedLyrics extends HookConsumerWidget {
), ),
); );
}, },
itemCount: lyricValue.lyrics.length,
), ),
), ),
if (playback.track != null &&
(lyricValue == null || lyricValue.lyrics.isEmpty == true))
const Expanded(child: ShimmerLyrics()),
], ],
), ),
); );

View File

@ -1,19 +1,14 @@
import 'dart:async';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart';
import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
class Player extends HookConsumerWidget { class Player extends HookConsumerWidget {
Player({Key? key}) : super(key: key); Player({Key? key}) : super(key: key);
@ -23,41 +18,14 @@ class Player extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final _volume = useState(0.0);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final AudioPlayerHandler player = playback.player;
final Future<SharedPreferences> future =
useMemoized(SharedPreferences.getInstance);
final AsyncSnapshot<SharedPreferences?> localStorage =
useFuture(future, initialData: null);
useEffect(() {
/// warm up the audio player before playing actual audio
/// It's for resolving unresolved issue related to just_audio's
/// [disposeAllPlayers] method which is throwing
/// [UnimplementedException] in the [PlatformInterface]
/// implementation
player.core.setAsset("assets/warmer.mp3");
return null;
}, []);
useEffect(() {
if (localStorage.hasData) {
_volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ??
player.core.volume;
}
return null;
}, [localStorage.data]);
String albumArt = useMemoized( String albumArt = useMemoized(
() => imageToUrlString( () => imageToUrlString(
playback.currentTrack?.album?.images, playback.track?.album?.images,
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, index: (playback.track?.album?.images?.length ?? 1) - 1,
), ),
[playback.currentTrack?.album?.images], [playback.track?.album?.images],
); );
final entryRef = useRef<OverlayEntry?>(null); final entryRef = useRef<OverlayEntry?>(null);
@ -82,7 +50,7 @@ class Player extends HookConsumerWidget {
// entry will result in splashing while resizing the window // entry will result in splashing while resizing the window
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
entryRef.value == null && entryRef.value == null &&
playback.currentTrack != null) { playback.track != null) {
entryRef.value = OverlayEntry( entryRef.value = OverlayEntry(
opaque: false, opaque: false,
builder: (context) => PlayerOverlay(albumArt: albumArt), builder: (context) => PlayerOverlay(albumArt: albumArt),
@ -104,7 +72,7 @@ class Player extends HookConsumerWidget {
return () { return () {
disposeOverlay(); disposeOverlay();
}; };
}, [breakpoint, playback.currentTrack]); }, [breakpoint, playback.track]);
// returning an empty non spacious Container as the overlay will take // returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries] // place in the global overlay stack aka [_entries]
@ -135,22 +103,29 @@ class Player extends HookConsumerWidget {
Container( Container(
height: 20, height: 20,
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive( child: HookBuilder(builder: (context) {
value: _volume.value, final volume = useState(
onChanged: (value) async { useMemoized(() => playback.volume, []),
try { );
await player.core.setVolume(value).then((_) { return Slider.adaptive(
_volume.value = value; min: 0,
localStorage.data?.setDouble( max: 1,
LocalStorageKeys.volume, value: volume.value,
value, onChanged: (v) {
); volume.value = v;
}); },
} catch (e, stack) { onChangeEnd: (value) async {
logger.e("onChange", e, stack); try {
} // You don't really need to know why but this
}, // way it works only
), await playback.setVolume(value);
await playback.setVolume(value);
} catch (e, stack) {
logger.e("onChange", e, stack);
}
},
);
}),
), ),
PlayerActions() PlayerActions()
], ],

View File

@ -28,12 +28,12 @@ class PlayerActions extends HookConsumerWidget {
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ children: [
DownloadTrackButton( DownloadTrackButton(
track: playback.currentTrack, track: playback.track,
), ),
if (auth.isLoggedIn) if (auth.isLoggedIn)
FutureBuilder<bool>( FutureBuilder<bool>(
future: playback.currentTrack?.id != null future: playback.track?.id != null
? spotifyApi.tracks.me.containsOne(playback.currentTrack!.id!) ? spotifyApi.tracks.me.containsOne(playback.track!.id!)
: Future.value(false), : Future.value(false),
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -42,12 +42,12 @@ class PlayerActions extends HookConsumerWidget {
isLiked: isLiked, isLiked: isLiked,
onPressed: () async { onPressed: () async {
try { try {
if (playback.currentTrack?.id == null) return; if (playback.track?.id == null) return;
isLiked isLiked
? await spotifyApi.tracks.me ? await spotifyApi.tracks.me
.removeOne(playback.currentTrack!.id!) .removeOne(playback.track!.id!)
: await spotifyApi.tracks.me : await spotifyApi.tracks.me
.saveOne(playback.currentTrack!.id!); .saveOne(playback.track!.id!);
} catch (e, stack) { } catch (e, stack) {
logger.e("FavoriteButton.onPressed", e, stack); logger.e("FavoriteButton.onPressed", e, stack);
} finally { } finally {

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/playback.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
class PlayerControls extends HookConsumerWidget { class PlayerControls extends HookConsumerWidget {
final Color? iconColor; final Color? iconColor;
@ -18,7 +18,6 @@ class PlayerControls extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final AudioPlayerHandler player = playback.player;
final onNext = useNextTrack(playback); final onNext = useNextTrack(playback);
@ -26,14 +25,14 @@ class PlayerControls extends HookConsumerWidget {
final _playOrPause = useTogglePlayPause(playback); final _playOrPause = useTogglePlayPause(playback);
final duration = playback.duration ?? Duration.zero; final duration = playback.currentDuration;
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 600), constraints: const BoxConstraints(maxWidth: 600),
child: Column( child: Column(
children: [ children: [
StreamBuilder<Duration>( StreamBuilder<Duration>(
stream: player.core.positionStream, stream: playback.player.onPositionChanged,
builder: (context, snapshot) { builder: (context, snapshot) {
final totalMinutes = final totalMinutes =
zeroPadNumStr(duration.inMinutes.remainder(60)); zeroPadNumStr(duration.inMinutes.remainder(60));
@ -48,59 +47,71 @@ class PlayerControls extends HookConsumerWidget {
final sliderMax = duration.inSeconds; final sliderMax = duration.inSeconds;
final sliderValue = snapshot.data?.inSeconds ?? 0; final sliderValue = snapshot.data?.inSeconds ?? 0;
return Column(
children: [ return HookBuilder(builder: (context) {
Slider.adaptive( final progressStatic =
// cannot divide by zero (sliderMax == 0 || sliderValue > sliderMax)
// there's an edge case for value being bigger
// than total duration. Keeping it resolved
value: (sliderMax == 0 || sliderValue > sliderMax)
? 0 ? 0
: sliderValue / sliderMax, : sliderValue / sliderMax;
onChanged: (value) {},
onChangeEnd: (value) { final progress = useState<num>(
player.seek( useMemoized(() => progressStatic, []),
Duration( );
seconds: (value * sliderMax).toInt(),
), useEffect(() {
); progress.value = progressStatic;
}, return null;
activeColor: iconColor, }, [progressStatic]);
),
Padding( return Column(
padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [
child: Row( Slider.adaptive(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // cannot divide by zero
children: [ // there's an edge case for value being bigger
Text( // than total duration. Keeping it resolved
"$currentMinutes:$currentSeconds", value: progress.value.toDouble(),
), onChanged: (v) {
Text("$totalMinutes:$totalSeconds"), progress.value = v;
], },
onChangeEnd: (value) async {
await playback.seekPosition(
Duration(
seconds: (value * sliderMax).toInt(),
),
);
},
activeColor: iconColor,
), ),
), Padding(
], padding: const EdgeInsets.symmetric(horizontal: 8.0),
); child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"$currentMinutes:$currentSeconds",
),
Text("$totalMinutes:$totalSeconds"),
],
),
),
],
);
});
}), }),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.shuffle_rounded), icon: const Icon(Icons.shuffle_rounded),
color: playback.shuffled color: playback.isShuffled
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: iconColor, : iconColor,
onPressed: () { onPressed: () {
if (playback.currentTrack == null || if (playback.track == null || playback.playlist == null) {
playback.currentPlaylist == null) {
return; return;
} }
try { try {
if (!playback.shuffled) { playback.toggleShuffle();
playback.shuffle();
} else {
playback.unshuffle();
}
} catch (e, stack) { } catch (e, stack) {
logger.e("onShuffle", e, stack); logger.e("onShuffle", e, stack);
} }
@ -128,12 +139,10 @@ class PlayerControls extends HookConsumerWidget {
IconButton( IconButton(
icon: const Icon(Icons.stop_rounded), icon: const Icon(Icons.stop_rounded),
color: iconColor, color: iconColor,
onPressed: playback.currentTrack != null onPressed: playback.track != null
? () async { ? () async {
try { try {
await player.pause(); await playback.stop();
await player.seek(Duration.zero);
playback.reset();
} catch (e, stack) { } catch (e, stack) {
logger.e("onStop", e, stack); logger.e("onStop", e, stack);
} }

View File

@ -38,7 +38,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
Flexible( Flexible(
child: Text( child: Text(
playback.currentTrack?.name ?? "Not playing", playback.track?.name ?? "Not playing",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -54,7 +54,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
Text( Text(
playback.currentTrack?.name ?? "Not playing", playback.track?.name ?? "Not playing",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -62,7 +62,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
?.copyWith(fontWeight: FontWeight.bold, color: color), ?.copyWith(fontWeight: FontWeight.bold, color: color),
), ),
artistsToClickableArtists( artistsToClickableArtists(
playback.currentTrack?.artists ?? [], playback.track?.artists ?? [],
) )
], ],
), ),

View File

@ -23,7 +23,7 @@ class PlayerView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final currentTrack = ref.watch(playbackProvider.select( final currentTrack = ref.watch(playbackProvider.select(
(value) => value.currentTrack, (value) => value.track,
)); ));
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
@ -59,7 +59,6 @@ class PlayerView extends HookConsumerWidget {
), ),
backgroundColor: paletteColor.color, backgroundColor: paletteColor.color,
body: Column( body: Column(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),

View File

@ -15,8 +15,8 @@ class PlaylistCard extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null && bool isPlaylistPlaying =
playback.currentPlaylist!.id == playlist.id; playback.playlist != null && playback.playlist!.id == playlist.id;
final int marginH = final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
@ -46,14 +46,14 @@ class PlaylistCard extends HookConsumerWidget {
if (tracks.isEmpty) return; if (tracks.isEmpty) return;
playback.setCurrentPlaylist = CurrentPlaylist( await playback.playPlaylist(
tracks: tracks, CurrentPlaylist(
id: playlist.id!, tracks: tracks,
name: playlist.name!, id: playlist.id!,
thumbnail: imageToUrlString(playlist.images), name: playlist.name!,
thumbnail: imageToUrlString(playlist.images),
),
); );
playback.setCurrentTrack = tracks.first;
await playback.startPlaying();
}, },
); );
} }

View File

@ -73,11 +73,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: description, controller: description,
keyboardType: TextInputType.multiline,
maxLines: 5,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Description...", hintText: "Description...",
), ),
keyboardType: TextInputType.multiline,
maxLines: 5,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
CheckboxListTile( CheckboxListTile(

View File

@ -24,22 +24,23 @@ class PlaylistView extends HookConsumerWidget {
playPlaylist(Playback playback, List<Track> tracks, playPlaylist(Playback playback, List<Track> tracks,
{Track? currentTrack}) async { {Track? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
final isPlaylistPlaying = playback.currentPlaylist?.id != null && final isPlaylistPlaying =
playback.currentPlaylist?.id == playlist.id; playback.playlist?.id != null && playback.playlist?.id == playlist.id;
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist( await playback.playPlaylist(
tracks: tracks, CurrentPlaylist(
id: playlist.id!, tracks: tracks,
name: playlist.name!, id: playlist.id!,
thumbnail: imageToUrlString(playlist.images), name: playlist.name!,
thumbnail: imageToUrlString(playlist.images),
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null && currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) { currentTrack.id != playback.track?.id) {
playback.setCurrentTrack = currentTrack; await playback.play(currentTrack);
} }
await playback.startPlaying();
} }
@override @override
@ -47,8 +48,8 @@ class PlaylistView extends HookConsumerWidget {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
SpotifyApi spotify = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
final isPlaylistPlaying = playback.currentPlaylist?.id != null && final isPlaylistPlaying =
playback.currentPlaylist?.id == playlist.id; playback.playlist?.id != null && playback.playlist?.id == playlist.id;
final meSnapshot = ref.watch(currentUserQuery); final meSnapshot = ref.watch(currentUserQuery);
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!)); final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));

View File

@ -48,8 +48,8 @@ class Search extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
decoration: const InputDecoration(hintText: "Search..."),
controller: controller, controller: controller,
decoration: const InputDecoration(hintText: "Search..."),
onSubmitted: (value) { onSubmitted: (value) {
ref.read(searchTermStateProvider.notifier).state = ref.read(searchTermStateProvider.notifier).state =
controller.value.text; controller.value.text;
@ -115,26 +115,24 @@ class Search extends HookConsumerWidget {
thumbnailUrl: thumbnailUrl:
imageToUrlString(track.value.album?.images), imageToUrlString(track.value.album?.images),
onTrackPlayButtonPressed: (currentTrack) async { onTrackPlayButtonPressed: (currentTrack) async {
var isPlaylistPlaying = var isPlaylistPlaying = playback.playlist?.id !=
playback.currentPlaylist?.id != null && null &&
playback.currentPlaylist?.id == playback.playlist?.id == currentTrack.id;
currentTrack.id;
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist( playback.playPlaylist(
tracks: [currentTrack], CurrentPlaylist(
id: currentTrack.id!, tracks: [currentTrack],
name: currentTrack.name!, id: currentTrack.id!,
thumbnail: imageToUrlString( name: currentTrack.name!,
currentTrack.album?.images), thumbnail: imageToUrlString(
currentTrack.album?.images),
),
); );
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
currentTrack.id != null && currentTrack.id != null &&
currentTrack.id != currentTrack.id != playback.track?.id) {
playback.currentTrack?.id) { playback.play(currentTrack);
playback.setCurrentTrack = currentTrack;
} }
await playback.startPlaying();
}, },
); );
}), }),

View File

@ -77,14 +77,16 @@ class Login extends HookConsumerWidget {
hintText: "Spotify Client ID", hintText: "Spotify Client ID",
label: Text("ClientID"), label: Text("ClientID"),
), ),
keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: clientSecretController,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: "Spotify Client Secret", hintText: "Spotify Client Secret",
label: Text("Client Secret"), label: Text("Client Secret"),
), ),
controller: clientSecretController, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(

View File

@ -1,19 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Settings/About.dart'; import 'package:spotube/components/Settings/About.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/models/SpotifyMarkets.dart'; import 'package:spotube/models/SpotifyMarkets.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class Settings extends HookConsumerWidget { class Settings extends HookConsumerWidget {
@ -57,29 +53,6 @@ class Settings extends HookConsumerWidget {
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: ListView( child: ListView(
children: [ children: [
if (!kIsMobile) ...[
SettingsHotKeyTile(
title: "Next track global shortcut",
currentHotKey: preferences.nextTrackHotKey,
onHotKeyRecorded: (value) {
preferences.setNextTrackHotKey(value);
},
),
SettingsHotKeyTile(
title: "Prev track global shortcut",
currentHotKey: preferences.prevTrackHotKey,
onHotKeyRecorded: (value) {
preferences.setPrevTrackHotKey(value);
},
),
SettingsHotKeyTile(
title: "Play/Pause global shortcut",
currentHotKey: preferences.playPauseHotKey,
onHotKeyRecorded: (value) {
preferences.setPlayPauseHotKey(value);
},
),
],
ListTile( ListTile(
title: const Text("Theme"), title: const Text("Theme"),
horizontalTitleGap: 10, horizontalTitleGap: 10,

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/components/Shared/RecordHotKeyDialog.dart';
class SettingsHotKeyTile extends StatelessWidget {
final String title;
final HotKey? currentHotKey;
final ValueChanged<HotKey> onHotKeyRecorded;
const SettingsHotKeyTile({
required this.onHotKeyRecorded,
required this.title,
Key? key,
this.currentHotKey,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!),
const SizedBox(width: 10),
ElevatedButton(
child: const Text("Set Shortcut"),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return RecordHotKeyDialog(
onHotKeyRecorded: onHotKeyRecorded,
);
},
);
},
),
],
),
);
}
}

View File

@ -115,28 +115,6 @@ class DownloadTrackButton extends HookConsumerWidget {
if (!await outputFile.exists()) await outputFile.create(recursive: true); if (!await outputFile.exists()) await outputFile.create(recursive: true);
if (preferences.saveTrackLyrics && playback.currentTrack != null) {
if (!await outputLyricsFile.exists()) {
await outputLyricsFile.create(recursive: true);
}
final lyrics = await getLyrics(
playback.currentTrack!.name!,
playback.currentTrack!.artists
?.map((s) => s.name)
.whereNotNull()
.toList() ??
[],
apiKey: preferences.geniusAccessToken,
optimizeQuery: true,
);
if (lyrics != null) {
await outputLyricsFile.writeAsString(
"$lyrics\n\nPowered by genius.com",
mode: FileMode.writeOnly,
);
}
}
IOSink outputFileStream = outputFile.openWrite(); IOSink outputFileStream = outputFile.openWrite();
await audioStream.pipe(outputFileStream); await audioStream.pipe(outputFileStream);
await outputFileStream.flush(); await outputFileStream.flush();
@ -154,12 +132,31 @@ class DownloadTrackButton extends HookConsumerWidget {
} }
return statusCb.cancel(); return statusCb.cancel();
}); });
if (preferences.saveTrackLyrics && playback.track != null) {
if (!await outputLyricsFile.exists()) {
await outputLyricsFile.create(recursive: true);
}
final lyrics = await getLyrics(
playback.track!.name!,
playback.track!.artists?.map((s) => s.name).whereNotNull().toList() ??
[],
apiKey: preferences.geniusAccessToken,
optimizeQuery: true,
);
if (lyrics != null) {
await outputLyricsFile.writeAsString(
"$lyrics\n\nPowered by genius.com",
mode: FileMode.writeOnly,
);
}
}
}, [ }, [
track, track,
status, status,
yt, yt,
preferences.saveTrackLyrics, preferences.saveTrackLyrics,
playback.currentTrack, playback.track,
]); ]);
useEffect(() { useEffect(() {

View File

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
class RecordHotKeyDialog extends HookWidget {
final ValueChanged<HotKey> onHotKeyRecorded;
const RecordHotKeyDialog({
Key? key,
required this.onHotKeyRecorded,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final _hotKey = useState<HotKey?>(null);
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'Press the keys you want to use',
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
const Text(
"DO NOT Use only letters (e.g. k, g etc..)\nUse in combination with these"),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KeyCode.control,
KeyCode.shift,
KeyCode.alt,
KeyCode.superKey,
KeyCode.meta,
]
.map((key) => HotKeyVirtualView(
hotKey: HotKey(key),
))
.toList(),
),
Container(
width: 100,
height: 60,
margin: const EdgeInsets.only(top: 20),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).primaryColor,
),
),
child: Stack(
alignment: Alignment.center,
children: [
HotKeyRecorder(
onHotKeyRecorded: (hotKey) {
_hotKey.value = hotKey;
},
),
],
),
),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('OK'),
onPressed: _hotKey.value == null
? null
: () {
onHotKeyRecorded(_hotKey.value!);
GoRouter.of(context).pop();
},
),
],
);
}
}

View File

@ -84,7 +84,7 @@ class TrackTile extends HookConsumerWidget {
}); });
} }
actionAddToPlaylist() async { Future<void> actionAddToPlaylist() async {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@ -196,8 +196,7 @@ class TrackTile extends HookConsumerWidget {
), ),
IconButton( IconButton(
icon: Icon( icon: Icon(
playback.currentTrack?.id != null && playback.track?.id != null && playback.track?.id == track.value.id
playback.currentTrack?.id == track.value.id
? Icons.pause_circle_rounded ? Icons.pause_circle_rounded
: Icons.play_circle_rounded, : Icons.play_circle_rounded,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,

View File

@ -30,3 +30,71 @@ extension VideoFromCacheTrackExtension on Video {
); );
} }
} }
extension ThumbnailSetJson on ThumbnailSet {
static ThumbnailSet fromJson(Map<String, dynamic> map) {
return ThumbnailSet(map["videoId"]);
}
Map<String, dynamic> toJson() {
return {
"videoId": videoId,
};
}
}
extension EngagementJson on Engagement {
static Engagement fromJson(Map<String, dynamic> map) {
return Engagement(
map["viewCount"],
map["likeCount"],
map["dislikeCount"],
);
}
Map<String, dynamic> toJson() {
return {
"dislikeCount": dislikeCount,
"likeCount": likeCount,
"viewCount": viewCount,
};
}
}
extension VideoToJson on Video {
static Video fromJson(Map<String, dynamic> map) {
return Video(
VideoId(map["id"]),
map["title"],
map["author"],
ChannelId(map["channelId"]),
DateTime.tryParse(map["uploadDate"]),
DateTime.tryParse(map["publishDate"]),
map["description"],
parseDuration(map["duration"]),
ThumbnailSetJson.fromJson(map["thumbnails"]),
List.castFrom<dynamic, String>(map["keywords"]),
EngagementJson.fromJson(map["engagement"]),
map["isLive"],
);
}
Map<String, dynamic> toJson() {
return {
"hasWatchPage": hasWatchPage,
"url": url,
"author": author,
"channelId": channelId.value,
"description": description,
"duration": duration.toString(),
"engagement": engagement.toJson(),
"id": id.value,
"isLive": isLive,
"keywords": keywords.toList(),
"publishDate": publishDate.toString(),
"thumbnails": thumbnails.toJson(),
"title": title,
"uploadDate": uploadDate.toString(),
};
}
}

View File

@ -107,10 +107,16 @@ Future<SpotubeTrack> toSpotubeTrack({
"[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}", "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}",
); );
final audioManifest = (Platform.isMacOS || Platform.isIOS) final audioManifest = trackManifest.audioOnly.where((info) {
? trackManifest.audioOnly final isMp4a = info.codec.mimeType == "audio/mp4";
.where((info) => info.codec.mimeType == "audio/mp4") if (Platform.isLinux) {
: trackManifest.audioOnly; return !isMp4a;
} else if (Platform.isMacOS || Platform.isIOS) {
return isMp4a;
} else {
return true;
}
});
final ytUri = (audioQuality == AudioQuality.high final ytUri = (audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate() ? audioManifest.withHighestBitrate()

View File

@ -8,7 +8,7 @@ Future<void> Function() useNextTrack(Playback playback) {
try { try {
await playback.player.pause(); await playback.player.pause();
await playback.player.seek(Duration.zero); await playback.player.seek(Duration.zero);
playback.movePlaylistPositionBy(1); playback.seekForward();
} catch (e, stack) { } catch (e, stack) {
logger.e("useNextTrack", e, stack); logger.e("useNextTrack", e, stack);
} }
@ -20,7 +20,7 @@ Future<void> Function() usePreviousTrack(Playback playback) {
try { try {
await playback.player.pause(); await playback.player.pause();
await playback.player.seek(Duration.zero); await playback.player.seek(Duration.zero);
playback.movePlaylistPositionBy(-1); playback.seekBackward();
} catch (e, stack) { } catch (e, stack) {
logger.e("onPrevious", e, stack); logger.e("onPrevious", e, stack);
} }
@ -30,10 +30,15 @@ Future<void> Function() usePreviousTrack(Playback playback) {
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) { Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
return ([key]) async { return ([key]) async {
try { try {
if (playback.currentTrack == null) return; if (playback.track == null) {
playback.isPlaying return;
? await playback.player.pause() } else if (playback.track != null &&
: await playback.player.play(); playback.currentDuration == Duration.zero &&
await playback.player.getCurrentPosition() == Duration.zero) {
await playback.play(playback.track!);
} else {
await playback.togglePlayPause();
}
} catch (e, stack) { } catch (e, stack) {
logger.e("useTogglePlayPause", e, stack); logger.e("useTogglePlayPause", e, stack);
} }

View File

@ -1,49 +0,0 @@
import 'dart:io';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/hooks/playback.dart';
import 'package:spotube/models/GlobalKeyActions.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
useHotKeys(WidgetRef ref) {
final playback = ref.watch(playbackProvider);
final preferences = ref.watch(userPreferencesProvider);
List<GlobalKeyActions> _hotKeys = [];
final onNext = useNextTrack(playback);
final onPrevious = usePreviousTrack(playback);
final _playOrPause = useTogglePlayPause(playback);
useEffect(() {
if (kIsMobile) return null;
_hotKeys = [
GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
_playOrPause,
),
if (preferences.nextTrackHotKey != null)
GlobalKeyActions(preferences.nextTrackHotKey!, (key) => onNext()),
if (preferences.prevTrackHotKey != null)
GlobalKeyActions(preferences.prevTrackHotKey!, (key) => onPrevious()),
if (preferences.playPauseHotKey != null)
GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause)
];
Future.wait(
_hotKeys.map((e) {
return hotKeyManager.register(
e.hotKey,
keyDownHandler: e.onKeyDown,
);
}),
);
return () {
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
};
});
}

View File

@ -6,7 +6,7 @@ useSyncedLyrics(WidgetRef ref, Map<int, String> lyricsMap) {
final player = ref.watch(playbackProvider.select( final player = ref.watch(playbackProvider.select(
(value) => (value.player), (value) => (value.player),
)); ));
final stream = player.core.positionStream; final stream = player.onPositionChanged;
final currentTime = useState(0); final currentTime = useState(0);

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,7 +5,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/entities/CacheTrack.dart'; import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/GoRouteDeclarations.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
@ -15,26 +12,19 @@ import 'package:spotube/provider/AudioPlayer.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart'; import 'package:spotube/provider/YouTube.dart';
import 'package:spotube/services/MobileAudioService.dart';
import 'package:spotube/themes/dark-theme.dart'; import 'package:spotube/themes/dark-theme.dart';
import 'package:spotube/themes/light-theme.dart'; import 'package:spotube/themes/light-theme.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
void main() async { void main() async {
await Hive.initFlutter(); await Hive.initFlutter();
Hive.registerAdapter(CacheTrackAdapter()); Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter()); Hive.registerAdapter(CacheTrackEngagementAdapter());
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
builder: () => AudioPlayerHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true,
),
);
if (kIsDesktop) { if (kIsDesktop) {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll(); // final client = DBusClient.session();
// await client.registerObject(Media_Player());
doWhenWindowReady(() { doWhenWindowReady(() {
appWindow.minSize = const Size(359, 700); appWindow.minSize = const Size(359, 700);
appWindow.alignment = Alignment.center; appWindow.alignment = Alignment.center;
@ -43,17 +33,38 @@ void main() async {
appWindow.show(); appWindow.show();
}); });
} }
MobileAudioService? audioServiceHandler;
runApp(ProviderScope( runApp(ProviderScope(
child: Spotube(), child: Spotube(),
overrides: [ overrides: [
playbackProvider.overrideWithProvider(ChangeNotifierProvider( playbackProvider.overrideWithProvider(ChangeNotifierProvider(
(ref) { (ref) {
final youtube = ref.watch(youtubeProvider); final youtube = ref.watch(youtubeProvider);
return Playback( final player = ref.watch(audioPlayerProvider);
player: audioPlayerHandler,
final playback = Playback(
player: player,
youtube: youtube, youtube: youtube,
ref: ref, ref: ref,
); );
if (audioServiceHandler == null) {
AudioService.init(
builder: () => MobileAudioService(playback),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: true,
),
).then(
(value) {
playback.mobileAudioService = value;
audioServiceHandler = value;
},
);
}
return playback;
}, },
)) ))
], ],

View File

@ -1,7 +0,0 @@
import 'package:hotkey_manager/hotkey_manager.dart';
class GlobalKeyActions {
late final HotKey hotKey;
late final Function(HotKey hotKey) onKeyDown;
GlobalKeyActions(this.hotKey, this.onKeyDown);
}

View File

@ -24,8 +24,8 @@ GoRouter createGoRouter() => GoRouter(
), ),
GoRoute( GoRoute(
path: "/settings", path: "/settings",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => const SpotubePage(
child: const Settings(), child: Settings(),
), ),
), ),
GoRoute( GoRoute(

View File

@ -1,4 +1,6 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
enum SpotubeTrackMatchAlgorithm { enum SpotubeTrackMatchAlgorithm {
@ -14,11 +16,16 @@ class SpotubeTrack extends Track {
Video ytTrack; Video ytTrack;
String ytUri; String ytUri;
SpotubeTrack(
this.ytTrack,
this.ytUri,
) : super();
SpotubeTrack.fromTrack({ SpotubeTrack.fromTrack({
required Track track, required Track track,
required this.ytTrack, required this.ytTrack,
required this.ytUri, required this.ytUri,
}) { }) : super() {
album = track.album; album = track.album;
artists = track.artists; artists = track.artists;
availableMarkets = track.availableMarkets; availableMarkets = track.availableMarkets;
@ -38,4 +45,38 @@ class SpotubeTrack extends Track {
type = track.type; type = track.type;
uri = track.uri; uri = track.uri;
} }
static SpotubeTrack fromJson(Map<String, dynamic> map) {
return SpotubeTrack.fromTrack(
track: Track.fromJson(map),
ytTrack: VideoToJson.fromJson(map["ytTrack"]),
ytUri: map["ytUri"],
);
}
Map<String, dynamic> toJson() {
return {
"album": album?.toJson(),
"artists": artists?.map((artist) => artist.toJson()).toList(),
"availableMarkets": availableMarkets,
"discNumber": discNumber,
"duration": duration.toString(),
"durationMs": durationMs,
"explicit": explicit,
// "externalIds": externalIds,
// "externalUrls": externalUrls,
"href": href,
"id": id,
"isPlayable": isPlayable,
// "linkedFrom": linkedFrom,
"name": name,
"popularity": popularity,
"previewUrl": previewUrl,
"trackNumber": trackNumber,
"type": type,
"uri": uri,
"ytTrack": ytTrack.toJson(),
"ytUri": ytUri,
};
}
} }

View File

@ -1,5 +1,5 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
final audioPlayerProvider = Provider<AudioPlayer>((ref) { final audioPlayerProvider = Provider<AudioPlayer>((ref) {
return AudioPlayer(); return AudioPlayer();

12
lib/provider/DBus.dart Normal file
View File

@ -0,0 +1,12 @@
import 'dart:io';
import 'package:dbus/dbus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final Provider<DBusClient?> dbusClientProvider = Provider<DBusClient?>((ref) {
if (Platform.isLinux) {
return DBusClient.session();
}
});
final dbus = DBusClient.session();

View File

@ -1,304 +1,383 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:just_audio/just_audio.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/entities/CacheTrack.dart'; import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/contains-text-in-bracket.dart';
import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/AudioPlayer.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart'; import 'package:spotube/provider/YouTube.dart';
import 'package:spotube/utils/AudioPlayerHandler.dart'; import 'package:spotube/services/LinuxAudioService.dart';
import 'package:spotube/services/MobileAudioService.dart';
import 'package:spotube/utils/PersistedChangeNotifier.dart'; import 'package:spotube/utils/PersistedChangeNotifier.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
import 'package:collection/collection.dart';
import 'package:spotube/extensions/list-sort-multiple.dart';
class Playback extends PersistedChangeNotifier { class Playback extends PersistedChangeNotifier {
AudioSource? _currentAudioSource; // player properties
final _logger = getLogger(Playback); bool isShuffled;
CurrentPlaylist? _currentPlaylist; bool isPlaying;
Track? _currentTrack; Duration currentDuration;
double volume;
// states // class dependencies
bool _isPlaying = false; LinuxAudioService? _linuxAudioService;
Duration? duration; MobileAudioService? mobileAudioService;
Duration _prevPosition = Duration.zero; // foreign/passed properties
bool _shuffled = false; AudioPlayer player;
AudioPlayerHandler player;
YoutubeExplode youtube; YoutubeExplode youtube;
Ref ref; Ref ref;
UserPreferences get preferences => ref.read(userPreferencesProvider);
LazyBox<CacheTrack>? cacheTrackBox; // playlist & track list properties
late LazyBox<CacheTrack> cache;
CurrentPlaylist? playlist;
SpotubeTrack? track;
// internal stuff
final List<StreamSubscription> _subscriptions;
final _logger = getLogger(Playback);
Playback({ Playback({
required this.player, required this.player,
required this.youtube, required this.youtube,
required this.ref, required this.ref,
CurrentPlaylist? currentPlaylist, this.mobileAudioService,
Track? currentTrack, }) : volume = 0,
}) : _currentPlaylist = currentPlaylist, isShuffled = false,
_currentTrack = currentTrack, isPlaying = false,
currentDuration = Duration.zero,
_subscriptions = [],
super() { super() {
player.onNextRequest = () { if (Platform.isLinux) {
movePlaylistPositionBy(1); _linuxAudioService = LinuxAudioService(this);
}; }
player.onPreviousRequest = () {
movePlaylistPositionBy(-1);
};
_init(); (() async {
} cache = await Hive.openLazyBox<CacheTrack>("track-cache");
_subscriptions.addAll([
StreamSubscription<Duration?>? _durationStream; player.onPlayerStateChanged.listen(
StreamSubscription<Duration>? _positionStream; (state) async {
StreamSubscription<bool>? _playingStream; isPlaying = state == PlayerState.playing;
notifyListeners();
void _init() async { },
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache"); ),
player.onPlayerComplete.listen((_) {
_playingStream = player.core.playingStream.listen( if (track?.id != null) {
(playing) { seekForward();
_isPlaying = playing; } else {
notifyListeners(); isPlaying = false;
}, currentDuration = Duration.zero;
); notifyListeners();
}
_durationStream = player.core.durationStream.listen((event) async { }),
if (event != null) { player.onDurationChanged.listen((event) {
// Actually things doesn't work all the time as they were if (event != currentDuration) {
// described. So instead of listening to a `_ready` currentDuration = event;
// stream, it has to listen to duration stream since duration notifyListeners();
// is always added to the Stream sink after all icyMetadata has }
// been loaded thus indicating buffering started }),
if (event != Duration.zero && event != duration) { player.onPositionChanged.listen((pos) async {
// this line is for prev/next or already playing playlist if (pos > Duration.zero && currentDuration == Duration.zero) {
if (player.core.playing) await player.pause(); currentDuration = await player.getDuration() ?? Duration.zero;
await player.play(); notifyListeners();
} }
duration = event; }),
notifyListeners(); ]);
} }());
});
_positionStream =
player.core.createPositionStream().listen((position) async {
// detecting multiple same call
if (_prevPosition.inSeconds == position.inSeconds) return;
_prevPosition = position;
/// Because of ProcessingState.complete never gets set bug using a
/// custom solution to know when the audio stops playing
///
/// Details: https://github.com/KRTirtho/spotube/issues/46
if (duration != Duration.zero &&
duration?.isNegative == false &&
position.inSeconds == duration?.inSeconds) {
if (_currentTrack?.id != null) {
await player.pause();
movePlaylistPositionBy(1);
} else {
_isPlaying = false;
duration = null;
notifyListeners();
}
}
});
} }
@override @override
void dispose() { void dispose() {
_positionStream?.cancel(); _linuxAudioService?.dispose();
_playingStream?.cancel(); for (var subscription in _subscriptions) {
_durationStream?.cancel(); subscription.cancel();
cacheTrackBox?.close(); }
super.dispose(); super.dispose();
} }
bool get shuffled => _shuffled; Future<void> playPlaylist(CurrentPlaylist playlist, [int index = 0]) async {
CurrentPlaylist? get currentPlaylist => _currentPlaylist; if (index < 0 || index > playlist.tracks.length - 1) return;
Track? get currentTrack => _currentTrack; this.playlist = playlist;
bool get isPlaying => _isPlaying; final played = this.playlist!.tracks[index];
await play(played).then((_) {
int i = this
.playlist!
.tracks
.indexWhere((element) => element.id == played.id);
if (index == -1) return;
this.playlist!.tracks[i] = track!;
});
}
set setCurrentTrack(Track track) { // player methods
_logger.v("[Setting Current Track] ${track.name} - ${track.id}"); Future<void> play(Track track) async {
_currentTrack = track; _logger.v("[Track Playing] ${track.name} - ${track.id}");
try {
// the track is already playing so no need to change that
if (track.id == this.track?.id) return;
final tag = MediaItem(
id: track.id!,
title: track.name!,
album: track.album?.name,
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
artUri: Uri.parse(imageToUrlString(track.album?.images)),
);
mobileAudioService?.addItem(tag);
// the track is not a SpotubeTrack so turning it to one
if (track is! SpotubeTrack) {
track = await toSpotubeTrack(track);
}
_logger.v("[Track Direct Source] - ${(track).ytUri}");
this.track = track;
notifyListeners();
updatePersistence();
await player.play(UrlSource(track.ytUri));
} catch (e, stack) {
_logger.e("play", e, stack);
}
}
Future<void> resume() async {
if (isPlaying || (playlist == null && track == null)) return;
await player.resume();
isPlaying = true;
notifyListeners();
}
Future<void> pause() async {
if (!isPlaying || (playlist == null && track == null)) return;
await player.pause();
isPlaying = false;
notifyListeners();
}
Future<void> togglePlayPause() async {
isPlaying ? await pause() : await resume();
}
toggleShuffle() {
final result = isShuffled ? playlist?.unshuffle() : playlist?.shuffle();
if (result == true) {
isShuffled = !isShuffled;
notifyListeners();
}
}
Future<void> seekPosition(Duration position) {
return player.seek(position);
}
Future<void> setVolume(double newVolume) async {
await player.setVolume(volume);
volume = newVolume;
notifyListeners(); notifyListeners();
updatePersistence(); updatePersistence();
} }
set setCurrentPlaylist(CurrentPlaylist playlist) { Future<void> stop() async {
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}"); await player.stop();
_currentPlaylist = playlist; await player.release();
notifyListeners(); isPlaying = false;
updatePersistence(); isShuffled = false;
} playlist = null;
track = null;
void reset() { currentDuration = Duration.zero;
_logger.v("Playback Reset");
_isPlaying = false;
_shuffled = false;
duration = null;
_currentPlaylist = null;
_currentTrack = null;
notifyListeners(); notifyListeners();
updatePersistence(clearNullEntries: true); updatePersistence(clearNullEntries: true);
} }
/// sets the provided id matched track's uri\ void destroy() {
/// Doesn't notify listeners\ stop();
/// @returns `bool` - `true` if succeed & `false` when failed player.dispose();
bool setTrackUriById(String id, String uri) {
if (_currentPlaylist == null) return false;
try {
int index =
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
if (index == -1) return false;
_currentPlaylist!.tracks[index].uri = uri;
updatePersistence();
return _currentPlaylist!.tracks[index].uri == uri;
} catch (e) {
return false;
}
} }
void movePlaylistPositionBy(int pos) { // playlist & track list methods
_logger.v("[Playlist Position Move] $pos"); Future<SpotubeTrack> toSpotubeTrack(Track track) async {
if (_currentTrack != null && _currentPlaylist != null) { final format = preferences.ytSearchFormat;
int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos; final matchAlgorithm = preferences.trackMatchAlgorithm;
final artistsName =
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
[];
final audioQuality = preferences.audioQuality;
_logger.v("[Track Search Artists] $artistsName");
final mainArtist = artistsName.first;
final featuredArtists = artistsName.length > 1
? "feat. " + artistsName.sublist(1).join(" ")
: "";
final title = getTitle(
track.name!,
artists: artistsName,
onlyCleanArtist: true,
).trim();
_logger.v("[Track Search Title] $title");
final queryString = format
.replaceAll("\$MAIN_ARTIST", mainArtist)
.replaceAll("\$TITLE", title)
.replaceAll("\$FEATURED_ARTISTS", featuredArtists);
_logger.v("[Youtube Search Term] $queryString");
var safeIndex = index > _currentPlaylist!.trackIds.length - 1 Video ytVideo;
? 0 final cachedTrack = await cache.get(track.id);
: index < 0 if (cachedTrack != null && cachedTrack.mode == matchAlgorithm.name) {
? _currentPlaylist!.trackIds.length _logger.v(
: index; "[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex) );
? _currentPlaylist!.tracks.elementAt(safeIndex) ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
: null; } else {
if (track != null) { VideoSearchList videos = await youtube.search.search(queryString);
duration = null; if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
_currentTrack = track; List<Map> ratedRankedVideos = videos
notifyListeners(); .map((video) {
updatePersistence(); // the find should be lazy thus everything case insensitive
// starts to play the newly entered next/prev track final ytTitle = video.title.toLowerCase();
startPlaying(); final bool hasTitle = ytTitle.contains(title);
final bool hasAllArtists = track.artists?.every(
(artist) => ytTitle.contains(artist.name!.toLowerCase()),
) ??
false;
final bool authorIsArtist =
track.artists?.first.name?.toLowerCase() ==
video.author.toLowerCase();
final bool hasNoLiveInTitle =
!containsTextInBracket(ytTitle, "live");
int rate = 0;
for (final el in [
hasTitle,
hasAllArtists,
if (matchAlgorithm ==
SpotubeTrackMatchAlgorithm.authenticPopular)
authorIsArtist,
hasNoLiveInTitle,
!video.isLive,
]) {
if (el) rate++;
}
// can't let pass any non title matching track
if (!hasTitle) rate = rate - 2;
return {
"video": video,
"points": rate,
"views": video.engagement.viewCount,
};
})
.toList()
.sortByProperties(
[false, false],
["points", "views"],
);
ytVideo = ratedRankedVideos.first["video"] as Video;
} else {
ytVideo = videos.where((video) => !video.isLive).first;
} }
} }
}
Future<void> startPlaying([Track? track]) async { final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
_logger.v("[Track Playing] ${track?.name} - ${track?.id}");
try { _logger.v(
// the track is already playing so no need to change that "[YouTube Matched Track] ${ytVideo.title} | ${ytVideo.author} - ${ytVideo.url}",
if (track != null && track.id == _currentTrack?.id) return; );
track ??= _currentTrack;
if (track != null) { final audioManifest = trackManifest.audioOnly.where((info) {
Uri? parsedUri = Uri.tryParse(track.uri ?? ""); final isMp4a = info.codec.mimeType == "audio/mp4";
final tag = MediaItem( if (Platform.isLinux) {
id: track.id!, return !isMp4a;
title: track.name!, } else if (Platform.isMacOS || Platform.isIOS) {
album: track.album?.name, return isMp4a;
artist: artistsToString(track.artists ?? <ArtistSimple>[]), } else {
artUri: Uri.parse(imageToUrlString(track.album?.images)), return true;
);
player.addItem(tag);
if (parsedUri != null && parsedUri.hasAbsolutePath) {
_currentAudioSource = AudioSource.uri(parsedUri);
await player.core
.setAudioSource(
_currentAudioSource!,
preload: true,
)
.then((value) async {
_currentTrack = track;
notifyListeners();
updatePersistence();
});
return;
}
final preferences = ref.read(userPreferencesProvider);
final spotubeTrack = await toSpotubeTrack(
youtube: youtube,
track: track,
format: preferences.ytSearchFormat,
matchAlgorithm: preferences.trackMatchAlgorithm,
audioQuality: preferences.audioQuality,
box: cacheTrackBox,
);
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
_currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri));
await player.core
.setAudioSource(
_currentAudioSource!,
preload: true,
)
.then((value) {
_currentTrack = spotubeTrack;
notifyListeners();
updatePersistence();
});
}
} }
} catch (e, stack) { });
_logger.e("startPlaying", e, stack);
final ytUri = (audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last)
.url
.toString();
// only save when the track isn't available in the cache with same
// matchAlgorithm
if (cachedTrack == null || cachedTrack.mode != matchAlgorithm.name) {
await cache.put(
track.id!, CacheTrack.fromVideo(ytVideo, matchAlgorithm.name));
} }
return SpotubeTrack.fromTrack(
track: track,
ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms
ytUri: ytUri,
);
} }
void shuffle() { Future<void> setPlaylistPosition(int position) async {
if (currentPlaylist?.shuffle() == true) { if (playlist == null) return;
_shuffled = true; await playPlaylist(playlist!, position);
notifyListeners();
}
} }
void unshuffle() { Future<void> seekForward() async {
if (currentPlaylist?.unshuffle() == true) { if (playlist == null || track == null) return;
_shuffled = false; final int nextTrackIndex =
notifyListeners(); (playlist!.trackIds.indexOf(track!.id!) + 1).toInt();
} // checking if there's any track available forward
if (nextTrackIndex > (playlist?.tracks.length ?? 0) - 1) return;
await play(playlist!.tracks.elementAt(nextTrackIndex));
}
Future<void> seekBackward() async {
if (playlist == null || track == null) return;
final int prevTrackIndex =
(playlist!.trackIds.indexOf(track!.id!) - 1).toInt();
// checking if there's any track available behind
if (prevTrackIndex < 0) return;
await play(playlist!.tracks.elementAt(prevTrackIndex));
} }
@override @override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) { FutureOr<void> loadFromLocal(Map<String, dynamic> map) async {
if (map["currentPlaylist"] != null) { if (map["playlist"] != null) {
_currentPlaylist = playlist = CurrentPlaylist.fromJson(jsonDecode(map["playlist"]));
CurrentPlaylist.fromJson(jsonDecode(map["currentPlaylist"]));
} }
if (map["currentTrack"] != null) { if (map["track"] != null) {
_currentTrack = Track.fromJson(jsonDecode(map["currentTrack"])); track = SpotubeTrack.fromJson(jsonDecode(map["track"]));
startPlaying().then((_) {
Timer.periodic(const Duration(milliseconds: 100), (timer) {
if (player.core.playing) {
player.pause();
timer.cancel();
}
});
});
} }
volume = map["volume"] ?? volume;
} }
@override @override
FutureOr<Map<String, dynamic>> toMap() { FutureOr<Map<String, dynamic>> toMap() {
return { return {
"currentPlaylist": currentPlaylist != null "playlist": playlist != null ? jsonEncode(playlist?.toJson()) : null,
? jsonEncode(currentPlaylist?.toJson()) "track": track != null ? jsonEncode(track?.toJson()) : null,
: null, "volume": volume,
"currentTrack":
currentTrack != null ? jsonEncode(currentTrack?.toJson()) : null,
}; };
} }
} }
final playbackProvider = ChangeNotifierProvider<Playback>((ref) { final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
final player = AudioPlayerHandler();
final youtube = ref.watch(youtubeProvider); final youtube = ref.watch(youtubeProvider);
final player = ref.watch(audioPlayerProvider);
return Playback( return Playback(
player: player, player: player,
youtube: youtube, youtube: youtube,

View File

@ -166,8 +166,7 @@ final searchQuery = FutureProvider.family<List<Page>, String>((ref, term) {
final geniusLyricsQuery = FutureProvider<String?>( final geniusLyricsQuery = FutureProvider<String?>(
(ref) { (ref) {
final currentTrack = final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
ref.watch(playbackProvider.select((s) => s.currentTrack));
final geniusAccessToken = final geniusAccessToken =
ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken)); ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken));
if (currentTrack == null) { if (currentTrack == null) {
@ -184,8 +183,7 @@ final geniusLyricsQuery = FutureProvider<String?>(
final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>( final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>(
(ref) { (ref) {
final currentTrack = final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
ref.watch(playbackProvider.select((s) => s.currentTrack));
if (currentTrack == null) return null; if (currentTrack == null) return null;
return getTimedLyrics(currentTrack as SpotubeTrack); return getTimedLyrics(currentTrack as SpotubeTrack);
}, },

View File

@ -1,9 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/helpers/get-random-element.dart'; import 'package:spotube/helpers/get-random-element.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
@ -18,9 +16,6 @@ class UserPreferences extends PersistedChangeNotifier {
String recommendationMarket; String recommendationMarket;
bool saveTrackLyrics; bool saveTrackLyrics;
String geniusAccessToken; String geniusAccessToken;
HotKey? nextTrackHotKey;
HotKey? prevTrackHotKey;
HotKey? playPauseHotKey;
bool checkUpdate; bool checkUpdate;
SpotubeTrackMatchAlgorithm trackMatchAlgorithm; SpotubeTrackMatchAlgorithm trackMatchAlgorithm;
AudioQuality audioQuality; AudioQuality audioQuality;
@ -35,9 +30,6 @@ class UserPreferences extends PersistedChangeNotifier {
this.saveTrackLyrics = false, this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green, this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey, this.backgroundColorScheme = Colors.grey,
this.nextTrackHotKey,
this.prevTrackHotKey,
this.playPauseHotKey,
this.checkUpdate = true, this.checkUpdate = true,
this.trackMatchAlgorithm = SpotubeTrackMatchAlgorithm.authenticPopular, this.trackMatchAlgorithm = SpotubeTrackMatchAlgorithm.authenticPopular,
this.audioQuality = AudioQuality.high, this.audioQuality = AudioQuality.high,
@ -67,24 +59,6 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence(); updatePersistence();
} }
void setNextTrackHotKey(HotKey? value) {
nextTrackHotKey = value;
notifyListeners();
updatePersistence();
}
void setPrevTrackHotKey(HotKey? value) {
prevTrackHotKey = value;
notifyListeners();
updatePersistence();
}
void setPlayPauseHotKey(HotKey? value) {
playPauseHotKey = value;
notifyListeners();
updatePersistence();
}
void setYtSearchFormat(String format) { void setYtSearchFormat(String format) {
ytSearchFormat = format; ytSearchFormat = format;
notifyListeners(); notifyListeners();
@ -128,15 +102,7 @@ class UserPreferences extends PersistedChangeNotifier {
checkUpdate = map["checkUpdate"] ?? checkUpdate; checkUpdate = map["checkUpdate"] ?? checkUpdate;
geniusAccessToken = geniusAccessToken =
map["geniusAccessToken"] ?? getRandomElement(lyricsSecrets); map["geniusAccessToken"] ?? getRandomElement(lyricsSecrets);
nextTrackHotKey = map["nextTrackHotKey"] != null
? HotKey.fromJson(jsonDecode(map["nextTrackHotKey"]))
: null;
prevTrackHotKey = map["prevTrackHotKey"] != null
? HotKey.fromJson(jsonDecode(map["prevTrackHotKey"]))
: null;
playPauseHotKey = map["playPauseHotKey"] != null
? HotKey.fromJson(jsonDecode(map["playPauseHotKey"]))
: null;
ytSearchFormat = map["ytSearchFormat"] ?? ytSearchFormat; ytSearchFormat = map["ytSearchFormat"] ?? ytSearchFormat;
themeMode = ThemeMode.values[map["themeMode"] ?? 0]; themeMode = ThemeMode.values[map["themeMode"] ?? 0];
backgroundColorScheme = colorsMap.values backgroundColorScheme = colorsMap.values
@ -159,15 +125,6 @@ class UserPreferences extends PersistedChangeNotifier {
"saveTrackLyrics": saveTrackLyrics, "saveTrackLyrics": saveTrackLyrics,
"recommendationMarket": recommendationMarket, "recommendationMarket": recommendationMarket,
"geniusAccessToken": geniusAccessToken, "geniusAccessToken": geniusAccessToken,
"nextTrackHotKey": nextTrackHotKey != null
? jsonEncode(nextTrackHotKey?.toJson())
: null,
"prevTrackHotKey": prevTrackHotKey != null
? jsonEncode(prevTrackHotKey?.toJson())
: null,
"playPauseHotKey": playPauseHotKey != null
? jsonEncode(playPauseHotKey?.toJson())
: null,
"ytSearchFormat": ytSearchFormat, "ytSearchFormat": ytSearchFormat,
"themeMode": themeMode.index, "themeMode": themeMode.index,
"backgroundColorScheme": backgroundColorScheme.value, "backgroundColorScheme": backgroundColorScheme.value,

View File

@ -0,0 +1,697 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:dbus/dbus.dart';
import 'package:spotube/provider/DBus.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart';
class _MprisMediaPlayer2 extends DBusObject {
/// Creates a new object to expose on [path].
_MprisMediaPlayer2() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) {
dbus.registerObject(this);
}
void dispose() {
dbus.unregisterObject(this);
}
/// Gets value of property org.mpris.MediaPlayer2.CanQuit
Future<DBusMethodResponse> getCanQuit() async {
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Fullscreen
Future<DBusMethodResponse> getFullscreen() async {
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
}
/// Sets property org.mpris.MediaPlayer2.Fullscreen
Future<DBusMethodResponse> setFullscreen(bool value) async {
return DBusMethodSuccessResponse();
}
/// Gets value of property org.mpris.MediaPlayer2.CanSetFullscreen
Future<DBusMethodResponse> getCanSetFullscreen() async {
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
}
/// Gets value of property org.mpris.MediaPlayer2.CanRaise
Future<DBusMethodResponse> getCanRaise() async {
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
}
/// Gets value of property org.mpris.MediaPlayer2.HasTrackList
Future<DBusMethodResponse> getHasTrackList() async {
return DBusMethodSuccessResponse([const DBusBoolean(false)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Identity
Future<DBusMethodResponse> getIdentity() async {
return DBusMethodSuccessResponse([const DBusString("Spotube")]);
}
/// Gets value of property org.mpris.MediaPlayer2.DesktopEntry
Future<DBusMethodResponse> getDesktopEntry() async {
return DBusMethodSuccessResponse([const DBusString("spotube")]);
}
/// Gets value of property org.mpris.MediaPlayer2.SupportedUriSchemes
Future<DBusMethodResponse> getSupportedUriSchemes() async {
return DBusMethodSuccessResponse([
DBusArray.string(["http"])
]);
}
/// Gets value of property org.mpris.MediaPlayer2.SupportedMimeTypes
Future<DBusMethodResponse> getSupportedMimeTypes() async {
return DBusMethodSuccessResponse([
DBusArray.string(["audio/mpeg"])
]);
}
/// Implementation of org.mpris.MediaPlayer2.Raise()
Future<DBusMethodResponse> doRaise() async {
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Quit()
Future<DBusMethodResponse> doQuit() async {
appWindow.close();
return DBusMethodSuccessResponse();
}
@override
List<DBusIntrospectInterface> introspect() {
return [
DBusIntrospectInterface('org.mpris.MediaPlayer2', methods: [
DBusIntrospectMethod('Raise'),
DBusIntrospectMethod('Quit')
], properties: [
DBusIntrospectProperty('CanQuit', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('Fullscreen', DBusSignature('b'),
access: DBusPropertyAccess.readwrite),
DBusIntrospectProperty('CanSetFullscreen', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanRaise', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('HasTrackList', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('Identity', DBusSignature('s'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('DesktopEntry', DBusSignature('s'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('SupportedUriSchemes', DBusSignature('as'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('SupportedMimeTypes', DBusSignature('as'),
access: DBusPropertyAccess.read)
])
];
}
@override
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
if (methodCall.interface == 'org.mpris.MediaPlayer2') {
if (methodCall.name == 'Raise') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doRaise();
} else if (methodCall.name == 'Quit') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doQuit();
} else {
return DBusMethodErrorResponse.unknownMethod();
}
} else {
return DBusMethodErrorResponse.unknownInterface();
}
}
@override
Future<DBusMethodResponse> getProperty(String interface, String name) async {
if (interface == 'org.mpris.MediaPlayer2') {
if (name == 'CanQuit') {
return getCanQuit();
} else if (name == 'Fullscreen') {
return getFullscreen();
} else if (name == 'CanSetFullscreen') {
return getCanSetFullscreen();
} else if (name == 'CanRaise') {
return getCanRaise();
} else if (name == 'HasTrackList') {
return getHasTrackList();
} else if (name == 'Identity') {
return getIdentity();
} else if (name == 'DesktopEntry') {
return getDesktopEntry();
} else if (name == 'SupportedUriSchemes') {
return getSupportedUriSchemes();
} else if (name == 'SupportedMimeTypes') {
return getSupportedMimeTypes();
} else {
return DBusMethodErrorResponse.unknownProperty();
}
} else {
return DBusMethodErrorResponse.unknownProperty();
}
}
@override
Future<DBusMethodResponse> setProperty(
String interface, String name, DBusValue value) async {
if (interface == 'org.mpris.MediaPlayer2') {
if (name == 'CanQuit') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'Fullscreen') {
if (value.signature != DBusSignature('b')) {
return DBusMethodErrorResponse.invalidArgs();
}
return setFullscreen((value as DBusBoolean).value);
} else if (name == 'CanSetFullscreen') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanRaise') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'HasTrackList') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'Identity') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'DesktopEntry') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'SupportedUriSchemes') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'SupportedMimeTypes') {
return DBusMethodErrorResponse.propertyReadOnly();
} else {
return DBusMethodErrorResponse.unknownProperty();
}
} else {
return DBusMethodErrorResponse.unknownProperty();
}
}
@override
Future<DBusMethodResponse> getAllProperties(String interface) async {
var properties = <String, DBusValue>{};
if (interface == 'org.mpris.MediaPlayer2') {
properties['CanQuit'] = (await getCanQuit()).returnValues[0];
properties['Fullscreen'] = (await getFullscreen()).returnValues[0];
properties['CanSetFullscreen'] =
(await getCanSetFullscreen()).returnValues[0];
properties['CanRaise'] = (await getCanRaise()).returnValues[0];
properties['HasTrackList'] = (await getHasTrackList()).returnValues[0];
properties['Identity'] = (await getIdentity()).returnValues[0];
properties['DesktopEntry'] = (await getDesktopEntry()).returnValues[0];
properties['SupportedUriSchemes'] =
(await getSupportedUriSchemes()).returnValues[0];
properties['SupportedMimeTypes'] =
(await getSupportedMimeTypes()).returnValues[0];
}
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
}
}
class _MprisMediaPlayer2Player extends DBusObject {
final Playback playback;
/// Creates a new object to expose on [path].
_MprisMediaPlayer2Player({
required this.playback,
}) : super(DBusObjectPath("/org/mpris/MediaPlayer2")) {
(() async {
final nameStatus =
await dbus.requestName("org.mpris.MediaPlayer2.spotube");
if (nameStatus == DBusRequestNameReply.exists) {
await dbus.requestName("org.mpris.MediaPlayer2.spotube.instance$pid");
}
await dbus.registerObject(this);
}());
}
void dispose() {
dbus.unregisterObject(this);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus
Future<DBusMethodResponse> getPlaybackStatus() async {
final status = playback.isPlaying
? "Playing"
: playback.playlist == null
? "Stopped"
: "Paused";
return DBusMethodSuccessResponse([DBusString(status)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.LoopStatus
Future<DBusMethodResponse> getLoopStatus() async {
return DBusMethodSuccessResponse([const DBusString("Playlist")]);
}
/// Sets property org.mpris.MediaPlayer2.Player.LoopStatus
Future<DBusMethodResponse> setLoopStatus(String value) async {
return DBusMethodErrorResponse.failed(
'Set org.mpris.MediaPlayer2.Player.LoopStatus not implemented');
}
/// Gets value of property org.mpris.MediaPlayer2.Player.Rate
Future<DBusMethodResponse> getRate() async {
return DBusMethodSuccessResponse([DBusDouble(1)]);
}
/// Sets property org.mpris.MediaPlayer2.Player.Rate
Future<DBusMethodResponse> setRate(double value) async {
return DBusMethodSuccessResponse();
}
/// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle
Future<DBusMethodResponse> getShuffle() async {
return DBusMethodSuccessResponse([DBusBoolean(playback.isShuffled)]);
}
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
Future<DBusMethodResponse> setShuffle(bool value) async {
playback.toggleShuffle();
return DBusMethodSuccessResponse();
}
/// Gets value of property org.mpris.MediaPlayer2.Player.Metadata
Future<DBusMethodResponse> getMetadata() async {
try {
if (playback.track == null) {
return DBusMethodSuccessResponse([DBusDict.stringVariant({})]);
}
final id = (playback.playlist != null
? playback.playlist!.tracks.indexWhere(
(track) => playback.track!.id == track.id!,
)
: 0)
.abs();
return DBusMethodSuccessResponse([
DBusDict.stringVariant({
"mpris:trackid": DBusString("${path.value}/Track/$id"),
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
"mpris:artUrl":
DBusString(imageToUrlString(playback.track?.album?.images)),
"xesam:album": DBusString(playback.track!.album!.name!),
"xesam:artist": DBusArray.string(
playback.track!.artists!.map((artist) => artist.name!),
),
"xesam:title": DBusString(playback.track!.name!),
"xesam:url": DBusString(
playback.track is SpotubeTrack
? (playback.track as SpotubeTrack).ytUri
: playback.track!.previewUrl!,
),
"xesam:genre": const DBusString("Unknown"),
}),
]);
} catch (e) {
print("[DBUS ERROR] $e");
rethrow;
}
}
/// Gets value of property org.mpris.MediaPlayer2.Player.Volume
Future<DBusMethodResponse> getVolume() async {
return DBusMethodSuccessResponse([DBusDouble(playback.volume)]);
}
/// Sets property org.mpris.MediaPlayer2.Player.Volume
Future<DBusMethodResponse> setVolume(double value) async {
playback.setVolume(value);
return DBusMethodSuccessResponse();
}
/// Gets value of property org.mpris.MediaPlayer2.Player.Position
Future<DBusMethodResponse> getPosition() async {
return DBusMethodSuccessResponse([
DBusInt64((await playback.player.getDuration())?.inMicroseconds ?? 0),
]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.MinimumRate
Future<DBusMethodResponse> getMinimumRate() async {
return DBusMethodSuccessResponse([const DBusDouble(1)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.MaximumRate
Future<DBusMethodResponse> getMaximumRate() async {
return DBusMethodSuccessResponse([const DBusDouble(1)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanGoNext
Future<DBusMethodResponse> getCanGoNext() async {
return DBusMethodSuccessResponse([
DBusBoolean(
playback.playlist?.tracks.isNotEmpty == true,
)
]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanGoPrevious
Future<DBusMethodResponse> getCanGoPrevious() async {
return DBusMethodSuccessResponse([
DBusBoolean(
playback.playlist?.tracks.isNotEmpty == true,
)
]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanPlay
Future<DBusMethodResponse> getCanPlay() async {
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanPause
Future<DBusMethodResponse> getCanPause() async {
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanSeek
Future<DBusMethodResponse> getCanSeek() async {
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
}
/// Gets value of property org.mpris.MediaPlayer2.Player.CanControl
Future<DBusMethodResponse> getCanControl() async {
return DBusMethodSuccessResponse([const DBusBoolean(true)]);
}
/// Implementation of org.mpris.MediaPlayer2.Player.Next()
Future<DBusMethodResponse> doNext() async {
playback.seekForward();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.Previous()
Future<DBusMethodResponse> doPrevious() async {
playback.seekBackward();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.Pause()
Future<DBusMethodResponse> doPause() async {
playback.pause();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.PlayPause()
Future<DBusMethodResponse> doPlayPause() async {
playback.isPlaying ? playback.pause() : playback.resume();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.Stop()
Future<DBusMethodResponse> doStop() async {
playback.stop();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.Play()
Future<DBusMethodResponse> doPlay() async {
playback.resume();
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.Seek()
Future<DBusMethodResponse> doSeek(int offset) async {
playback.seekPosition(Duration(microseconds: offset));
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.SetPosition()
Future<DBusMethodResponse> doSetPosition(String TrackId, int Position) async {
return DBusMethodSuccessResponse();
}
/// Implementation of org.mpris.MediaPlayer2.Player.OpenUri()
Future<DBusMethodResponse> doOpenUri(String Uri) async {
return DBusMethodSuccessResponse();
}
/// Emits signal org.mpris.MediaPlayer2.Player.Seeked
Future<void> emitSeeked(int Position) async {
await emitSignal(
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]);
}
@override
List<DBusIntrospectInterface> introspect() {
return [
DBusIntrospectInterface('org.mpris.MediaPlayer2.Player', methods: [
DBusIntrospectMethod('Next'),
DBusIntrospectMethod('Previous'),
DBusIntrospectMethod('Pause'),
DBusIntrospectMethod('PlayPause'),
DBusIntrospectMethod('Stop'),
DBusIntrospectMethod('Play'),
DBusIntrospectMethod('Seek', args: [
DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_,
name: 'Offset')
]),
DBusIntrospectMethod('SetPosition', args: [
DBusIntrospectArgument(DBusSignature('o'), DBusArgumentDirection.in_,
name: 'TrackId'),
DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_,
name: 'Position')
]),
DBusIntrospectMethod('OpenUri', args: [
DBusIntrospectArgument(DBusSignature('s'), DBusArgumentDirection.in_,
name: 'Uri')
])
], signals: [
DBusIntrospectSignal('Seeked', args: [
DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.out,
name: 'Position')
])
], properties: [
DBusIntrospectProperty('PlaybackStatus', DBusSignature('s'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('LoopStatus', DBusSignature('s'),
access: DBusPropertyAccess.readwrite),
DBusIntrospectProperty('Rate', DBusSignature('d'),
access: DBusPropertyAccess.readwrite),
DBusIntrospectProperty('Shuffle', DBusSignature('b'),
access: DBusPropertyAccess.readwrite),
DBusIntrospectProperty('Metadata', DBusSignature('a{sv}'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('Volume', DBusSignature('d'),
access: DBusPropertyAccess.readwrite),
DBusIntrospectProperty('Position', DBusSignature('x'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('MinimumRate', DBusSignature('d'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('MaximumRate', DBusSignature('d'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanGoNext', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanGoPrevious', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanPlay', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanPause', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanSeek', DBusSignature('b'),
access: DBusPropertyAccess.read),
DBusIntrospectProperty('CanControl', DBusSignature('b'),
access: DBusPropertyAccess.read)
])
];
}
@override
Future<DBusMethodResponse> handleMethodCall(DBusMethodCall methodCall) async {
if (methodCall.interface == 'org.mpris.MediaPlayer2.Player') {
if (methodCall.name == 'Next') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doNext();
} else if (methodCall.name == 'Previous') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doPrevious();
} else if (methodCall.name == 'Pause') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doPause();
} else if (methodCall.name == 'PlayPause') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doPlayPause();
} else if (methodCall.name == 'Stop') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doStop();
} else if (methodCall.name == 'Play') {
if (methodCall.values.isNotEmpty) {
return DBusMethodErrorResponse.invalidArgs();
}
return doPlay();
} else if (methodCall.name == 'Seek') {
if (methodCall.signature != DBusSignature('x')) {
return DBusMethodErrorResponse.invalidArgs();
}
return doSeek((methodCall.values[0] as DBusInt64).value);
} else if (methodCall.name == 'SetPosition') {
if (methodCall.signature != DBusSignature('ox')) {
return DBusMethodErrorResponse.invalidArgs();
}
return doSetPosition((methodCall.values[0] as DBusObjectPath).value,
(methodCall.values[1] as DBusInt64).value);
} else if (methodCall.name == 'OpenUri') {
if (methodCall.signature != DBusSignature('s')) {
return DBusMethodErrorResponse.invalidArgs();
}
return doOpenUri((methodCall.values[0] as DBusString).value);
} else {
return DBusMethodErrorResponse.unknownMethod();
}
} else {
return DBusMethodErrorResponse.unknownInterface();
}
}
@override
Future<DBusMethodResponse> getProperty(String interface, String name) async {
if (interface == 'org.mpris.MediaPlayer2.Player') {
if (name == 'PlaybackStatus') {
return getPlaybackStatus();
} else if (name == 'LoopStatus') {
return getLoopStatus();
} else if (name == 'Rate') {
return getRate();
} else if (name == 'Shuffle') {
return getShuffle();
} else if (name == 'Metadata') {
return getMetadata();
} else if (name == 'Volume') {
return getVolume();
} else if (name == 'Position') {
return getPosition();
} else if (name == 'MinimumRate') {
return getMinimumRate();
} else if (name == 'MaximumRate') {
return getMaximumRate();
} else if (name == 'CanGoNext') {
return getCanGoNext();
} else if (name == 'CanGoPrevious') {
return getCanGoPrevious();
} else if (name == 'CanPlay') {
return getCanPlay();
} else if (name == 'CanPause') {
return getCanPause();
} else if (name == 'CanSeek') {
return getCanSeek();
} else if (name == 'CanControl') {
return getCanControl();
} else {
return DBusMethodErrorResponse.unknownProperty();
}
} else {
return DBusMethodErrorResponse.unknownProperty();
}
}
@override
Future<DBusMethodResponse> setProperty(
String interface, String name, DBusValue value) async {
if (interface == 'org.mpris.MediaPlayer2.Player') {
if (name == 'PlaybackStatus') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'LoopStatus') {
if (value.signature != DBusSignature('s')) {
return DBusMethodErrorResponse.invalidArgs();
}
return setLoopStatus((value as DBusString).value);
} else if (name == 'Rate') {
if (value.signature != DBusSignature('d')) {
return DBusMethodErrorResponse.invalidArgs();
}
return setRate((value as DBusDouble).value);
} else if (name == 'Shuffle') {
if (value.signature != DBusSignature('b')) {
return DBusMethodErrorResponse.invalidArgs();
}
return setShuffle((value as DBusBoolean).value);
} else if (name == 'Metadata') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'Volume') {
if (value.signature != DBusSignature('d')) {
return DBusMethodErrorResponse.invalidArgs();
}
return setVolume((value as DBusDouble).value);
} else if (name == 'Position') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'MinimumRate') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'MaximumRate') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanGoNext') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanGoPrevious') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanPlay') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanPause') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanSeek') {
return DBusMethodErrorResponse.propertyReadOnly();
} else if (name == 'CanControl') {
return DBusMethodErrorResponse.propertyReadOnly();
} else {
return DBusMethodErrorResponse.unknownProperty();
}
} else {
return DBusMethodErrorResponse.unknownProperty();
}
}
@override
Future<DBusMethodResponse> getAllProperties(String interface) async {
var properties = <String, DBusValue>{};
if (interface == 'org.mpris.MediaPlayer2.Player') {
properties['PlaybackStatus'] =
(await getPlaybackStatus()).returnValues[0];
properties['LoopStatus'] = (await getLoopStatus()).returnValues[0];
properties['Rate'] = (await getRate()).returnValues[0];
properties['Shuffle'] = (await getShuffle()).returnValues[0];
properties['Metadata'] = (await getMetadata()).returnValues[0];
properties['Volume'] = (await getVolume()).returnValues[0];
properties['Position'] = (await getPosition()).returnValues[0];
properties['MinimumRate'] = (await getMinimumRate()).returnValues[0];
properties['MaximumRate'] = (await getMaximumRate()).returnValues[0];
properties['CanGoNext'] = (await getCanGoNext()).returnValues[0];
properties['CanGoPrevious'] = (await getCanGoPrevious()).returnValues[0];
properties['CanPlay'] = (await getCanPlay()).returnValues[0];
properties['CanPause'] = (await getCanPause()).returnValues[0];
properties['CanSeek'] = (await getCanSeek()).returnValues[0];
properties['CanControl'] = (await getCanControl()).returnValues[0];
}
return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]);
}
}
class LinuxAudioService {
_MprisMediaPlayer2 mp2;
_MprisMediaPlayer2Player player;
LinuxAudioService(Playback playback)
: mp2 = _MprisMediaPlayer2(),
player = _MprisMediaPlayer2Player(playback: playback);
void dispose() {
mp2.dispose();
player.dispose();
}
}

View File

@ -0,0 +1,84 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:spotube/provider/Playback.dart';
class MobileAudioService extends BaseAudioHandler {
final Playback playback;
MobileAudioService(this.playback) {
final _player = playback.player;
_player.onPlayerStateChanged.listen((state) async {
if (state != PlayerState.completed) {
playbackState.add(await _transformEvent());
}
});
_player.onPlayerComplete.listen((_) {
if (playback.playlist == null && playback.track == null) {
playbackState.add(
PlaybackState(
processingState: AudioProcessingState.completed,
),
);
}
});
}
void addItem(MediaItem item) {
mediaItem.add(item);
}
@override
Future<void> play() => playback.resume();
@override
Future<void> pause() => playback.pause();
@override
Future<void> seek(Duration position) => playback.seekPosition(position);
@override
Future<void> stop() => playback.stop();
@override
Future<void> skipToNext() async {
playback.seekForward();
await super.skipToNext();
}
@override
Future<void> skipToPrevious() async {
playback.seekBackward();
await super.skipToPrevious();
}
@override
Future<void> onTaskRemoved() {
playback.destroy();
return super.onTaskRemoved();
}
Future<PlaybackState> _transformEvent() async {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
playback.player.state == PlayerState.playing
? MediaControl.pause
: MediaControl.play,
MediaControl.skipToNext,
MediaControl.stop,
],
androidCompactActionIndices: const [0, 1, 2],
playing: playback.player.state == PlayerState.playing,
updatePosition:
(await playback.player.getCurrentPosition()) ?? Duration.zero,
processingState: playback.player.state == PlayerState.paused
? AudioProcessingState.buffering
: playback.player.state == PlayerState.playing
? AudioProcessingState.ready
: AudioProcessingState.idle,
);
}
}

View File

@ -1,83 +0,0 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
/// An [AudioHandler] for playing a single item.
class AudioPlayerHandler extends BaseAudioHandler {
final _player = AudioPlayer();
FutureOr<void> Function()? onNextRequest;
FutureOr<void> Function()? onPreviousRequest;
/// Initialise our audio handler.
AudioPlayerHandler() {
// So that our clients (the Flutter UI and the system notification) know
// what state to display, here we set up our audio handler to broadcast all
// playback state changes as they happen via playbackState...
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
}
AudioPlayer get core => _player;
void addItem(MediaItem item) {
mediaItem.add(item);
}
// In this simple example, we handle only 4 actions: play, pause, seek and
// stop. Any button press from the Flutter UI, notification, lock screen or
// headset will be routed through to these 4 methods so that you can handle
// your audio playback logic in one place.
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
@override
Future<void> seek(Duration position) => _player.seek(position);
@override
Future<void> stop() => _player.stop();
@override
Future<void> skipToNext() async {
await onNextRequest?.call();
await super.skipToNext();
}
@override
Future<void> skipToPrevious() async {
await onPreviousRequest?.call();
await super.skipToPrevious();
}
/// Transform a just_audio event into an audio_service state.
///
/// This method is used from the constructor. Every event received from the
/// just_audio player will be transformed into an audio_service state so that
/// it can be broadcast to audio_service clients.
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
androidCompactActionIndices: const [0, 1, 2],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
);
}
}

View File

@ -6,21 +6,17 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h> #include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <hotkey_manager/hotkey_manager_plugin.h>
#include <libwinmedia/libwinmedia_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) hotkey_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin");
hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar);
g_autoptr(FlPluginRegistrar) libwinmedia_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LibwinmediaPlugin");
libwinmedia_plugin_register_with_registrar(libwinmedia_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -3,9 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
bitsdojo_window_linux bitsdojo_window_linux
hotkey_manager
libwinmedia
url_launcher_linux url_launcher_linux
) )

View File

@ -7,9 +7,8 @@ import Foundation
import audio_service import audio_service
import audio_session import audio_session
import audioplayers_darwin
import bitsdojo_window_macos import bitsdojo_window_macos
import hotkey_manager
import just_audio
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import shared_preferences_macos import shared_preferences_macos
@ -19,9 +18,8 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
HotkeyManagerPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@ -71,6 +71,55 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.6+1" version: "0.1.6+1"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
bitsdojo_window: bitsdojo_window:
dependency: "direct main" dependency: "direct main"
description: description:
@ -267,6 +316,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.3" version: "2.2.3"
dbus:
dependency: "direct main"
description:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.3"
fading_edge_scrollview: fading_edge_scrollview:
dependency: transitive dependency: transitive
description: description:
@ -429,13 +485,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
hotkey_manager:
dependency: "direct main"
description:
name: hotkey_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.7"
html: html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -499,41 +548,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.5.0" version: "4.5.0"
just_audio:
dependency: "direct main"
description:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.21"
just_audio_libwinmedia:
dependency: "direct main"
description:
name: just_audio_libwinmedia
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.7"
libwinmedia:
dependency: transitive
description:
name: libwinmedia
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.7"
lints: lints:
dependency: transitive dependency: transitive
description: description:

View File

@ -42,9 +42,6 @@ dependencies:
url_launcher: ^6.0.17 url_launcher: ^6.0.17
youtube_explode_dart: ^1.10.8 youtube_explode_dart: ^1.10.8
bitsdojo_window: ^0.1.2 bitsdojo_window: ^0.1.2
hotkey_manager: ^0.1.6
just_audio: ^0.9.18
just_audio_libwinmedia: ^0.0.4
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.8 path_provider: ^2.0.8
collection: ^1.15.0 collection: ^1.15.0
@ -64,6 +61,8 @@ dependencies:
skeleton_text: ^3.0.0 skeleton_text: ^3.0.0
hive: ^2.2.2 hive: ^2.2.2
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
dbus: ^0.7.3
audioplayers: ^1.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,19 +6,16 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <hotkey_manager/hotkey_manager_plugin.h>
#include <libwinmedia/libwinmedia_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BitsdojoWindowPluginRegisterWithRegistrar( BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
HotkeyManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
LibwinmediaPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LibwinmediaPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

@ -3,9 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
bitsdojo_window_windows bitsdojo_window_windows
hotkey_manager
libwinmedia
permission_handler_windows permission_handler_windows
url_launcher_windows url_launcher_windows
) )