diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 5d15080a..becf00bf 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -47,14 +47,15 @@ jobs: Set-Variable -Name HASH -Value (Get-FileHash dist\Spotube-windows-x86_64-setup.exe).Hash sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt make choco + + - run: mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg # Publish to Chocolatey Repository - run: | choco apikey -k ${{ secrets.CHOCO_API_KEY }} -s https://push.chocolatey.org/ + choco push dist/Spotube-windows-x86_64.nupkg echo 'published to community.chocolatey.org' - # choco push dist/${{ steps.tag.outputs.tag }}/Spotube-windows-x86_64.nupkg - # Upload artifacts - uses: actions/upload-artifact@v3 with: @@ -191,6 +192,10 @@ jobs: with: name: Spotube-Linux-Bundle path: ./Spotube-Linux-Bundle + - uses: actions/download-artifact@v3 + with: + name: Spotube-Android-Bundle + path: ./Spotube-Android-Bundle - name: Get latest tag id: tag uses: dawidd6/action-get-tag@v1 @@ -200,14 +205,14 @@ jobs: # generating checksums for all the binary - run: | tree . - md5sum ./Spotube-Windows-Bundle/*.{exe,nupkg} > RELEASE.md5sum - md5sum ./Spotube-Macos-Bundle/*.dmg > RELEASE.md5sum - md5sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} > RELEASE.md5sum - md5sum ./Spotube-Android-Bundle/*.apk > RELEASE.md5sum - sha256sum ./Spotube-Macos-Bundle/*.dmg > RELEASE.sha256sum - sha256sum ./Spotube-Windows-Bundle/*.{exe,nupkg} > RELEASE.sha256sum - sha256sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} > RELEASE.sha256sum - sha256sum ./Spotube-Android-Bundle/*.apk > RELEASE.sha256sum + md5sum ./Spotube-Windows-Bundle/*.{exe,nupkg} >> RELEASE.md5sum + md5sum ./Spotube-Macos-Bundle/*.dmg >> RELEASE.md5sum + md5sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} >> RELEASE.md5sum + md5sum ./Spotube-Android-Bundle/*.apk >> RELEASE.md5sum + sha256sum ./Spotube-Macos-Bundle/*.dmg >> RELEASE.sha256sum + sha256sum ./Spotube-Windows-Bundle/*.{exe,nupkg} >> RELEASE.sha256sum + sha256sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} >> RELEASE.sha256sum + sha256sum ./Spotube-Android-Bundle/*.apk >> RELEASE.sha256sum sed -i 's|Spotube-.*-Bundle/||' RELEASE.sha256sum RELEASE.md5sum # Upload release binary - uses: ncipollo/release-action@v1 @@ -252,11 +257,11 @@ jobs: strip_v: true - run: | python3 spotube/scripts/update_flathub_version.py ${{ steps.tag.outputs.tag }} + rm -rf spotube - uses: EndBug/add-and-commit@v9 with: message: v${{ steps.tag.outputs.tag }} Update - # push: origin master - push: false + push: origin master publish_aur: needs: update_release @@ -276,11 +281,11 @@ jobs: sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" aur-struct/PKGBUILD sed -i "s/%{{PKGREL}}%/1/" aur-struct/PKGBUILD sed -i "s/%{{LINUX_MD5}}%/`md5sum Spotube-Linux-Bundle/Spotube-linux-x86_64.tar.xz | awk '{print $1}'`/" aur-struct/PKGBUILD - # - uses: KSXGitHub/github-actions-deploy-aur@v2.2.5 - # with: - # pkgname: spotube-bin - # pkgbuild: aur-struct/PKGBUILD - # commit_username: ${{ secrets.AUR_USERNAME }} - # commit_email: ${{ secrets.AUR_EMAIL }} - # ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - # commit_message: Updated to v${{ steps.tag.outputs.tag }} + - uses: KSXGitHub/github-actions-deploy-aur@v2.2.5 + with: + pkgname: spotube-bin + pkgbuild: aur-struct/PKGBUILD + commit_username: ${{ secrets.AUR_USERNAME }} + commit_email: ${{ secrets.AUR_EMAIL }} + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: Updated to v${{ steps.tag.outputs.tag }} diff --git a/.metadata b/.metadata index fd70cabc..b80e7b3e 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 2248d357..6d27ad30 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -14,7 +14,8 @@ "compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64\\cl.exe", "cStandard": "c17", "cppStandard": "c++17", - "intelliSenseMode": "windows-msvc-x64" + "intelliSenseMode": "windows-msvc-x64", + "configurationProvider": "ms-vscode.makefile-tools" } ], "version": 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3ce8dc..c030ca2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# v2.1.0 + +### New +- Synced Lyrics (with fallback genius lyrics) +- Playlist create/delete +- Add/Remove tracks to own playlists +- Custom YouTube track search term template +- Downloading lyrics along with a track (can be toggled) +- Customize Marketplace location + +### Improved +- Spotify track to youtube track algorithm +- Genius lyrics matching algorithm +- Download track. Checks if already exists & replaces on user command +- Wide screen responsiveness & adaptation +- Bigger Title display (replaced word-break with Marquee Text for better visibility) (https://github.com/KRTirtho/spotube/pull/47) + +### Bug fixes +- Sequential playlist playback not working with latest webkit2gtk (https://github.com/KRTirtho/spotube/issues/46) +- Theme modification state doesn't persist (https://github.com/KRTirtho/spotube/issues/54) +- Wrong URI path for "Login with Spotify" tutorial (https://github.com/KRTirtho/spotube/issues/69) +- Card shadow showing in the background of TitleBar & Searchbar + # v2.0.0 ### New diff --git a/Makefile b/Makefile index 08f45a28..b7e8029d 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ inno: powershell .\build\iscc\iscc.exe scripts\windows-setup-creator.iss choco: - powershell cp dist\**\spotube-*-windows-setup.exe choco-struct\tools + powershell cp dist\Spotube-windows-x86_64-setup.exe choco-struct\tools powershell choco pack .\choco-struct\spotube.nuspec --outputdirectory dist apk: diff --git a/README.md b/README.md index 89aa4a3b..abdd72d8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Following are the features that currently spotube offers: - Playback control is on user's machine instead of server based - Small size & less data hungry - No spotify or youtube ads since it uses all public & free APIs (But it's recommended to support the creators by watching/liking/subscribing to the artists youtube channel or add as favourite track in spotify. Mostly buying spotify premium is the best way to support their valuable creations) -- Lyrics +- Synced Lyrics - Downloadable track Spotube - A lightweight+free Spotify crossplatform-client made with flutter | Product Hunt @@ -50,71 +50,26 @@ Following are the features that currently spotube offers: I'm always releasing newer versions of binary of the software each 2-3 month with minor changes & each 6-8 month with major changes. Grab the binaries -All the binaries are located in the [releases](https://github.com/krtirtho/spotube/releases), just download +| Platform | Package/Installation Method | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Android | [Android Download][android-dlink] | +| Debian/Ubuntu | [Linux Debian/Ubuntu Download][deb-dlink]
Then run: `sudo apt install Spotube-linux-x86_64.deb` | +| Flatpak | `flatpak install com.github.KRTirtho.Spotube`
Download on Flathub | +| Arch/Manjaro | pamac: `pamac install spotube-bin`
yay: `yay -Sy spotube-bin` | +| AppImage | [AppImage Download][appimage-dlink]
**Note**: AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed | +| Linux (tarball) | [Tarball Download][linux-dlink] | +| Windows | [Windows Download][win32-dlink] | +| Windows (Chocolatey) | `choco install spotube` | +| Windows (WinGet) | `winget install --id KRTirtho.Spotube` | +| MacOS | [MacOS Download][mac-dlink] | -## Android - -Download the [Android app](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk) & then install it on your Android smartphone/tablet - -## Windows - -Download the [setup file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe) & follow along the installer - -### Chocolatey - -Run the following command to install Spotube with windows chocolatey package manager -```powershell -choco install spotube -``` - -### Winget -Run the following command to install Spotube with new Windows Package Manager: -```powershell -winget install --id KRTirtho.Spotube -``` - -## Linux - -### Flatpak -Run in terminal: -```shell -$ flatpak install flathub com.github.KRTirtho.Spotube -``` -Download on Flathub - -### Ubuntu/Debian/Linux Mint/Pop_!OS: - Download the [Spotube-linux-x86_64.deb](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.deb) then double click it or run - ```bash - $ sudo apt install Spotube-linux-x86_64.deb - # or - $ sudo dpkg -i Spotube-linux-x86_64.deb - ``` - in the directory where it was downloaded - - -### Arch/Manjaro/Endeavour: - Run following terminal - ```bash - # for `yay` users - $ yay -S spotube-bin - # for `pamac` users - $ pamac install spotube-bin - ``` - - -### AppImage: - Download the [Spotube-linux-x86_64.AppImage](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage) file & double click to run it. AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed - -## Mac OS -Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard +> **Note!:** If you don't understand this download table. You can read [installation instructions][wiki-installation-instructions] from the wiki ## Nightly Builds Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/spotube/workflows/flutter-build/build) ## Optional Configurations -
- Login with Spotify - +### Login with Spotify You need a spotify account & a developer app for - clientId @@ -123,31 +78,28 @@ Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/sp **Grab credentials:** - Go to https://developer.spotify.com/dashboard/login & login with your spotify account (Skip if you're logged in) - ![Step 1](https://user-images.githubusercontent.com/61944859/111762106-d1d37680-88ca-11eb-9884-ec7a40c0dd27.png) + Step 1 - - Create an web app for Spotify Public API - ![step 2](https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png) + - Create an web app for Spotify Public API
+ step 2 - - Give the app a name & description. Then Edit settings & add **http://localhost:4304/auth/spotify/callback** as **Redirect URI** for the app. Its important for authenticating - ![setp-3](https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png) + - **MOST IMPORTANT:** Give the app a name & description. Then Edit settings & add `http://localhost:4304/auth/spotify/callback` as **Redirect URI** for the app. Its important for authenticating
+ setp-3 - - Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields - ![step-4](https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png) -
+ - Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields
+ step-4 -
-Setup Genius Lyrics +### Setup Genius Lyrics - Signup/Login into [genius](https://genius.com/signup) for **lyrics** -- Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client - ![Step 2](https://user-images.githubusercontent.com/61944859/158823216-b4942731-c4c5-46c8-8b60-82a372b51cc5.png) -- Generate & copy access token - ![Step 3](https://user-images.githubusercontent.com/61944859/158822817-f04da060-3094-4a3b-8ace-a936d0cda8db.png) -- Paste the copied access token in Spotube's Settings - ![Step 4](https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png) +- Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client
+ Step 2 +- Generate & copy access token
+ Step 3 +- Paste the copied access token in Spotube's Settings
+ Step 4 > **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself -
# TODO: @@ -173,7 +125,7 @@ You can find the details [here](CONTRIBUTION.md#your-first-code-contribution) Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p) -# Relevant Project/Tools Links +# Library/Plugin/Framework Credits - [Flutter](https://flutter.dev/) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase - [Linux](https://www.linux.org/) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution @@ -197,6 +149,9 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour - [logger](https://github.com/leisim/logger) - Small, easy to use and extensible logger which prints beautiful logs - [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. - [permission_handler](https://github.com/baseflow/flutter-permission-handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. +- [marquee](https://github.com/MarcelGarus/marquee) - ⏩ A Flutter widget that scrolls text infinitely. Provides many customizations including custom scroll directions, durations, curves as well as pauses after every round +- [scroll_to_index](https://github.com/quire-io/scroll-to-index) - scroll to index with fixed/variable row height inside Flutter scrollable widget +- [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/) - This Flutter plugin provides an API for querying information about an application package. # Social handlers @@ -205,3 +160,14 @@ Follow me on [Twitter](https://twitter.com/@krtirtho) for newer updates about th

