Merge branch 'master' into build

This commit is contained in:
Kingkor Roy Tirtho 2022-05-27 17:59:30 +06:00
commit 76d8ec1dc9
35 changed files with 885 additions and 481 deletions

View File

@ -48,13 +48,14 @@ jobs:
sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt sed -i "s/%{{WIN_SHA256}}%/$HASH/" choco-struct/tools/VERIFICATION.txt
make choco make choco
- run: mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg
# Publish to Chocolatey Repository # Publish to Chocolatey Repository
- run: | - run: |
choco apikey -k ${{ secrets.CHOCO_API_KEY }} -s https://push.chocolatey.org/ 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' echo 'published to community.chocolatey.org'
# choco push dist/${{ steps.tag.outputs.tag }}/Spotube-windows-x86_64.nupkg
# Upload artifacts # Upload artifacts
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
@ -191,6 +192,10 @@ jobs:
with: with:
name: Spotube-Linux-Bundle name: Spotube-Linux-Bundle
path: ./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 - name: Get latest tag
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
@ -200,14 +205,14 @@ jobs:
# generating checksums for all the binary # generating checksums for all the binary
- run: | - run: |
tree . tree .
md5sum ./Spotube-Windows-Bundle/*.{exe,nupkg} > RELEASE.md5sum md5sum ./Spotube-Windows-Bundle/*.{exe,nupkg} >> RELEASE.md5sum
md5sum ./Spotube-Macos-Bundle/*.dmg > RELEASE.md5sum md5sum ./Spotube-Macos-Bundle/*.dmg >> RELEASE.md5sum
md5sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} > RELEASE.md5sum md5sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} >> RELEASE.md5sum
md5sum ./Spotube-Android-Bundle/*.apk > RELEASE.md5sum md5sum ./Spotube-Android-Bundle/*.apk >> RELEASE.md5sum
sha256sum ./Spotube-Macos-Bundle/*.dmg > RELEASE.sha256sum sha256sum ./Spotube-Macos-Bundle/*.dmg >> RELEASE.sha256sum
sha256sum ./Spotube-Windows-Bundle/*.{exe,nupkg} > RELEASE.sha256sum sha256sum ./Spotube-Windows-Bundle/*.{exe,nupkg} >> RELEASE.sha256sum
sha256sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} > RELEASE.sha256sum sha256sum ./Spotube-Linux-Bundle/*.{AppImage,tar.xz,deb} >> RELEASE.sha256sum
sha256sum ./Spotube-Android-Bundle/*.apk > RELEASE.sha256sum sha256sum ./Spotube-Android-Bundle/*.apk >> RELEASE.sha256sum
sed -i 's|Spotube-.*-Bundle/||' RELEASE.sha256sum RELEASE.md5sum sed -i 's|Spotube-.*-Bundle/||' RELEASE.sha256sum RELEASE.md5sum
# Upload release binary # Upload release binary
- uses: ncipollo/release-action@v1 - uses: ncipollo/release-action@v1
@ -252,11 +257,11 @@ jobs:
strip_v: true strip_v: true
- run: | - run: |
python3 spotube/scripts/update_flathub_version.py ${{ steps.tag.outputs.tag }} python3 spotube/scripts/update_flathub_version.py ${{ steps.tag.outputs.tag }}
rm -rf spotube
- uses: EndBug/add-and-commit@v9 - uses: EndBug/add-and-commit@v9
with: with:
message: v${{ steps.tag.outputs.tag }} Update message: v${{ steps.tag.outputs.tag }} Update
# push: origin master push: origin master
push: false
publish_aur: publish_aur:
needs: update_release needs: update_release
@ -276,11 +281,11 @@ jobs:
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" aur-struct/PKGBUILD sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" aur-struct/PKGBUILD
sed -i "s/%{{PKGREL}}%/1/" 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 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 - uses: KSXGitHub/github-actions-deploy-aur@v2.2.5
# with: with:
# pkgname: spotube-bin pkgname: spotube-bin
# pkgbuild: aur-struct/PKGBUILD pkgbuild: aur-struct/PKGBUILD
# commit_username: ${{ secrets.AUR_USERNAME }} commit_username: ${{ secrets.AUR_USERNAME }}
# commit_email: ${{ secrets.AUR_EMAIL }} commit_email: ${{ secrets.AUR_EMAIL }}
# ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
# commit_message: Updated to v${{ steps.tag.outputs.tag }} commit_message: Updated to v${{ steps.tag.outputs.tag }}

View File

@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project. # This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc. # 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: version:
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
channel: stable channel: stable
project_type: app 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'

View File

@ -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", "compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64\\cl.exe",
"cStandard": "c17", "cStandard": "c17",
"cppStandard": "c++17", "cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x64" "intelliSenseMode": "windows-msvc-x64",
"configurationProvider": "ms-vscode.makefile-tools"
} }
], ],
"version": 4 "version": 4

View File

@ -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 # v2.0.0
### New ### New

View File

@ -47,7 +47,7 @@ inno:
powershell .\build\iscc\iscc.exe scripts\windows-setup-creator.iss powershell .\build\iscc\iscc.exe scripts\windows-setup-creator.iss
choco: 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 powershell choco pack .\choco-struct\spotube.nuspec --outputdirectory dist
apk: apk:

122
README.md
View File

@ -41,7 +41,7 @@ Following are the features that currently spotube offers:
- Playback control is on user's machine instead of server based - Playback control is on user's machine instead of server based
- Small size & less data hungry - 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) - 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 - Downloadable track
<a href="https://www.producthunt.com/posts/spotube?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-spotube" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=327965&theme=dark" alt="Spotube - A lightweight+free Spotify crossplatform-client made with flutter | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/spotube?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-spotube" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=327965&theme=dark" alt="Spotube - A lightweight+free Spotify crossplatform-client made with flutter | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@ -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 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 | [<img width='240' alt='Android Download' src='https://www.remcsteuben.com/sites/default/files/images/apkdaddy%20download.png'/>][android-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` |
| 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` |
| AppImage | [<img width='240' alt='AppImage Download' src='https://user-images.githubusercontent.com/61944859/169455015-13385466-8901-48fe-ba90-b62d58b0be64.png'/>][appimage-dlink]<br/> **Note**: AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed |
| Linux (tarball) | [<img width='240' alt='Tarball Download' src='https://user-images.githubusercontent.com/61944859/169456985-e0ba1fd4-10e8-4cc0-ab94-337acc6e0295.png'/>][linux-dlink] |
| Windows | [<img width='240' alt='Windows Download' src='https://get.todoist.help/hc/article_attachments/4403191721234/WindowsButton.svg'/>][win32-dlink] |
| Windows (Chocolatey) | `choco install spotube` |
| Windows (WinGet) | `winget install --id KRTirtho.Spotube` |
| MacOS | [<img width='240' alt='MacOS Download' src='https://reachify.io/wp-content/uploads/2018/09/mac-download-button-1.png'/>][mac-dlink] |
## Android > **Note!:** If you don't understand this download table. You can read [installation instructions][wiki-installation-instructions] from the wiki
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
```
<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>
### 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
## Nightly Builds ## Nightly Builds
Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/spotube/workflows/flutter-build/build) Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/spotube/workflows/flutter-build/build)
## Optional Configurations ## Optional Configurations
<details> ### Login with <b>Spotify</b>
<summary>Login with <b>Spotify</b></summary>
You need a spotify account & a developer app for You need a spotify account & a developer app for
- clientId - clientId
@ -123,31 +78,28 @@ Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/sp
**Grab credentials:** **Grab credentials:**
- Go to https://developer.spotify.com/dashboard/login & login with your spotify account (Skip if you're logged in) - 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) <img width='480' alt='Step 1' src='https://user-images.githubusercontent.com/61944859/111762106-d1d37680-88ca-11eb-9884-ec7a40c0dd27.png'/>
- Create an web app for Spotify Public API - Create an web app for Spotify Public API<br/>
![step 2](https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png) <img width='480' alt='step 2' src='https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png'/>
- 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 - **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<br/>
![setp-3](https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png) <img width='720' alt='setp-3' src='https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png'/>
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields - Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields<br/>
![step-4](https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png) <img width='480' alt='step-4' src='https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png'/>
</details>
<details> ### Setup <b>Genius Lyrics</b>
<summary>Setup <b>Genius Lyrics</b></summary>
- Signup/Login into [genius](https://genius.com/signup) for **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 - Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client<br/>
![Step 2](https://user-images.githubusercontent.com/61944859/158823216-b4942731-c4c5-46c8-8b60-82a372b51cc5.png) <img width='480' alt='Step 2' src='https://user-images.githubusercontent.com/61944859/158823216-b4942731-c4c5-46c8-8b60-82a372b51cc5.png' />
- Generate & copy access token - Generate & copy access token<br/>
![Step 3](https://user-images.githubusercontent.com/61944859/158822817-f04da060-3094-4a3b-8ace-a936d0cda8db.png) <img width='480' alt='Step 3' src='https://user-images.githubusercontent.com/61944859/158822817-f04da060-3094-4a3b-8ace-a936d0cda8db.png' />
- Paste the copied access token in Spotube's Settings - Paste the copied access token in Spotube's Settings<br/>
![Step 4](https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png) <img width='480' alt='Step 4' src='https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png' />
> **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself > **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
</details>
# TODO: # 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) 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 - [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 - [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 - [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. - [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. - [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 # Social handlers
@ -205,3 +160,14 @@ Follow me on [Twitter](https://twitter.com/@krtirtho) for newer updates about th
<p align="center">&copy; 2022 Spotube</p> <p align="center">&copy; 2022 Spotube</p>
<!-- Variables/Text References -->
[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

View File

@ -3,6 +3,139 @@ import 'dart:io';
import 'package:path/path.dart' as path; 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<String> args) async { void main(List<String> args) async {
List<String> val; List<String> val;
List<Map> val2; List<Map> val2;
@ -10,7 +143,7 @@ void main(List<String> args) async {
final cwd = Directory.current.path; final cwd = Directory.current.path;
final binSafe = cwd.endsWith("/bin") ? ".." : ""; final binSafe = cwd.endsWith("/bin") ? ".." : "";
if (args.isEmpty) { 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")) { if (args.contains("--local")) {
final secretFilePath = path.join(cwd, binSafe, "secrets.json"); final secretFilePath = path.join(cwd, binSafe, "secrets.json");
@ -19,11 +152,22 @@ void main(List<String> args) async {
final data = jsonDecode(await file.readAsString()); final data = jsonDecode(await file.readAsString());
val = List.castFrom<dynamic, String>(data["LYRICS_SECRET"]); val = List.castFrom<dynamic, String>(data["LYRICS_SECRET"]);
val2 = List.castFrom<dynamic, Map>(data["SPOTIFY_SECRET"]); val2 = List.castFrom<dynamic, Map>(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<dynamic, String>(jsonDecode(decodedLyricSecret));
val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecret));
} else { } else {
final decodedLyricSecret = utf8.decode(base64Decode(args.first)); final decodedLyricSecret = utf8.decode(base64Decode(args.first));
final decodedSpotifySecrete = utf8.decode(base64Decode(args.last)); final decodedSpotifySecret = utf8.decode(base64Decode(args.last));
val = List.castFrom<dynamic, String>(jsonDecode(decodedLyricSecret)); val = List.castFrom<dynamic, String>(jsonDecode(decodedLyricSecret));
val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecrete)); val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecret));
} }
await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart")) await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart"))

View File

@ -6,7 +6,6 @@ import 'package:flutter/services.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:infinite_scroll_pagination/infinite_scroll_pagination.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:spotify/spotify.dart' hide Image, Player, Search;
import 'package:spotube/components/Category/CategoryCard.dart'; 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/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/helpers/get-random-element.dart';
import 'package:spotube/helpers/oauth-login.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/useHotKeys.dart';
import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/usePagingController.dart';
import 'package:spotube/hooks/useSharedPreferences.dart'; import 'package:spotube/hooks/useSharedPreferences.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.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/SpotifyDI.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -48,7 +42,6 @@ class Home extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Auth auth = ref.watch(authProvider);
String recommendationMarket = ref.watch(userPreferencesProvider.select( String recommendationMarket = ref.watch(userPreferencesProvider.select(
(value) => (value.recommendationMarket), (value) => (value.recommendationMarket),
)); ));
@ -98,70 +91,12 @@ class Home extends HookConsumerWidget {
}, [recommendationMarket]); }, [recommendationMarket]);
useEffect(() { 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 { 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); pagingController.addPageRequestListener(listener);
// the world is full of surprises and the previously working // the world is full of surprises and the previously working
// fine pageRequestListener now doesn't notify the listeners // fine pageRequestListener now doesn't notify the listeners
// automatically after assigning a listener. So doing it manually // automatically after assigning a listener. So doing it manually
pagingController.notifyPageRequestListeners(0); 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);
}
} catch (e, stack) { } catch (e, stack) {
logger.e("initState", e, stack); logger.e("initState", e, stack);
} }

View File

@ -81,7 +81,7 @@ class Sidebar extends HookConsumerWidget {
trailing: FutureBuilder<User>( trailing: FutureBuilder<User>(
future: spotify.me.get(), future: spotify.me.get(),
builder: (context, snapshot) { builder: (context, snapshot) {
var avatarImg = imageToUrlString(snapshot.data?.images, final avatarImg = imageToUrlString(snapshot.data?.images,
index: (snapshot.data?.images?.length ?? 1) - 1); index: (snapshot.data?.images?.length ?? 1) - 1);
return extended.value return extended.value
? Padding( ? Padding(

View File

@ -21,7 +21,7 @@ class _UserArtistsState extends ConsumerState<UserArtists> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timestamp) { WidgetsBinding.instance.addPostFrameCallback((timestamp) {
_pagingController.addPageRequestListener((pageKey) async { _pagingController.addPageRequestListener((pageKey) async {
try { try {
SpotifyApi spotifyApi = ref.read(spotifyProvider); SpotifyApi spotifyApi = ref.read(spotifyProvider);

View File

@ -92,7 +92,7 @@ class Player extends HookConsumerWidget {
// I can't believe useEffect doesn't run Post Frame aka // I can't believe useEffect doesn't run Post Frame aka
// after rendering/painting the UI // after rendering/painting the UI
// `My disappointment is immeasurable and my day is ruined` XD // `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 // clearing the overlay-entry as passing the already available
// 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) &&

View File

@ -1,5 +1,4 @@
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:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:spotube/helpers/zero-pad-num-str.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 Playback playback = ref.watch(playbackProvider);
final AudioPlayer player = playback.player; final AudioPlayer player = playback.player;
final _shuffled = useState(false);
final onNext = useNextTrack(playback); final onNext = useNextTrack(playback);
final onPrevious = usePreviousTrack(playback); final onPrevious = usePreviousTrack(playback);
@ -92,7 +89,7 @@ class PlayerControls extends HookConsumerWidget {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.shuffle_rounded), icon: const Icon(Icons.shuffle_rounded),
color: _shuffled.value color: playback.shuffled
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: iconColor, : iconColor,
onPressed: () { onPressed: () {
@ -101,12 +98,10 @@ class PlayerControls extends HookConsumerWidget {
return; return;
} }
try { try {
if (!_shuffled.value) { if (!playback.shuffled) {
playback.currentPlaylist!.shuffle(); playback.shuffle();
_shuffled.value = true;
} else { } else {
playback.currentPlaylist!.unshuffle(); playback.unshuffle();
_shuffled.value = false;
} }
} catch (e, stack) { } catch (e, stack) {
logger.e("onShuffle", e, stack); logger.e("onShuffle", e, stack);
@ -140,7 +135,6 @@ class PlayerControls extends HookConsumerWidget {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
_shuffled.value = false;
playback.reset(); playback.reset();
} catch (e, stack) { } catch (e, stack) {
logger.e("onStop", e, stack); logger.e("onStop", e, stack);

View File

@ -29,7 +29,7 @@ class PlayerView extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (breakpoint.isMoreThan(Breakpoints.md)) { if (breakpoint.isMoreThan(Breakpoints.md)) {
WidgetsBinding.instance?.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
GoRouter.of(context).pop(); GoRouter.of(context).pop();
}); });
} }

View File

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

View File

@ -5,6 +5,7 @@ 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:shared_preferences/shared_preferences.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/Settings/SettingsHotkeyTile.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -39,6 +40,16 @@ class Settings extends HookConsumerWidget {
packageName: 'spotube', packageName: 'spotube',
); );
final pickColorScheme = useCallback((ColorSchemeType schemeType) {
return () => showDialog(
context: context,
builder: (context) {
return ColorSchemePickerDialog(
schemeType: schemeType,
);
});
}, []);
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
@ -159,6 +170,27 @@ class Settings extends HookConsumerWidget {
], ],
), ),
const SizedBox(height: 10), 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -13,7 +13,7 @@ class RecordHotKeyDialog extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var _hotKey = useState(HotKey(null)); final _hotKey = useState<HotKey?>(null);
return AlertDialog( return AlertDialog(
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody( child: ListBody(
@ -72,10 +72,10 @@ class RecordHotKeyDialog extends HookWidget {
), ),
TextButton( TextButton(
child: const Text('OK'), child: const Text('OK'),
onPressed: !_hotKey.value.isSetted onPressed: _hotKey.value == null
? null ? null
: () { : () {
onHotKeyRecorded(_hotKey.value); onHotKeyRecorded(_hotKey.value!);
GoRouter.of(context).pop(); GoRouter.of(context).pop();
}, },
), ),

View File

@ -26,7 +26,7 @@ Future<void> oauthLogin(Auth auth,
if (responseUri != null) { if (responseUri != null) {
final SpotifyApi spotify = final SpotifyApi spotify =
SpotifyApi.fromAuthCodeGrant(grant, responseUri); SpotifyApi.fromAuthCodeGrant(grant, responseUri);
var credentials = await spotify.getCredentials(); final credentials = await spotify.getCredentials();
if (credentials.accessToken != null) { if (credentials.accessToken != null) {
accessToken = credentials.accessToken; accessToken = credentials.accessToken;
await localStorage.setString( await localStorage.setString(
@ -56,7 +56,6 @@ Future<void> oauthLogin(Auth auth,
accessToken: accessToken, accessToken: accessToken,
refreshToken: refreshToken, refreshToken: refreshToken,
expiration: expiration, expiration: expiration,
isLoggedIn: true,
); );
} catch (e, stack) { } catch (e, stack) {
logger.e("oauthLogin", e, stack); logger.e("oauthLogin", e, stack);

View File

@ -30,7 +30,7 @@ Future<SpotubeTrack> toSpotubeTrack(
.replaceAll("\$FEATURED_ARTISTS", featuredArtists); .replaceAll("\$FEATURED_ARTISTS", featuredArtists);
logger.v("[Youtube Search Term] $queryString"); logger.v("[Youtube Search Term] $queryString");
SearchList videos = await youtube.search.getVideos(queryString); VideoSearchList videos = await youtube.search.search(queryString);
List<Map> ratedRankedVideos = videos List<Map> ratedRankedVideos = videos
.map((video) { .map((video) {

View File

@ -8,7 +8,7 @@ final logger = getLogger("ServerIPC");
Future<String?> connectIpc(String authUri, String redirectUri) async { Future<String?> connectIpc(String authUri, String redirectUri) async {
try { try {
logger.i("[Launching]: $authUri"); logger.i("[Launching]: $authUri");
await launch(authUri); await launchUrl(Uri.parse(authUri));
HttpServer server = HttpServer server =
await HttpServer.bind(InternetAddress.loopbackIPv4, 4304); await HttpServer.bind(InternetAddress.loopbackIPv4, 4304);
@ -21,7 +21,7 @@ Future<String?> connectIpc(String authUri, String redirectUri) async {
if (code != null) { if (code != null) {
request.response request.response
..statusCode = HttpStatus.ok ..statusCode = HttpStatus.ok
..write("Authentication successful") ..write("Authentication successful. Now Go back to Spotube")
..close(); ..close();
return "$redirectUri?code=$code"; return "$redirectUri?code=$code";
} else { } else {

View File

@ -6,7 +6,7 @@ bool? useIsCurrentRoute([String matcher = "/"]) {
final isCurrentRoute = useState<bool?>(null); final isCurrentRoute = useState<bool?>(null);
final context = useContext(); final context = useContext();
useEffect(() { useEffect(() {
WidgetsBinding.instance?.addPostFrameCallback((timer) { WidgetsBinding.instance.addPostFrameCallback((timer) {
final isCurrent = GoRouter.of(context).location == matcher; final isCurrent = GoRouter.of(context).location == matcher;
if (isCurrent != isCurrentRoute.value) { if (isCurrent != isCurrentRoute.value) {
isCurrentRoute.value = isCurrent; isCurrentRoute.value = isCurrent;

View File

@ -9,7 +9,7 @@ PaletteColor usePaletteColor(BuildContext context, String imageUrl) {
final mounted = useIsMounted(); final mounted = useIsMounted();
useEffect(() { useEffect(() {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final palette = await PaletteGenerator.fromImageProvider( final palette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider( CachedNetworkImageProvider(
imageUrl, imageUrl,

View File

@ -12,6 +12,8 @@ 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:just_audio_background/just_audio_background.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 { void main() async {
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
@ -45,6 +47,10 @@ class MyApp extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final themeMode = final themeMode =
ref.watch(userPreferencesProvider.select((s) => s.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 player = ref.watch(audioPlayerProvider);
final youtube = ref.watch(youtubeProvider); final youtube = ref.watch(youtubeProvider);
useEffect(() { useEffect(() {
@ -59,98 +65,13 @@ class MyApp extends HookConsumerWidget {
routerDelegate: _router.routerDelegate, routerDelegate: _router.routerDelegate,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Spotube', title: 'Spotube',
theme: ThemeData( theme: lightTheme(
primaryColor: Colors.green, accentMaterialColor: accentMaterialColor,
primarySwatch: Colors.green, backgroundMaterialColor: backgroundMaterialColor,
buttonTheme: const ButtonThemeData(
buttonColor: Colors.green,
), ),
shadowColor: Colors.grey[300], darkTheme: darkTheme(
backgroundColor: Colors.white, accentMaterialColor: accentMaterialColor,
textTheme: TextTheme( backgroundMaterialColor: backgroundMaterialColor,
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,
),
),
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],
), ),
themeMode: themeMode, themeMode: themeMode,
); );

View File

@ -1,14 +1,13 @@
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Home/Home.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/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.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'; import 'package:spotube/components/Shared/SpotubePageRoute.dart';
GoRouter createGoRouter() => GoRouter( GoRouter createGoRouter() => GoRouter(

View File

@ -3,10 +3,10 @@ abstract class LocalStorageKeys {
static String recommendationMarket = 'recommendation_market'; static String recommendationMarket = 'recommendation_market';
static String ytSearchFormate = 'youtube_search_format'; static String ytSearchFormate = 'youtube_search_format';
static String clientId = 'client_id'; static String clientId = 'clientId';
static String clientSecret = 'client_secret'; static String clientSecret = 'clientSecret';
static String accessToken = 'access_token'; static String accessToken = 'accessToken';
static String refreshToken = 'refresh_token'; static String refreshToken = 'refreshToken';
static String expiration = "expiration"; static String expiration = "expiration";
static String geniusAccessToken = "genius_access_token"; static String geniusAccessToken = "genius_access_token";

View File

@ -1,26 +1,32 @@
import 'package:flutter/cupertino.dart'; import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Auth with ChangeNotifier { import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/utils/PersistedChangeNotifier.dart';
class Auth extends PersistedChangeNotifier {
String? _clientId; String? _clientId;
String? _clientSecret; String? _clientSecret;
String? _accessToken; String? _accessToken;
String? _refreshToken; String? _refreshToken;
DateTime? _expiration; DateTime? _expiration;
bool _isLoggedIn = false; Auth() : super();
String? get clientId => _clientId; String? get clientId => _clientId;
String? get clientSecret => _clientSecret; String? get clientSecret => _clientSecret;
String? get accessToken => _accessToken; String? get accessToken => _accessToken;
String? get refreshToken => _refreshToken; String? get refreshToken => _refreshToken;
DateTime? get expiration => _expiration; DateTime? get expiration => _expiration;
bool get isLoggedIn => _isLoggedIn;
bool get isAnonymous => bool get isAnonymous =>
!_isLoggedIn && _clientId == null && _clientSecret == null; _clientId == null &&
_clientSecret == null &&
accessToken == null &&
refreshToken == null;
bool get isLoggedIn => !isAnonymous && _expiration != null;
void setAuthState({ void setAuthState({
bool? isLoggedIn,
bool safe = true, bool safe = true,
String? clientId, String? clientId,
String? clientSecret, String? clientSecret,
@ -31,7 +37,6 @@ class Auth with ChangeNotifier {
if (safe) { if (safe) {
if (clientId != null) _clientId = clientId; if (clientId != null) _clientId = clientId;
if (clientSecret != null) _clientSecret = clientSecret; if (clientSecret != null) _clientSecret = clientSecret;
if (isLoggedIn != null) _isLoggedIn = isLoggedIn;
if (refreshToken != null) _refreshToken = refreshToken; if (refreshToken != null) _refreshToken = refreshToken;
if (accessToken != null) _accessToken = accessToken; if (accessToken != null) _accessToken = accessToken;
if (expiration != null) _expiration = expiration; if (expiration != null) _expiration = expiration;
@ -43,6 +48,7 @@ class Auth with ChangeNotifier {
_expiration = expiration; _expiration = expiration;
} }
notifyListeners(); notifyListeners();
updatePersistence();
} }
logout() { logout() {
@ -51,14 +57,34 @@ class Auth with ChangeNotifier {
_accessToken = null; _accessToken = null;
_refreshToken = null; _refreshToken = null;
_expiration = null; _expiration = null;
_isLoggedIn = false;
notifyListeners(); notifyListeners();
updatePersistence();
} }
@override @override
String toString() { String toString() {
return "Auth(clientId: $clientId, clientSecret: $clientSecret, accessToken: $accessToken, refreshToken: $refreshToken, expiration: $expiration, isLoggedIn: $isLoggedIn)"; return "Auth(clientId: $clientId, clientSecret: $clientSecret, accessToken: $accessToken, refreshToken: $refreshToken, expiration: $expiration, isLoggedIn: $isLoggedIn)";
} }
@override
FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
_clientId = map["clientId"];
_clientSecret = map["clientSecret"];
_accessToken = map["accessToken"];
_refreshToken = map["refreshToken"];
_expiration = DateTime.tryParse(map["expiration"]);
} }
var authProvider = ChangeNotifierProvider<Auth>((ref) => Auth()); @override
FutureOr<Map<String, dynamic>> toMap() {
return {
"clientId": _clientId,
"clientSecret": _clientSecret,
"accessToken": _accessToken,
"refreshToken": _refreshToken,
"expiration": _expiration.toString(),
};
}
}
final authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());

View File

@ -31,20 +31,24 @@ class CurrentPlaylist {
List<String> get trackIds => tracks.map((e) => e.id!).toList(); List<String> get trackIds => tracks.map((e) => e.id!).toList();
void shuffle() { bool shuffle() {
// won't shuffle if already shuffled // won't shuffle if already shuffled
if (_tempTrack == null) { if (_tempTrack == null) {
_tempTrack = [...tracks]; _tempTrack = [...tracks];
tracks.shuffle(); tracks.shuffle();
return true;
} }
return false;
} }
void unshuffle() { bool unshuffle() {
// without _tempTracks unshuffling can't be done // without _tempTracks unshuffling can't be done
if (_tempTrack != null) { if (_tempTrack != null) {
tracks = [..._tempTrack!]; tracks = [..._tempTrack!];
_tempTrack = null; _tempTrack = null;
return true;
} }
return false;
} }
} }
@ -66,6 +70,7 @@ class Playback extends ChangeNotifier {
StreamSubscription<Duration>? _positionStreamListener; StreamSubscription<Duration>? _positionStreamListener;
Duration _prevPosition = Duration.zero; Duration _prevPosition = Duration.zero;
bool _shuffled = false;
AudioPlayer player; AudioPlayer player;
YoutubeExplode youtube; YoutubeExplode youtube;
@ -138,6 +143,7 @@ class Playback extends ChangeNotifier {
}); });
} }
bool get shuffled => _shuffled;
CurrentPlaylist? get currentPlaylist => _currentPlaylist; CurrentPlaylist? get currentPlaylist => _currentPlaylist;
Track? get currentTrack => _currentTrack; Track? get currentTrack => _currentTrack;
bool get isPlaying => _isPlaying; bool get isPlaying => _isPlaying;
@ -158,6 +164,7 @@ class Playback extends ChangeNotifier {
void reset() { void reset() {
_logger.v("Playback Reset"); _logger.v("Playback Reset");
_isPlaying = false; _isPlaying = false;
_shuffled = false;
duration = null; duration = null;
_currentPlaylist = null; _currentPlaylist = null;
_currentTrack = null; _currentTrack = null;
@ -265,6 +272,20 @@ class Playback extends ChangeNotifier {
_logger.e("startPlaying", e, stack); _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<Playback>((ref) { final playbackProvider = ChangeNotifierProvider<Playback>((ref) {

View File

@ -1,13 +1,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/helpers/get-random-element.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/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
var spotifyProvider = Provider<SpotifyApi>((ref) { final spotifyProvider = Provider<SpotifyApi>((ref) {
Auth authState = ref.watch(authProvider); Auth authState = ref.watch(authProvider);
final anonCred = getRandomElement(spotifySecrets); final anonCred = getRandomElement(spotifySecrets);
SpotifyApiCredentials apiCredentials = authState.isAnonymous SpotifyApiCredentials apiCredentials = authState.isAnonymous
@ -26,20 +24,13 @@ var spotifyProvider = Provider<SpotifyApi>((ref) {
return SpotifyApi( return SpotifyApi(
apiCredentials, apiCredentials,
onCredentialsRefreshed: (credentials) async { onCredentialsRefreshed: (credentials) {
SharedPreferences localStorage = await SharedPreferences.getInstance(); authState.setAuthState(
localStorage.setString( clientId: credentials.clientId,
LocalStorageKeys.refreshToken, clientSecret: credentials.clientSecret,
credentials.refreshToken!, accessToken: credentials.accessToken,
); refreshToken: credentials.refreshToken,
localStorage.setString( expiration: credentials.expiration,
LocalStorageKeys.accessToken,
credentials.accessToken!,
);
localStorage.setString(LocalStorageKeys.clientId, credentials.clientId!);
localStorage.setString(
LocalStorageKeys.clientSecret,
credentials.clientSecret!,
); );
}, },
); );

View File

@ -1,162 +1,139 @@
import 'dart:async';
import 'dart:convert'; 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: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/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/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; ThemeMode themeMode;
String ytSearchFormat; String ytSearchFormat;
String recommendationMarket; String recommendationMarket;
bool saveTrackLyrics; bool saveTrackLyrics;
String geniusAccessToken; String geniusAccessToken;
SharedPreferences? localStorage;
HotKey? nextTrackHotKey; HotKey? nextTrackHotKey;
HotKey? prevTrackHotKey; HotKey? prevTrackHotKey;
HotKey? playPauseHotKey; HotKey? playPauseHotKey;
MaterialColor accentColorScheme;
MaterialColor backgroundColorScheme;
UserPreferences({ UserPreferences({
required this.geniusAccessToken, required this.geniusAccessToken,
required this.recommendationMarket, required this.recommendationMarket,
required this.themeMode, required this.themeMode,
required this.ytSearchFormat, required this.ytSearchFormat,
this.saveTrackLyrics = false, this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey,
this.nextTrackHotKey, this.nextTrackHotKey,
this.prevTrackHotKey, this.prevTrackHotKey,
this.playPauseHotKey, this.playPauseHotKey,
}) { }) : super();
onInit();
}
final logger = getLogger(UserPreferences);
Future<HotKey?> _getHotKeyFromLocalStorage(String key) async {
String? str = localStorage?.getString(key);
if (str != null) {
Map<String, dynamic> json = await jsonDecode(str);
if (json.isEmpty) {
return null;
}
return HotKey.fromJson(json);
}
return null;
}
Future<void> 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);
}
}
void setThemeMode(ThemeMode mode) { void setThemeMode(ThemeMode mode) {
themeMode = mode; themeMode = mode;
localStorage?.setString(LocalStorageKeys.themeMode, mode.name);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setSaveTrackLyrics(bool shouldSave) { void setSaveTrackLyrics(bool shouldSave) {
saveTrackLyrics = shouldSave; saveTrackLyrics = shouldSave;
localStorage?.setBool(LocalStorageKeys.saveTrackLyrics, shouldSave);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setRecommendationMarket(String country) { void setRecommendationMarket(String country) {
recommendationMarket = country; recommendationMarket = country;
localStorage?.setString(LocalStorageKeys.recommendationMarket, country);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setGeniusAccessToken(String token) { void setGeniusAccessToken(String token) {
geniusAccessToken = token; geniusAccessToken = token;
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setNextTrackHotKey(HotKey? value) { void setNextTrackHotKey(HotKey? value) {
nextTrackHotKey = value; nextTrackHotKey = value;
localStorage?.setString(
LocalStorageKeys.nextTrackHotKey,
jsonEncode(value?.toJson() ?? {}),
);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setPrevTrackHotKey(HotKey? value) { void setPrevTrackHotKey(HotKey? value) {
prevTrackHotKey = value; prevTrackHotKey = value;
localStorage?.setString(
LocalStorageKeys.prevTrackHotKey,
jsonEncode(value?.toJson() ?? {}),
);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setPlayPauseHotKey(HotKey? value) { void setPlayPauseHotKey(HotKey? value) {
playPauseHotKey = value; playPauseHotKey = value;
localStorage?.setString(
LocalStorageKeys.playPauseHotKey,
jsonEncode(value?.toJson() ?? {}),
);
notifyListeners(); notifyListeners();
updatePersistence();
} }
void setYtSearchFormat(String format) { void setYtSearchFormat(String format) {
ytSearchFormat = format; ytSearchFormat = format;
localStorage?.setString(LocalStorageKeys.ytSearchFormate, format);
notifyListeners(); notifyListeners();
updatePersistence();
}
void setAccentColorScheme(MaterialColor color) {
accentColorScheme = color;
notifyListeners();
updatePersistence();
}
void setBackgroundColorScheme(MaterialColor color) {
backgroundColorScheme = color;
notifyListeners();
updatePersistence();
}
@override
FutureOr<void> loadFromLocal(Map<String, dynamic> 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<Map<String, dynamic>> 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,
};
} }
} }

View File

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

View File

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

View File

@ -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<Map<String, dynamic>>({}, (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<void> loadFromLocal(Map<String, dynamic> map);
FutureOr<Map<String, dynamic>> toMap();
Future<void> 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);
}
}
}
}

View File

@ -1,4 +1,6 @@
PODS: PODS:
- audio_service (0.14.1):
- FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- FlutterMacOS - FlutterMacOS
- bitsdojo_window_macos (0.0.1): - bitsdojo_window_macos (0.0.1):
@ -13,6 +15,8 @@ PODS:
- HotKey - HotKey
- just_audio (0.0.1): - just_audio (0.0.1):
- FlutterMacOS - FlutterMacOS
- package_info_plus_macos (0.0.1):
- FlutterMacOS
- path_provider_macos (0.0.1): - path_provider_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_macos (0.0.1): - shared_preferences_macos (0.0.1):
@ -24,11 +28,13 @@ PODS:
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- hotkey_manager (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos`) - hotkey_manager (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/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`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
@ -40,6 +46,8 @@ SPEC REPOS:
- HotKey - HotKey
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos
audio_session: audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
bitsdojo_window_macos: bitsdojo_window_macos:
@ -50,6 +58,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos
just_audio: just_audio:
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos :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_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
shared_preferences_macos: shared_preferences_macos:
@ -60,17 +70,19 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
bitsdojo_window_macos: 7e9b1bbb09bdce418d9657ead7fc9d824203ff0d bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
HotKey: ad59450195936c10992438c4210f673de5aee43e HotKey: ad59450195936c10992438c4210f673de5aee43e
hotkey_manager: ad673457691f4d39e481be04a61da2ae07d81c62 hotkey_manager: ad673457691f4d39e481be04a61da2ae07d81c62
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f
shared_preferences_macos: 480ce071d0666e37cef23fe6c702293a3d21799e shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
PODFILE CHECKSUM: f7c7be88e75cc0b6c98b7564b0771f5dff5c5490 PODFILE CHECKSUM: f7c7be88e75cc0b6c98b7564b0771f5dff5c5490

View File

@ -14,14 +14,14 @@ packages:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.1" version: "3.3.0"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -63,35 +63,35 @@ packages:
name: bitsdojo_window name: bitsdojo_window
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.1+1" version: "0.1.2"
bitsdojo_window_linux: bitsdojo_window_linux:
dependency: transitive dependency: transitive
description: description:
name: bitsdojo_window_linux name: bitsdojo_window_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.1" version: "0.1.2"
bitsdojo_window_macos: bitsdojo_window_macos:
dependency: transitive dependency: transitive
description: description:
name: bitsdojo_window_macos name: bitsdojo_window_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.2"
bitsdojo_window_platform_interface: bitsdojo_window_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: bitsdojo_window_platform_interface name: bitsdojo_window_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.2"
bitsdojo_window_windows: bitsdojo_window_windows:
dependency: transitive dependency: transitive
description: description:
name: bitsdojo_window_windows name: bitsdojo_window_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -105,7 +105,7 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -154,7 +154,7 @@ packages:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -189,7 +189,7 @@ packages:
name: ffi name: ffi
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.2" version: "1.2.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -208,7 +208,7 @@ packages:
name: flutter_blurhash name: flutter_blurhash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.4" version: "0.7.0"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@ -222,7 +222,7 @@ packages:
name: flutter_hooks name: flutter_hooks
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.18.2+1" version: "0.18.4"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -243,7 +243,7 @@ packages:
name: flutter_riverpod name: flutter_riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -267,21 +267,21 @@ packages:
name: go_router name: go_router
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.4" version: "3.1.0"
hooks_riverpod: hooks_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
name: hooks_riverpod name: hooks_riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.4"
hotkey_manager: hotkey_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: hotkey_manager name: hotkey_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.6" version: "0.1.7"
html: html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -302,7 +302,7 @@ packages:
name: http_parser name: http_parser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0" version: "4.0.1"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -330,14 +330,14 @@ packages:
name: json_annotation name: json_annotation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "4.5.0"
just_audio: just_audio:
dependency: "direct main" dependency: "direct main"
description: description:
name: just_audio name: just_audio
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.20" version: "0.9.21"
just_audio_background: just_audio_background:
dependency: "direct main" dependency: "direct main"
description: description:
@ -400,7 +400,7 @@ packages:
name: marquee name: marquee
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.2.2"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -442,7 +442,7 @@ packages:
name: octo_image name: octo_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -512,49 +512,49 @@ packages:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.9" version: "2.0.10"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.14"
path_provider_ios: path_provider_ios:
dependency: transitive dependency: transitive
description: description:
name: path_provider_ios name: path_provider_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.9"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.6"
path_provider_macos: path_provider_macos:
dependency: transitive dependency: transitive
description: description:
name: path_provider_macos name: path_provider_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -582,7 +582,7 @@ packages:
name: permission_handler_apple name: permission_handler_apple
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.0.3" version: "9.0.4"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -603,7 +603,7 @@ packages:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "5.0.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -652,35 +652,35 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.13" version: "2.0.15"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.12"
shared_preferences_ios: shared_preferences_ios:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_ios name: shared_preferences_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
shared_preferences_macos: shared_preferences_macos:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_macos name: shared_preferences_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -694,14 +694,14 @@ packages:
name: shared_preferences_web name: shared_preferences_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -713,7 +713,7 @@ packages:
name: sliver_tools name: sliver_tools
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.5" version: "0.2.6"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -726,7 +726,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: e36eb5884de44d39b310d0878779a697048061bd resolved-ref: ea313e2d21c38157cd8255d248bcd7897bf51360
url: "https://github.com/KRTirtho/spotify-dart.git" url: "https://github.com/KRTirtho/spotify-dart.git"
source: git source: git
version: "0.7.0" version: "0.7.0"
@ -736,14 +736,14 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2+1"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1+1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -778,7 +778,7 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0+2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -799,42 +799,42 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.20" version: "6.1.2"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.15" version: "6.0.17"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.15" version: "6.0.17"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -848,14 +848,14 @@ packages:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.8" version: "2.0.11"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@ -876,7 +876,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.1" version: "2.6.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -890,21 +890,21 @@ packages:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.3.1" version: "5.4.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
name: yaml name: yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
youtube_explode_dart: youtube_explode_dart:
dependency: "direct main" dependency: "direct main"
description: description:
name: youtube_explode_dart name: youtube_explode_dart
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.9+1" version: "1.11.0"
sdks: sdks:
dart: ">=2.17.0-0 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=2.10.0" flutter: ">=3.0.0"

View File

@ -42,7 +42,7 @@ dependencies:
url_launcher: ^6.0.17 url_launcher: ^6.0.17
youtube_explode_dart: ^1.10.8 youtube_explode_dart: ^1.10.8
infinite_scroll_pagination: ^3.1.0 infinite_scroll_pagination: ^3.1.0
bitsdojo_window: ^0.1.1+1 bitsdojo_window: ^0.1.2
hotkey_manager: ^0.1.6 hotkey_manager: ^0.1.6
just_audio: ^0.9.18 just_audio: ^0.9.18
just_audio_libwinmedia: ^0.0.4 just_audio_libwinmedia: ^0.0.4