mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
76d8ec1dc9
45
.github/workflows/release-build.yml
vendored
45
.github/workflows/release-build.yml
vendored
@ -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 }}
|
||||||
|
24
.metadata
24
.metadata
@ -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'
|
||||||
|
3
.vscode/c_cpp_properties.json
vendored
3
.vscode/c_cpp_properties.json
vendored
@ -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
|
||||||
|
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,3 +1,26 @@
|
|||||||
|
# v2.1.0
|
||||||
|
|
||||||
|
### New
|
||||||
|
- Synced Lyrics (with fallback genius lyrics)
|
||||||
|
- Playlist create/delete
|
||||||
|
- Add/Remove tracks to own playlists
|
||||||
|
- Custom YouTube track search term template
|
||||||
|
- Downloading lyrics along with a track (can be toggled)
|
||||||
|
- Customize Marketplace location
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Spotify track to youtube track algorithm
|
||||||
|
- Genius lyrics matching algorithm
|
||||||
|
- Download track. Checks if already exists & replaces on user command
|
||||||
|
- Wide screen responsiveness & adaptation
|
||||||
|
- Bigger Title display (replaced word-break with Marquee Text for better visibility) (https://github.com/KRTirtho/spotube/pull/47)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- Sequential playlist playback not working with latest webkit2gtk (https://github.com/KRTirtho/spotube/issues/46)
|
||||||
|
- Theme modification state doesn't persist (https://github.com/KRTirtho/spotube/issues/54)
|
||||||
|
- Wrong URI path for "Login with Spotify" tutorial (https://github.com/KRTirtho/spotube/issues/69)
|
||||||
|
- Card shadow showing in the background of TitleBar & Searchbar
|
||||||
|
|
||||||
# v2.0.0
|
# v2.0.0
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
2
Makefile
2
Makefile
@ -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
122
README.md
@ -41,7 +41,7 @@ Following are the features that currently spotube offers:
|
|||||||
- Playback control is on user's machine instead of server based
|
- 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)
|
||||||

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

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

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

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

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

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

|
<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">© 2022 Spotube</p>
|
<p align="center">© 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
|
@ -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"))
|
||||||
|
@ -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 =
|
pagingController.addPageRequestListener(listener);
|
||||||
expirationStr != null ? DateTime.parse(expirationStr) : null;
|
// the world is full of surprises and the previously working
|
||||||
final anonCred = getRandomElement(spotifySecrets);
|
// fine pageRequestListener now doesn't notify the listeners
|
||||||
SpotifyApiCredentials apiCredentials =
|
// automatically after assigning a listener. So doing it manually
|
||||||
clientId != null && clientSecret != null
|
pagingController.notifyPageRequestListeners(0);
|
||||||
? SpotifyApiCredentials(
|
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
expiration: expiration,
|
|
||||||
scopes: spotifyScopes,
|
|
||||||
)
|
|
||||||
: SpotifyApiCredentials(
|
|
||||||
anonCred["clientId"],
|
|
||||||
anonCred["clientSecret"],
|
|
||||||
);
|
|
||||||
|
|
||||||
SpotifyApi spotify = SpotifyApi(apiCredentials);
|
|
||||||
if (clientId != null && clientSecret != null) {
|
|
||||||
spotify.getCredentials().then((credentials) {
|
|
||||||
if (credentials.accessToken?.isNotEmpty == true) {
|
|
||||||
auth.setAuthState(
|
|
||||||
clientId: clientId,
|
|
||||||
clientSecret: clientSecret,
|
|
||||||
accessToken:
|
|
||||||
credentials.accessToken, // accessToken can be new/refreshed
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
expiration: credentials.expiration,
|
|
||||||
isLoggedIn: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pagingController.addPageRequestListener(listener);
|
|
||||||
// the world is full of surprises and the previously working
|
|
||||||
// fine pageRequestListener now doesn't notify the listeners
|
|
||||||
// automatically after assigning a listener. So doing it manually
|
|
||||||
pagingController.notifyPageRequestListeners(0);
|
|
||||||
}).catchError((e, stack) {
|
|
||||||
if (e is AuthorizationException) {
|
|
||||||
oauthLogin(
|
|
||||||
auth,
|
|
||||||
clientId: clientId,
|
|
||||||
clientSecret: clientSecret,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
logger.e("useEffect.spotify.getCredentials", e, stack);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
pagingController.addPageRequestListener(listener);
|
|
||||||
pagingController.notifyPageRequestListeners(0);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logger.e("initState", e, stack);
|
logger.e("initState", e, stack);
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
@ -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) &&
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
136
lib/components/Settings/ColorSchemePickerDialog.dart
Normal file
136
lib/components/Settings/ColorSchemePickerDialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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: [
|
@ -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();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
103
lib/main.dart
103
lib/main.dart
@ -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],
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
textTheme: TextTheme(
|
|
||||||
bodyText1: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline1: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline2: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline3: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline4: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline5: TextStyle(color: Colors.grey[850]),
|
|
||||||
headline6: TextStyle(color: Colors.grey[850]),
|
|
||||||
),
|
|
||||||
listTileTheme: ListTileThemeData(
|
|
||||||
iconColor: Colors.grey[850],
|
|
||||||
horizontalTitleGap: 0,
|
|
||||||
),
|
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Colors.green[400]!,
|
|
||||||
width: 2.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Colors.grey[800]!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
navigationRailTheme: NavigationRailThemeData(
|
|
||||||
backgroundColor: Colors.blueGrey[50],
|
|
||||||
unselectedIconTheme:
|
|
||||||
IconThemeData(color: Colors.grey[850], opacity: 1),
|
|
||||||
unselectedLabelTextStyle: TextStyle(
|
|
||||||
color: Colors.grey[850],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
|
||||||
backgroundColor: Colors.blueGrey[50],
|
|
||||||
height: 55,
|
|
||||||
),
|
|
||||||
cardTheme: CardTheme(
|
|
||||||
shape:
|
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: darkTheme(
|
||||||
brightness: Brightness.dark,
|
accentMaterialColor: accentMaterialColor,
|
||||||
primaryColor: Colors.green,
|
backgroundMaterialColor: backgroundMaterialColor,
|
||||||
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,
|
||||||
);
|
);
|
||||||
|
@ -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(
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Map<String, dynamic>> toMap() {
|
||||||
|
return {
|
||||||
|
"clientId": _clientId,
|
||||||
|
"clientSecret": _clientSecret,
|
||||||
|
"accessToken": _accessToken,
|
||||||
|
"refreshToken": _refreshToken,
|
||||||
|
"expiration": _expiration.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());
|
final authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());
|
||||||
|
@ -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) {
|
||||||
|
@ -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!,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
62
lib/themes/dark-theme.dart
Normal file
62
lib/themes/dark-theme.dart
Normal 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],
|
||||||
|
);
|
||||||
|
}
|
87
lib/themes/light-theme.dart
Normal file
87
lib/themes/light-theme.dart
Normal 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],
|
||||||
|
);
|
||||||
|
}
|
53
lib/utils/PersistedChangeNotifier.dart
Normal file
53
lib/utils/PersistedChangeNotifier.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
112
pubspec.lock
112
pubspec.lock
@ -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"
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user