© 2022 Spotube

+ + + +[win32-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe +[deb-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.deb +[linux-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.tar.xz +[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 +[android-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk + +[wiki-installation-instructions]: https://github.com/KRTirtho/spotube/wiki/Installation-Instrcutions \ No newline at end of file diff --git a/bin/create-secrets.dart b/bin/create-secrets.dart index feb0d7fe..e59c5668 100644 --- a/bin/create-secrets.dart +++ b/bin/create-secrets.dart @@ -3,6 +3,139 @@ import 'dart:io'; import 'package:path/path.dart' as path; +// blob metadata for de-stringifying +const randHash = [ + 49, + 111, + 98, + 72, + 78, + 122, + 98, + 48, + 112, + 73, + 81, + 50, + 112, + 89, + 90, + 50, + 116, + 83, + 84, + 110, + 99, + 105, + 76, + 67, + 74, + 67, + 89, + 121, + 48, + 119, + 77, + 106, + 69, + 50, + 86, + 69, + 53, + 107, + 77, + 69, + 86, + 71, + 101, + 68, + 66, + 113, + 78, + 110, + 66, + 119 +]; +const sugarCarbonator = [ + 81, + 119, + 79, + 71, + 85, + 53, + 78, + 50, + 69, + 52, + 90, + 68, + 107, + 120, + 77, + 87, + 89, + 52, + 89, + 84, + 73 +]; +const randomSalt = [ + 70, + 117, + 67, + 75, + 117, + 116, + 72, + 101, + 105, + 102, + 65, + 110, + 68, + 87, + 72, + 97, + 84, + 85, + 82, + 100, + 79, + 73, + 110, + 103, + 83, + 117, + 75, + 115 +]; +const algorithmicSugar = [ + 70, + 117, + 67, + 75, + 117, + 116, + 72, + 101, + 105, + 102, + 65, + 78, + 100, + 102, + 68, + 114, + 79, + 105, + 100, + 115, + 85, + 99, + 107, + 83 +]; + void main(List args) async { List val; List val2; @@ -10,7 +143,7 @@ void main(List args) async { final cwd = Directory.current.path; final binSafe = cwd.endsWith("/bin") ? ".." : ""; if (args.isEmpty) { - throw ArgumentError("Expected 1-2 arguments but passed none"); + throw ArgumentError("Expected 1-3 arguments but passed none"); } if (args.contains("--local")) { final secretFilePath = path.join(cwd, binSafe, "secrets.json"); @@ -19,11 +152,22 @@ void main(List args) async { final data = jsonDecode(await file.readAsString()); val = List.castFrom(data["LYRICS_SECRET"]); val2 = List.castFrom(data["SPOTIFY_SECRET"]); + } else if (args.contains("--fdroid")) { + final decodedLyricSecret = utf8.decode(base64Decode( + args[1].replaceAll( + String.fromCharCodes(randomSalt), String.fromCharCodes(randHash)), + )); + final decodedSpotifySecret = utf8.decode(base64Decode( + args.last.replaceAll(String.fromCharCodes(algorithmicSugar), + String.fromCharCodes(sugarCarbonator)), + )); + val = List.castFrom(jsonDecode(decodedLyricSecret)); + val2 = List.castFrom(jsonDecode(decodedSpotifySecret)); } else { final decodedLyricSecret = utf8.decode(base64Decode(args.first)); - final decodedSpotifySecrete = utf8.decode(base64Decode(args.last)); + final decodedSpotifySecret = utf8.decode(base64Decode(args.last)); val = List.castFrom(jsonDecode(decodedLyricSecret)); - val2 = List.castFrom(jsonDecode(decodedSpotifySecrete)); + val2 = List.castFrom(jsonDecode(decodedSpotifySecret)); } await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart")) diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index b2052f1c..b7527b89 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:oauth2/oauth2.dart' show AuthorizationException; import 'package:spotify/spotify.dart' hide Image, Player, Search; import 'package:spotube/components/Category/CategoryCard.dart'; @@ -17,16 +16,11 @@ import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; -import 'package:spotube/helpers/get-random-element.dart'; -import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/useSharedPreferences.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/models/generated_secrets.dart'; -import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -48,7 +42,6 @@ class Home extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - Auth auth = ref.watch(authProvider); String recommendationMarket = ref.watch(userPreferencesProvider.select( (value) => (value.recommendationMarket), )); @@ -98,70 +91,12 @@ class Home extends HookConsumerWidget { }, [recommendationMarket]); useEffect(() { - if (localStorage == null) return null; - final String? clientId = - localStorage.getString(LocalStorageKeys.clientId); - final String? clientSecret = - localStorage.getString(LocalStorageKeys.clientSecret); - final String? accessToken = - localStorage.getString(LocalStorageKeys.accessToken); - final String? refreshToken = - localStorage.getString(LocalStorageKeys.refreshToken); - final String? expirationStr = - localStorage.getString(LocalStorageKeys.expiration); - try { - final DateTime? expiration = - expirationStr != null ? DateTime.parse(expirationStr) : null; - final anonCred = getRandomElement(spotifySecrets); - SpotifyApiCredentials apiCredentials = - clientId != null && clientSecret != null - ? SpotifyApiCredentials( - clientId, - clientSecret, - accessToken: accessToken, - refreshToken: refreshToken, - expiration: expiration, - scopes: spotifyScopes, - ) - : SpotifyApiCredentials( - anonCred["clientId"], - anonCred["clientSecret"], - ); - - SpotifyApi spotify = SpotifyApi(apiCredentials); - if (clientId != null && clientSecret != null) { - spotify.getCredentials().then((credentials) { - if (credentials.accessToken?.isNotEmpty == true) { - auth.setAuthState( - clientId: clientId, - clientSecret: clientSecret, - accessToken: - credentials.accessToken, // accessToken can be new/refreshed - refreshToken: refreshToken, - expiration: credentials.expiration, - isLoggedIn: true, - ); - } - pagingController.addPageRequestListener(listener); - // the world is full of surprises and the previously working - // fine pageRequestListener now doesn't notify the listeners - // automatically after assigning a listener. So doing it manually - pagingController.notifyPageRequestListeners(0); - }).catchError((e, stack) { - if (e is AuthorizationException) { - oauthLogin( - auth, - clientId: clientId, - clientSecret: clientSecret, - ); - } - logger.e("useEffect.spotify.getCredentials", e, stack); - }); - } else { - pagingController.addPageRequestListener(listener); - pagingController.notifyPageRequestListeners(0); - } + pagingController.addPageRequestListener(listener); + // the world is full of surprises and the previously working + // fine pageRequestListener now doesn't notify the listeners + // automatically after assigning a listener. So doing it manually + pagingController.notifyPageRequestListeners(0); } catch (e, stack) { logger.e("initState", e, stack); } diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index ffd5219e..ae67b877 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -81,7 +81,7 @@ class Sidebar extends HookConsumerWidget { trailing: FutureBuilder( future: spotify.me.get(), builder: (context, snapshot) { - var avatarImg = imageToUrlString(snapshot.data?.images, + final avatarImg = imageToUrlString(snapshot.data?.images, index: (snapshot.data?.images?.length ?? 1) - 1); return extended.value ? Padding( diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 46524930..f7eabc1f 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -21,7 +21,7 @@ class _UserArtistsState extends ConsumerState { @override void initState() { super.initState(); - WidgetsBinding.instance?.addPostFrameCallback((timestamp) { + WidgetsBinding.instance.addPostFrameCallback((timestamp) { _pagingController.addPageRequestListener((pageKey) async { try { SpotifyApi spotifyApi = ref.read(spotifyProvider); diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index bd8d101f..0d7b9cdf 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -92,7 +92,7 @@ class Player extends HookConsumerWidget { // I can't believe useEffect doesn't run Post Frame aka // after rendering/painting the UI // `My disappointment is immeasurable and my day is ruined` XD - WidgetsBinding.instance?.addPostFrameCallback((time) { + WidgetsBinding.instance.addPostFrameCallback((time) { // clearing the overlay-entry as passing the already available // entry will result in splashing while resizing the window if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 71033f93..15d99e21 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; @@ -21,8 +20,6 @@ class PlayerControls extends HookConsumerWidget { final Playback playback = ref.watch(playbackProvider); final AudioPlayer player = playback.player; - final _shuffled = useState(false); - final onNext = useNextTrack(playback); final onPrevious = usePreviousTrack(playback); @@ -92,7 +89,7 @@ class PlayerControls extends HookConsumerWidget { children: [ IconButton( icon: const Icon(Icons.shuffle_rounded), - color: _shuffled.value + color: playback.shuffled ? Theme.of(context).primaryColor : iconColor, onPressed: () { @@ -101,12 +98,10 @@ class PlayerControls extends HookConsumerWidget { return; } try { - if (!_shuffled.value) { - playback.currentPlaylist!.shuffle(); - _shuffled.value = true; + if (!playback.shuffled) { + playback.shuffle(); } else { - playback.currentPlaylist!.unshuffle(); - _shuffled.value = false; + playback.unshuffle(); } } catch (e, stack) { logger.e("onShuffle", e, stack); @@ -140,7 +135,6 @@ class PlayerControls extends HookConsumerWidget { try { await player.pause(); await player.seek(Duration.zero); - _shuffled.value = false; playback.reset(); } catch (e, stack) { logger.e("onStop", e, stack); diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index eb66945b..8216af3e 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -29,7 +29,7 @@ class PlayerView extends HookConsumerWidget { useEffect(() { if (breakpoint.isMoreThan(Breakpoints.md)) { - WidgetsBinding.instance?.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { GoRouter.of(context).pop(); }); } diff --git a/lib/components/Settings/ColorSchemePickerDialog.dart b/lib/components/Settings/ColorSchemePickerDialog.dart new file mode 100644 index 00000000..89d2984b --- /dev/null +++ b/lib/components/Settings/ColorSchemePickerDialog.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/UserPreferences.dart'; + +final colorsMap = { + "Red": Colors.red, + "Pink": Colors.pink, + "Purple": Colors.purple, + "DeepPurple": Colors.deepPurple, + "Indigo": Colors.indigo, + "Blue": Colors.blue, + "LightBlue": Colors.lightBlue, + "Cyan": Colors.cyan, + "Teal": Colors.teal, + "Green": Colors.green, + "LightGreen": Colors.lightGreen, + "Lime": Colors.lime, + "Yellow": Colors.yellow, + "Amber": Colors.amber, + "Orange": Colors.orange, + "DeepOrange": Colors.deepOrange, + "Brown": Colors.brown, + "BlueGrey": Colors.blueGrey, + "Grey": Colors.grey, +}; + +enum ColorSchemeType { + accent, + background, +} + +class ColorSchemePickerDialog extends HookConsumerWidget { + final ColorSchemeType schemeType; + const ColorSchemePickerDialog({required this.schemeType, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final preferences = ref.watch(userPreferencesProvider); + final scheme = schemeType == ColorSchemeType.accent + ? preferences.accentColorScheme + : preferences.backgroundColorScheme; + final active = useState(colorsMap.entries.firstWhere( + (element) { + return scheme.value == element.value.value; + }, + ).key); + + return AlertDialog( + title: Text("Pick ${schemeType.name} color scheme"), + actions: [ + TextButton( + child: const Text("Cancel"), + onPressed: () { + Navigator.pop(context); + }, + ), + ElevatedButton( + child: const Text("Save"), + onPressed: () { + switch (schemeType) { + case ColorSchemeType.accent: + preferences.setAccentColorScheme(colorsMap[active.value]!); + break; + default: + preferences.setBackgroundColorScheme( + colorsMap[active.value]!, + ); + } + Navigator.pop(context); + }, + ) + ], + content: SizedBox( + height: 200, + width: 400, + child: Center( + child: Wrap( + spacing: 10, + runSpacing: 10, + children: colorsMap.entries + .map( + (e) => ColorTile( + color: e.value, + isActive: active.value == e.key, + tooltip: e.key, + onPressed: () { + active.value = e.key; + }, + ), + ) + .toList(), + ), + ), + ), + ); + } +} + +class ColorTile extends StatelessWidget { + final MaterialColor color; + final bool isActive; + final void Function()? onPressed; + final String? tooltip; + const ColorTile({ + required this.color, + this.isActive = false, + this.onPressed, + this.tooltip = "", + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: GestureDetector( + onTap: onPressed, + child: Container( + height: 50, + width: 50, + decoration: BoxDecoration( + border: isActive + ? const Border.fromBorderSide( + BorderSide(color: Colors.black, width: 5), + ) + : null, + shape: BoxShape.circle, + color: color, + ), + ), + ), + ); + } +} diff --git a/lib/components/Login.dart b/lib/components/Settings/Login.dart similarity index 100% rename from lib/components/Login.dart rename to lib/components/Settings/Login.dart diff --git a/lib/components/Settings.dart b/lib/components/Settings/Settings.dart similarity index 90% rename from lib/components/Settings.dart rename to lib/components/Settings/Settings.dart index d09739bd..9747eb92 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -39,6 +40,16 @@ class Settings extends HookConsumerWidget { packageName: 'spotube', ); + final pickColorScheme = useCallback((ColorSchemeType schemeType) { + return () => showDialog( + context: context, + builder: (context) { + return ColorSchemePickerDialog( + schemeType: schemeType, + ); + }); + }, []); + return SafeArea( child: Scaffold( appBar: PageWindowTitleBar( @@ -159,6 +170,27 @@ class Settings extends HookConsumerWidget { ], ), const SizedBox(height: 10), + ListTile( + title: const Text("Accent Color Scheme"), + trailing: ColorTile( + color: preferences.accentColorScheme, + onPressed: pickColorScheme(ColorSchemeType.accent), + isActive: true, + ), + onTap: pickColorScheme(ColorSchemeType.accent), + ), + const SizedBox(height: 10), + ListTile( + title: const Text("Background Color Scheme"), + trailing: ColorTile( + color: preferences.backgroundColorScheme, + onPressed: + pickColorScheme(ColorSchemeType.background), + isActive: true, + ), + onTap: pickColorScheme(ColorSchemeType.background), + ), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/components/Shared/RecordHotKeyDialog.dart b/lib/components/Shared/RecordHotKeyDialog.dart index 4114f3c6..ff8a6885 100644 --- a/lib/components/Shared/RecordHotKeyDialog.dart +++ b/lib/components/Shared/RecordHotKeyDialog.dart @@ -13,7 +13,7 @@ class RecordHotKeyDialog extends HookWidget { @override Widget build(BuildContext context) { - var _hotKey = useState(HotKey(null)); + final _hotKey = useState(null); return AlertDialog( content: SingleChildScrollView( child: ListBody( @@ -72,10 +72,10 @@ class RecordHotKeyDialog extends HookWidget { ), TextButton( child: const Text('OK'), - onPressed: !_hotKey.value.isSetted + onPressed: _hotKey.value == null ? null : () { - onHotKeyRecorded(_hotKey.value); + onHotKeyRecorded(_hotKey.value!); GoRouter.of(context).pop(); }, ), diff --git a/lib/helpers/oauth-login.dart b/lib/helpers/oauth-login.dart index c89197d8..12cd5499 100644 --- a/lib/helpers/oauth-login.dart +++ b/lib/helpers/oauth-login.dart @@ -26,7 +26,7 @@ Future oauthLogin(Auth auth, if (responseUri != null) { final SpotifyApi spotify = SpotifyApi.fromAuthCodeGrant(grant, responseUri); - var credentials = await spotify.getCredentials(); + final credentials = await spotify.getCredentials(); if (credentials.accessToken != null) { accessToken = credentials.accessToken; await localStorage.setString( @@ -56,7 +56,6 @@ Future oauthLogin(Auth auth, accessToken: accessToken, refreshToken: refreshToken, expiration: expiration, - isLoggedIn: true, ); } catch (e, stack) { logger.e("oauthLogin", e, stack); diff --git a/lib/helpers/search-youtube.dart b/lib/helpers/search-youtube.dart index afba453c..5d449144 100644 --- a/lib/helpers/search-youtube.dart +++ b/lib/helpers/search-youtube.dart @@ -30,7 +30,7 @@ Future toSpotubeTrack( .replaceAll("\$FEATURED_ARTISTS", featuredArtists); logger.v("[Youtube Search Term] $queryString"); - SearchList videos = await youtube.search.getVideos(queryString); + VideoSearchList videos = await youtube.search.search(queryString); List ratedRankedVideos = videos .map((video) { diff --git a/lib/helpers/server_ipc.dart b/lib/helpers/server_ipc.dart index f44d1dc5..c8f0de2e 100644 --- a/lib/helpers/server_ipc.dart +++ b/lib/helpers/server_ipc.dart @@ -8,7 +8,7 @@ final logger = getLogger("ServerIPC"); Future connectIpc(String authUri, String redirectUri) async { try { logger.i("[Launching]: $authUri"); - await launch(authUri); + await launchUrl(Uri.parse(authUri)); HttpServer server = await HttpServer.bind(InternetAddress.loopbackIPv4, 4304); @@ -21,7 +21,7 @@ Future connectIpc(String authUri, String redirectUri) async { if (code != null) { request.response ..statusCode = HttpStatus.ok - ..write("Authentication successful") + ..write("Authentication successful. Now Go back to Spotube") ..close(); return "$redirectUri?code=$code"; } else { diff --git a/lib/hooks/useIsCurrentRoute.dart b/lib/hooks/useIsCurrentRoute.dart index eeb1ff77..96c55a05 100644 --- a/lib/hooks/useIsCurrentRoute.dart +++ b/lib/hooks/useIsCurrentRoute.dart @@ -6,7 +6,7 @@ bool? useIsCurrentRoute([String matcher = "/"]) { final isCurrentRoute = useState(null); final context = useContext(); useEffect(() { - WidgetsBinding.instance?.addPostFrameCallback((timer) { + WidgetsBinding.instance.addPostFrameCallback((timer) { final isCurrent = GoRouter.of(context).location == matcher; if (isCurrent != isCurrentRoute.value) { isCurrentRoute.value = isCurrent; diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart index b6cebd5a..5d665b33 100644 --- a/lib/hooks/usePaletteColor.dart +++ b/lib/hooks/usePaletteColor.dart @@ -9,7 +9,7 @@ PaletteColor usePaletteColor(BuildContext context, String imageUrl) { final mounted = useIsMounted(); useEffect(() { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final palette = await PaletteGenerator.fromImageProvider( CachedNetworkImageProvider( imageUrl, diff --git a/lib/main.dart b/lib/main.dart index 61e868ad..747ec653 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,8 @@ import 'package:spotube/provider/AudioPlayer.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/YouTube.dart'; import 'package:just_audio_background/just_audio_background.dart'; +import 'package:spotube/themes/dark-theme.dart'; +import 'package:spotube/themes/light-theme.dart'; void main() async { if (Platform.isAndroid || Platform.isIOS) { @@ -45,6 +47,10 @@ class MyApp extends HookConsumerWidget { Widget build(BuildContext context, ref) { final themeMode = ref.watch(userPreferencesProvider.select((s) => s.themeMode)); + final accentMaterialColor = + ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme)); + final backgroundMaterialColor = ref + .watch(userPreferencesProvider.select((s) => s.backgroundColorScheme)); final player = ref.watch(audioPlayerProvider); final youtube = ref.watch(youtubeProvider); useEffect(() { @@ -59,98 +65,13 @@ class MyApp extends HookConsumerWidget { routerDelegate: _router.routerDelegate, debugShowCheckedModeBanner: false, title: 'Spotube', - theme: ThemeData( - primaryColor: Colors.green, - primarySwatch: Colors.green, - buttonTheme: const ButtonThemeData( - buttonColor: Colors.green, - ), - shadowColor: Colors.grey[300], - backgroundColor: Colors.white, - textTheme: TextTheme( - bodyText1: TextStyle(color: Colors.grey[850]), - headline1: TextStyle(color: Colors.grey[850]), - headline2: TextStyle(color: Colors.grey[850]), - headline3: TextStyle(color: Colors.grey[850]), - headline4: TextStyle(color: Colors.grey[850]), - headline5: TextStyle(color: Colors.grey[850]), - headline6: TextStyle(color: Colors.grey[850]), - ), - listTileTheme: ListTileThemeData( - iconColor: Colors.grey[850], - horizontalTitleGap: 0, - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.green[400]!, - width: 2.0, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey[800]!, - ), - ), - ), - navigationRailTheme: NavigationRailThemeData( - backgroundColor: Colors.blueGrey[50], - unselectedIconTheme: - IconThemeData(color: Colors.grey[850], opacity: 1), - unselectedLabelTextStyle: TextStyle( - color: Colors.grey[850], - ), - ), - navigationBarTheme: NavigationBarThemeData( - backgroundColor: Colors.blueGrey[50], - height: 55, - ), - cardTheme: CardTheme( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - color: Colors.white, - ), + theme: lightTheme( + accentMaterialColor: accentMaterialColor, + backgroundMaterialColor: backgroundMaterialColor, ), - darkTheme: ThemeData( - brightness: Brightness.dark, - primaryColor: Colors.green, - primarySwatch: Colors.green, - backgroundColor: Colors.blueGrey[900], - scaffoldBackgroundColor: Colors.blueGrey[900], - dialogBackgroundColor: Colors.blueGrey[800], - shadowColor: Colors.black26, - popupMenuTheme: PopupMenuThemeData(color: Colors.blueGrey[800]), - buttonTheme: const ButtonThemeData( - buttonColor: Colors.green, - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.green[400]!, - width: 2.0, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey[800]!, - ), - ), - ), - navigationRailTheme: NavigationRailThemeData( - backgroundColor: Colors.blueGrey[800], - unselectedIconTheme: const IconThemeData(opacity: 1), - ), - navigationBarTheme: NavigationBarThemeData( - backgroundColor: Colors.blueGrey[800], - height: 55, - ), - cardTheme: CardTheme( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - color: Colors.blueGrey[900], - elevation: 20, - ), - canvasColor: Colors.blueGrey[900], + darkTheme: darkTheme( + accentMaterialColor: accentMaterialColor, + backgroundMaterialColor: backgroundMaterialColor, ), themeMode: themeMode, ); diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index e734b7bd..3d91cc0b 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -1,14 +1,13 @@ import 'package:go_router/go_router.dart'; -import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; -import 'package:spotube/components/Login.dart'; +import 'package:spotube/components/Settings/Login.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; -import 'package:spotube/components/Settings.dart'; +import 'package:spotube/components/Settings/Settings.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; GoRouter createGoRouter() => GoRouter( diff --git a/lib/models/LocalStorageKeys.dart b/lib/models/LocalStorageKeys.dart index 0fedb95a..43e3b012 100644 --- a/lib/models/LocalStorageKeys.dart +++ b/lib/models/LocalStorageKeys.dart @@ -3,10 +3,10 @@ abstract class LocalStorageKeys { static String recommendationMarket = 'recommendation_market'; static String ytSearchFormate = 'youtube_search_format'; - static String clientId = 'client_id'; - static String clientSecret = 'client_secret'; - static String accessToken = 'access_token'; - static String refreshToken = 'refresh_token'; + static String clientId = 'clientId'; + static String clientSecret = 'clientSecret'; + static String accessToken = 'accessToken'; + static String refreshToken = 'refreshToken'; static String expiration = "expiration"; static String geniusAccessToken = "genius_access_token"; diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index 48e24567..b34df796 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -1,26 +1,32 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:async'; -class Auth with ChangeNotifier { +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/utils/PersistedChangeNotifier.dart'; + +class Auth extends PersistedChangeNotifier { String? _clientId; String? _clientSecret; String? _accessToken; String? _refreshToken; DateTime? _expiration; - bool _isLoggedIn = false; + Auth() : super(); String? get clientId => _clientId; String? get clientSecret => _clientSecret; String? get accessToken => _accessToken; String? get refreshToken => _refreshToken; DateTime? get expiration => _expiration; - bool get isLoggedIn => _isLoggedIn; + bool get isAnonymous => - !_isLoggedIn && _clientId == null && _clientSecret == null; + _clientId == null && + _clientSecret == null && + accessToken == null && + refreshToken == null; + + bool get isLoggedIn => !isAnonymous && _expiration != null; void setAuthState({ - bool? isLoggedIn, bool safe = true, String? clientId, String? clientSecret, @@ -31,7 +37,6 @@ class Auth with ChangeNotifier { if (safe) { if (clientId != null) _clientId = clientId; if (clientSecret != null) _clientSecret = clientSecret; - if (isLoggedIn != null) _isLoggedIn = isLoggedIn; if (refreshToken != null) _refreshToken = refreshToken; if (accessToken != null) _accessToken = accessToken; if (expiration != null) _expiration = expiration; @@ -43,6 +48,7 @@ class Auth with ChangeNotifier { _expiration = expiration; } notifyListeners(); + updatePersistence(); } logout() { @@ -51,14 +57,34 @@ class Auth with ChangeNotifier { _accessToken = null; _refreshToken = null; _expiration = null; - _isLoggedIn = false; notifyListeners(); + updatePersistence(); } @override String toString() { return "Auth(clientId: $clientId, clientSecret: $clientSecret, accessToken: $accessToken, refreshToken: $refreshToken, expiration: $expiration, isLoggedIn: $isLoggedIn)"; } + + @override + FutureOr loadFromLocal(Map map) { + _clientId = map["clientId"]; + _clientSecret = map["clientSecret"]; + _accessToken = map["accessToken"]; + _refreshToken = map["refreshToken"]; + _expiration = DateTime.tryParse(map["expiration"]); + } + + @override + FutureOr> toMap() { + return { + "clientId": _clientId, + "clientSecret": _clientSecret, + "accessToken": _accessToken, + "refreshToken": _refreshToken, + "expiration": _expiration.toString(), + }; + } } -var authProvider = ChangeNotifierProvider((ref) => Auth()); +final authProvider = ChangeNotifierProvider((ref) => Auth()); diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 1cc93fdf..8fe1cf1f 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -31,20 +31,24 @@ class CurrentPlaylist { List get trackIds => tracks.map((e) => e.id!).toList(); - void shuffle() { + bool shuffle() { // won't shuffle if already shuffled if (_tempTrack == null) { _tempTrack = [...tracks]; tracks.shuffle(); + return true; } + return false; } - void unshuffle() { + bool unshuffle() { // without _tempTracks unshuffling can't be done if (_tempTrack != null) { tracks = [..._tempTrack!]; _tempTrack = null; + return true; } + return false; } } @@ -66,6 +70,7 @@ class Playback extends ChangeNotifier { StreamSubscription? _positionStreamListener; Duration _prevPosition = Duration.zero; + bool _shuffled = false; AudioPlayer player; YoutubeExplode youtube; @@ -138,6 +143,7 @@ class Playback extends ChangeNotifier { }); } + bool get shuffled => _shuffled; CurrentPlaylist? get currentPlaylist => _currentPlaylist; Track? get currentTrack => _currentTrack; bool get isPlaying => _isPlaying; @@ -158,6 +164,7 @@ class Playback extends ChangeNotifier { void reset() { _logger.v("Playback Reset"); _isPlaying = false; + _shuffled = false; duration = null; _currentPlaylist = null; _currentTrack = null; @@ -265,6 +272,20 @@ class Playback extends ChangeNotifier { _logger.e("startPlaying", e, stack); } } + + void shuffle() { + if (currentPlaylist?.shuffle() == true) { + _shuffled = true; + notifyListeners(); + } + } + + void unshuffle() { + if (currentPlaylist?.unshuffle() == true) { + _shuffled = false; + notifyListeners(); + } + } } final playbackProvider = ChangeNotifierProvider((ref) { diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index 4383d3a2..db07a9fa 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -1,13 +1,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/helpers/get-random-element.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/provider/Auth.dart'; -var spotifyProvider = Provider((ref) { +final spotifyProvider = Provider((ref) { Auth authState = ref.watch(authProvider); final anonCred = getRandomElement(spotifySecrets); SpotifyApiCredentials apiCredentials = authState.isAnonymous @@ -26,20 +24,13 @@ var spotifyProvider = Provider((ref) { return SpotifyApi( apiCredentials, - onCredentialsRefreshed: (credentials) async { - SharedPreferences localStorage = await SharedPreferences.getInstance(); - localStorage.setString( - LocalStorageKeys.refreshToken, - credentials.refreshToken!, - ); - localStorage.setString( - LocalStorageKeys.accessToken, - credentials.accessToken!, - ); - localStorage.setString(LocalStorageKeys.clientId, credentials.clientId!); - localStorage.setString( - LocalStorageKeys.clientSecret, - credentials.clientSecret!, + onCredentialsRefreshed: (credentials) { + authState.setAuthState( + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + accessToken: credentials.accessToken, + refreshToken: credentials.refreshToken, + expiration: credentials.expiration, ); }, ); diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 3836115c..6ffd7c83 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -1,162 +1,139 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/helpers/get-random-element.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; -import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/generated_secrets.dart'; +import 'package:spotube/utils/PersistedChangeNotifier.dart'; +import 'package:collection/collection.dart'; -class UserPreferences extends ChangeNotifier { +class UserPreferences extends PersistedChangeNotifier { ThemeMode themeMode; String ytSearchFormat; String recommendationMarket; bool saveTrackLyrics; String geniusAccessToken; - SharedPreferences? localStorage; HotKey? nextTrackHotKey; HotKey? prevTrackHotKey; HotKey? playPauseHotKey; + + MaterialColor accentColorScheme; + MaterialColor backgroundColorScheme; UserPreferences({ required this.geniusAccessToken, required this.recommendationMarket, required this.themeMode, required this.ytSearchFormat, this.saveTrackLyrics = false, + this.accentColorScheme = Colors.green, + this.backgroundColorScheme = Colors.grey, this.nextTrackHotKey, this.prevTrackHotKey, this.playPauseHotKey, - }) { - onInit(); - } - - final logger = getLogger(UserPreferences); - - Future _getHotKeyFromLocalStorage(String key) async { - String? str = localStorage?.getString(key); - if (str != null) { - Map json = await jsonDecode(str); - if (json.isEmpty) { - return null; - } - return HotKey.fromJson(json); - } - return null; - } - - Future onInit() async { - try { - localStorage = await SharedPreferences.getInstance(); - String? accessToken = - localStorage?.getString(LocalStorageKeys.geniusAccessToken); - - saveTrackLyrics = - localStorage?.getBool(LocalStorageKeys.saveTrackLyrics) ?? false; - - final themeModeRaw = localStorage?.getString(LocalStorageKeys.themeMode); - switch (themeModeRaw) { - case "light": - themeMode = ThemeMode.light; - break; - case "dark": - themeMode = ThemeMode.dark; - break; - default: - themeMode = ThemeMode.system; - } - - recommendationMarket = - localStorage?.getString(LocalStorageKeys.recommendationMarket) ?? - 'US'; - geniusAccessToken = accessToken != null && accessToken.isNotEmpty - ? accessToken - : getRandomElement(lyricsSecrets); - - nextTrackHotKey ??= (await _getHotKeyFromLocalStorage( - LocalStorageKeys.nextTrackHotKey, - )) ?? - HotKey( - KeyCode.slash, - modifiers: [KeyModifier.control, KeyModifier.alt], - ); - - prevTrackHotKey ??= (await _getHotKeyFromLocalStorage( - LocalStorageKeys.prevTrackHotKey, - )) ?? - HotKey( - KeyCode.comma, - modifiers: [KeyModifier.control, KeyModifier.alt], - ); - - playPauseHotKey ??= (await _getHotKeyFromLocalStorage( - LocalStorageKeys.playPauseHotKey, - )) ?? - HotKey( - KeyCode.period, - modifiers: [KeyModifier.control, KeyModifier.alt], - ); - notifyListeners(); - } catch (e, stack) { - logger.e("onInit", e, stack); - } - } + }) : super(); void setThemeMode(ThemeMode mode) { themeMode = mode; - localStorage?.setString(LocalStorageKeys.themeMode, mode.name); notifyListeners(); + updatePersistence(); } void setSaveTrackLyrics(bool shouldSave) { saveTrackLyrics = shouldSave; - localStorage?.setBool(LocalStorageKeys.saveTrackLyrics, shouldSave); notifyListeners(); + updatePersistence(); } void setRecommendationMarket(String country) { recommendationMarket = country; - localStorage?.setString(LocalStorageKeys.recommendationMarket, country); notifyListeners(); + updatePersistence(); } void setGeniusAccessToken(String token) { geniusAccessToken = token; notifyListeners(); + updatePersistence(); } void setNextTrackHotKey(HotKey? value) { nextTrackHotKey = value; - localStorage?.setString( - LocalStorageKeys.nextTrackHotKey, - jsonEncode(value?.toJson() ?? {}), - ); notifyListeners(); + updatePersistence(); } void setPrevTrackHotKey(HotKey? value) { prevTrackHotKey = value; - localStorage?.setString( - LocalStorageKeys.prevTrackHotKey, - jsonEncode(value?.toJson() ?? {}), - ); notifyListeners(); + updatePersistence(); } void setPlayPauseHotKey(HotKey? value) { playPauseHotKey = value; - localStorage?.setString( - LocalStorageKeys.playPauseHotKey, - jsonEncode(value?.toJson() ?? {}), - ); notifyListeners(); + updatePersistence(); } void setYtSearchFormat(String format) { ytSearchFormat = format; - localStorage?.setString(LocalStorageKeys.ytSearchFormate, format); notifyListeners(); + updatePersistence(); + } + + void setAccentColorScheme(MaterialColor color) { + accentColorScheme = color; + notifyListeners(); + updatePersistence(); + } + + void setBackgroundColorScheme(MaterialColor color) { + backgroundColorScheme = color; + notifyListeners(); + updatePersistence(); + } + + @override + FutureOr loadFromLocal(Map map) { + saveTrackLyrics = map["saveTrackLyrics"] ?? false; + recommendationMarket = map["recommendationMarket"] ?? recommendationMarket; + geniusAccessToken = + 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; + themeMode = ThemeMode.values[map["themeMode"] ?? 0]; + backgroundColorScheme = colorsMap.values + .firstWhereOrNull((e) => e.value == map["backgroundColorScheme"]) ?? + backgroundColorScheme; + accentColorScheme = colorsMap.values + .firstWhereOrNull((e) => e.value == map["accentColorScheme"]) ?? + accentColorScheme; + } + + @override + FutureOr> toMap() { + return { + "saveTrackLyrics": saveTrackLyrics, + "recommendationMarket": recommendationMarket, + "geniusAccessToken": geniusAccessToken, + "nextTrackHotKey": jsonEncode(nextTrackHotKey?.toJson() ?? {}), + "prevTrackHotKey": jsonEncode(prevTrackHotKey?.toJson() ?? {}), + "playPauseHotKey": jsonEncode(playPauseHotKey?.toJson() ?? {}), + "ytSearchFormat": ytSearchFormat, + "themeMode": themeMode.index, + "backgroundColorScheme": backgroundColorScheme.value, + "accentColorScheme": accentColorScheme.value, + }; } } diff --git a/lib/themes/dark-theme.dart b/lib/themes/dark-theme.dart new file mode 100644 index 00000000..968a147f --- /dev/null +++ b/lib/themes/dark-theme.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +ThemeData darkTheme({ + required MaterialColor accentMaterialColor, + required MaterialColor backgroundMaterialColor, +}) { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + primaryColor: accentMaterialColor, + primarySwatch: accentMaterialColor, + backgroundColor: backgroundMaterialColor[900], + scaffoldBackgroundColor: backgroundMaterialColor[900], + dialogBackgroundColor: backgroundMaterialColor[800], + shadowColor: Colors.black26, + popupMenuTheme: PopupMenuThemeData(color: backgroundMaterialColor[800]), + buttonTheme: ButtonThemeData( + buttonColor: accentMaterialColor, + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: accentMaterialColor[400]!, + width: 2.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: backgroundMaterialColor[800]!, + ), + ), + ), + navigationRailTheme: NavigationRailThemeData( + backgroundColor: backgroundMaterialColor[800], + unselectedIconTheme: IconThemeData(color: Colors.grey[300], opacity: 1), + selectedIconTheme: IconThemeData(color: backgroundMaterialColor[850]), + selectedLabelTextStyle: TextStyle(color: accentMaterialColor[300]), + unselectedLabelTextStyle: TextStyle(color: Colors.grey[300]), + indicatorColor: accentMaterialColor[300], + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: backgroundMaterialColor[800], + height: 55, + indicatorColor: accentMaterialColor[300], + ), + cardTheme: CardTheme( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + color: backgroundMaterialColor[900], + elevation: 20, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + onPrimary: accentMaterialColor[300], + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + cardColor: backgroundMaterialColor[800], + canvasColor: backgroundMaterialColor[900], + ); +} diff --git a/lib/themes/light-theme.dart b/lib/themes/light-theme.dart new file mode 100644 index 00000000..540def04 --- /dev/null +++ b/lib/themes/light-theme.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +final materialWhite = MaterialColor(Colors.white.value, { + 50: Colors.white, + 100: Colors.blueGrey[50]!, + 200: Colors.white, + 300: Colors.white, + 400: Colors.white, + 500: Colors.blueGrey, + 600: Colors.white, + 700: Colors.white, + 800: Colors.white, + 900: Colors.white, +}); + +ThemeData lightTheme({ + required MaterialColor accentMaterialColor, + required MaterialColor backgroundMaterialColor, +}) { + return ThemeData( + useMaterial3: true, + primaryColor: accentMaterialColor, + primarySwatch: accentMaterialColor, + buttonTheme: ButtonThemeData( + buttonColor: accentMaterialColor, + ), + shadowColor: Colors.grey[300], + backgroundColor: backgroundMaterialColor[50], + textTheme: TextTheme( + bodyText1: TextStyle(color: Colors.grey[850]), + headline1: TextStyle(color: Colors.grey[850]), + headline2: TextStyle(color: Colors.grey[850]), + headline3: TextStyle(color: Colors.grey[850]), + headline4: TextStyle(color: Colors.grey[850]), + headline5: TextStyle(color: Colors.grey[850]), + headline6: TextStyle(color: Colors.grey[850]), + ), + listTileTheme: ListTileThemeData( + iconColor: Colors.grey[850], + horizontalTitleGap: 0, + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: accentMaterialColor[400]!, + width: 2.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey[800]!, + ), + ), + ), + navigationRailTheme: NavigationRailThemeData( + backgroundColor: backgroundMaterialColor[100], + indicatorColor: accentMaterialColor[300], + selectedIconTheme: IconThemeData(color: accentMaterialColor[850]), + unselectedIconTheme: IconThemeData(color: Colors.grey[850], opacity: 1), + unselectedLabelTextStyle: TextStyle( + color: Colors.grey[850], + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: backgroundMaterialColor[100], + height: 55, + indicatorColor: accentMaterialColor[300], + iconTheme: MaterialStateProperty.all( + IconThemeData(color: Colors.grey[850]), + ), + ), + cardTheme: CardTheme( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + color: backgroundMaterialColor[50], + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + onPrimary: accentMaterialColor[800], + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + cardColor: backgroundMaterialColor[50], + canvasColor: backgroundMaterialColor[50], + ); +} diff --git a/lib/utils/PersistedChangeNotifier.dart b/lib/utils/PersistedChangeNotifier.dart new file mode 100644 index 00000000..556984c7 --- /dev/null +++ b/lib/utils/PersistedChangeNotifier.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class PersistedChangeNotifier extends ChangeNotifier { + late SharedPreferences _localStorage; + PersistedChangeNotifier() { + SharedPreferences.getInstance().then((value) => _localStorage = value).then( + (_) async { + final persistedMap = (await toMap()) + .entries + .toList() + .fold>({}, (acc, entry) { + if (entry.value != null) { + if (entry.value is bool) { + acc[entry.key] = _localStorage.getBool(entry.key); + } else if (entry.value is int) { + acc[entry.key] = _localStorage.getInt(entry.key); + } else if (entry.value is double) { + acc[entry.key] = _localStorage.getDouble(entry.key); + } else if (entry.value is String) { + acc[entry.key] = _localStorage.getString(entry.key); + } + } else { + acc[entry.key] = _localStorage.get(entry.key); + } + return acc; + }); + await loadFromLocal(persistedMap); + notifyListeners(); + }, + ); + } + + FutureOr loadFromLocal(Map map); + + FutureOr> toMap(); + + Future updatePersistence() async { + for (final entry in (await toMap()).entries) { + if (entry.value is bool) { + await _localStorage.setBool(entry.key, entry.value); + } else if (entry.value is int) { + await _localStorage.setInt(entry.key, entry.value); + } else if (entry.value is double) { + await _localStorage.setDouble(entry.key, entry.value); + } else if (entry.value is String) { + await _localStorage.setString(entry.key, entry.value); + } + } + } +} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 9842e7d1..b49b6360 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audio_service (0.14.1): + - FlutterMacOS - audio_session (0.0.1): - FlutterMacOS - bitsdojo_window_macos (0.0.1): @@ -13,6 +15,8 @@ PODS: - HotKey - just_audio (0.0.1): - FlutterMacOS + - package_info_plus_macos (0.0.1): + - FlutterMacOS - path_provider_macos (0.0.1): - FlutterMacOS - shared_preferences_macos (0.0.1): @@ -24,11 +28,13 @@ PODS: - FlutterMacOS DEPENDENCIES: + - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - hotkey_manager (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) + - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) @@ -40,6 +46,8 @@ SPEC REPOS: - HotKey EXTERNAL SOURCES: + audio_service: + :path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos bitsdojo_window_macos: @@ -50,6 +58,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos just_audio: :path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos + package_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos path_provider_macos: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos shared_preferences_macos: @@ -60,17 +70,19 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 - bitsdojo_window_macos: 7e9b1bbb09bdce418d9657ead7fc9d824203ff0d + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a HotKey: ad59450195936c10992438c4210f673de5aee43e hotkey_manager: ad673457691f4d39e481be04a61da2ae07d81c62 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 + package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f - shared_preferences_macos: 480ce071d0666e37cef23fe6c702293a3d21799e + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4 + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 PODFILE CHECKSUM: f7c7be88e75cc0b6c98b7564b0771f5dff5c5490 diff --git a/pubspec.lock b/pubspec.lock index 2a37cf19..b6f97602 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,14 +14,14 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.3.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" async: dependency: transitive description: @@ -63,35 +63,35 @@ packages: name: bitsdojo_window url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+1" + version: "0.1.2" bitsdojo_window_linux: dependency: transitive description: name: bitsdojo_window_linux url: "https://pub.dartlang.org" source: hosted - version: "0.1.1" + version: "0.1.2" bitsdojo_window_macos: dependency: transitive description: name: bitsdojo_window_macos url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.2" bitsdojo_window_platform_interface: dependency: transitive description: name: bitsdojo_window_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.2" bitsdojo_window_windows: dependency: transitive description: name: bitsdojo_window_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.2" boolean_selector: dependency: transitive description: @@ -105,7 +105,7 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" csslib: dependency: transitive description: @@ -189,7 +189,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.1" file: dependency: transitive description: @@ -208,7 +208,7 @@ packages: name: flutter_blurhash url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.7.0" flutter_cache_manager: dependency: transitive description: @@ -222,7 +222,7 @@ packages: name: flutter_hooks url: "https://pub.dartlang.org" source: hosted - version: "0.18.2+1" + version: "0.18.4" flutter_launcher_icons: dependency: "direct dev" description: @@ -243,7 +243,7 @@ packages: name: flutter_riverpod url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" flutter_test: dependency: "direct dev" description: flutter @@ -267,21 +267,21 @@ packages: name: go_router url: "https://pub.dartlang.org" source: hosted - version: "3.0.4" + version: "3.1.0" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" hotkey_manager: dependency: "direct main" description: name: hotkey_manager url: "https://pub.dartlang.org" source: hosted - version: "0.1.6" + version: "0.1.7" html: dependency: "direct main" description: @@ -302,7 +302,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" image: dependency: transitive description: @@ -330,14 +330,14 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.5.0" just_audio: dependency: "direct main" description: name: just_audio url: "https://pub.dartlang.org" source: hosted - version: "0.9.20" + version: "0.9.21" just_audio_background: dependency: "direct main" description: @@ -400,7 +400,7 @@ packages: name: marquee url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.2" matcher: dependency: transitive description: @@ -442,7 +442,7 @@ packages: name: octo_image url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" package_config: dependency: transitive description: @@ -512,49 +512,49 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" pedantic: dependency: transitive description: @@ -582,7 +582,7 @@ packages: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.3" + version: "9.0.4" permission_handler_platform_interface: dependency: transitive description: @@ -603,7 +603,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -652,35 +652,35 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -694,14 +694,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -713,7 +713,7 @@ packages: name: sliver_tools url: "https://pub.dartlang.org" source: hosted - version: "0.2.5" + version: "0.2.6" source_span: dependency: transitive description: @@ -726,7 +726,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: e36eb5884de44d39b310d0878779a697048061bd + resolved-ref: ea313e2d21c38157cd8255d248bcd7897bf51360 url: "https://github.com/KRTirtho/spotify-dart.git" source: git version: "0.7.0" @@ -736,14 +736,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.2+1" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1+1" stack_trace: dependency: transitive description: @@ -778,7 +778,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -799,42 +799,42 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.20" + version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: @@ -848,14 +848,14 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" uuid: dependency: transitive description: @@ -876,7 +876,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.6.1" xdg_directories: dependency: transitive description: @@ -890,21 +890,21 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "5.4.1" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" youtube_explode_dart: dependency: "direct main" description: name: youtube_explode_dart url: "https://pub.dartlang.org" source: hosted - version: "1.10.9+1" + version: "1.11.0" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 096e6b26..c7e48629 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: url_launcher: ^6.0.17 youtube_explode_dart: ^1.10.8 infinite_scroll_pagination: ^3.1.0 - bitsdojo_window: ^0.1.1+1 + bitsdojo_window: ^0.1.2 hotkey_manager: ^0.1.6 just_audio: ^0.9.18 just_audio_libwinmedia: ^0.0.4