Merge pull request #536 from KRTirtho/dev

Version 3 🚀
This commit is contained in:
Kingkor Roy Tirtho 2023-07-02 22:47:06 +06:00 committed by GitHub
commit 902b66a71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
283 changed files with 17610 additions and 8059 deletions

View File

@ -1,7 +1,11 @@
POCKETBASE_URL= SUPABASE_URL=
USERNAME= SUPABASE_API_KEY=
PASSWORD=
# The format: # The format:
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2 # SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
SPOTIFY_SECRETS= SPOTIFY_SECRETS=
# 0 or 1
# 0 = disable
# 1 = enable
ENABLE_UPDATE_CHECK=

View File

@ -46,7 +46,7 @@ body:
attributes: attributes:
label: Spotube version label: Spotube version
description: In which version of Spotube did you encounter this bug? description: In which version of Spotube did you encounter this bug?
placeholder: (e.g.) v2.7.1 placeholder: (e.g.) v3.0.0
- type: dropdown - type: dropdown
attributes: attributes:
label: Installation source label: Installation source

View File

@ -4,7 +4,7 @@ on:
inputs: inputs:
version: version:
description: Version to publish (x.x.x) description: Version to publish (x.x.x)
default: 2.7.1 default: 3.0.0
required: true required: true
dry_run: dry_run:
description: Dry run description: Dry run

View File

@ -4,7 +4,7 @@ on:
inputs: inputs:
version: version:
description: Version to release (x.x.x) description: Version to release (x.x.x)
default: 2.7.1 default: 3.0.0
required: true required: true
channel: channel:
type: choice type: choice
@ -14,12 +14,20 @@ on:
- stable - stable
- nightly - nightly
default: nightly default: nightly
debug:
description: Debug on failed when channel is nightly
required: true
type: boolean
default: false
dry_run: dry_run:
description: Dry run description: Dry run
required: true required: true
type: boolean type: boolean
default: true default: true
env:
FLUTTER_VERSION: '3.10.0'
jobs: jobs:
windows: windows:
runs-on: windows-latest runs-on: windows-latest
@ -28,6 +36,7 @@ jobs:
- uses: subosito/flutter-action@v2.10.0 - uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Replace pubspec version and BUILD_VERSION Env (nightly) - name: Replace pubspec version and BUILD_VERSION Env (nightly)
if: ${{ inputs.channel == 'nightly' }} if: ${{ inputs.channel == 'nightly' }}
@ -61,7 +70,7 @@ jobs:
run: | run: |
flutter config --enable-windows-desktop flutter config --enable-windows-desktop
flutter pub get flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
- name: Build Windows Executable - name: Build Windows Executable
run: | run: |
@ -78,6 +87,12 @@ jobs:
make choco make choco
mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg
- name: Debug With SSH When fails
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
@ -91,6 +106,7 @@ jobs:
- uses: subosito/flutter-action@v2.10.0 - uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Get current date - name: Get current date
id: date id: date
@ -99,7 +115,7 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev locate patchelf libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev
- name: Install AppImage Tool - name: Install AppImage Tool
run: | run: |
@ -136,15 +152,15 @@ jobs:
run: | run: |
flutter config --enable-linux-desktop flutter config --enable-linux-desktop
flutter pub get flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
- name: Build Linux Packages - name: Build Linux Packages
run: | run: |
dart pub global activate flutter_distributor dart pub global activate flutter_distributor
alias dpkg-deb="dpkg-deb --Zxz" alias dpkg-deb="dpkg-deb --Zxz"
flutter_distributor package --platform=linux --targets=deb
flutter_distributor package --platform=linux --targets=appimage flutter_distributor package --platform=linux --targets=appimage
flutter_distributor package --platform=linux --targets=rpm flutter_distributor package --platform=linux --targets=rpm
flutter_distributor package --platform=linux --targets=deb
- name: Create tar.xz (stable) - name: Create tar.xz (stable)
if: ${{ inputs.channel == 'stable' }} if: ${{ inputs.channel == 'stable' }}
@ -161,18 +177,25 @@ jobs:
mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm
mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage
- name: Debug With SSH When fails
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: Spotube-Release-Binaries name: Spotube-Release-Binaries
path: dist/ path: dist/
android: android:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: subosito/flutter-action@v2.10.0 - uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Dependencies - name: Install Dependencies
run: | run: |
@ -203,7 +226,7 @@ jobs:
- name: Generate Secrets - name: Generate Secrets
run: | run: |
flutter pub get flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
- name: Sign Apk - name: Sign Apk
run: | run: |
@ -215,6 +238,12 @@ jobs:
flutter build apk flutter build apk
mv build/app/outputs/apk/release/app-release.apk build/Spotube-android-all-arch.apk mv build/app/outputs/apk/release/app-release.apk build/Spotube-android-all-arch.apk
- name: Debug With SSH When fails
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: Spotube-Release-Binaries name: Spotube-Release-Binaries
@ -228,6 +257,7 @@ jobs:
- uses: subosito/flutter-action@v2.10.0 - uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Replace pubspec version and BUILD_VERSION Env (nightly) - name: Replace pubspec version and BUILD_VERSION Env (nightly)
if: ${{ inputs.channel == 'nightly' }} if: ${{ inputs.channel == 'nightly' }}
@ -253,7 +283,8 @@ jobs:
- name: Generate Secrets - name: Generate Secrets
run: | run: |
flutter pub get flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs flutter pub remove media_kit_native_event_loop
dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
- name: Build Macos App - name: Build Macos App
run: | run: |
@ -265,16 +296,22 @@ jobs:
run: | run: |
npm install -g appdmg npm install -g appdmg
mkdir -p build/${{ env.BUILD_VERSION }} mkdir -p build/${{ env.BUILD_VERSION }}
appdmg appdmg.json build/Spotube-macos-x86_64.dmg appdmg appdmg.json build/Spotube-macos-universal.dmg
- name: Debug With SSH When fails
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: Spotube-Release-Binaries name: Spotube-Release-Binaries
path: | path: |
build/Spotube-macos-x86_64.dmg build/Spotube-macos-universal.dmg
upload: upload:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
needs: needs:
- windows - windows
- linux - linux

18
.vscode/launch.json vendored
View File

@ -2,11 +2,25 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Flutter", "name": "spotube",
"type": "dart", "type": "dart",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/lib/main.dart" "program": "lib/main.dart"
}, },
{
"name": "spotube (profile)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"flutterMode": "profile"
},
{
"name": "spotube (release)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"flutterMode": "release"
}
], ],
"compounds": [] "compounds": []
} }

View File

@ -1,7 +1,11 @@
{ {
"cmake.configureOnOpen": false, "cmake.configureOnOpen": false,
"cSpell.words": [ "cSpell.words": [
"acousticness",
"danceability",
"instrumentalness",
"Mpris", "Mpris",
"speechiness",
"Spotube", "Spotube",
"winget" "winget"
] ]

View File

@ -2,6 +2,158 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [3.0.0](https://github.com/KRTirtho/spotube/compare/v2.7.1...v3.0.0) (2023-07-02)
### Features
* adaptive controllers ([c8b7de0](https://github.com/KRTirtho/spotube/commit/c8b7de087917ec3037c015d5b55693cb3dbdecca))
* adaptive popup and bottom sheet list widget ([ddc1c5f](https://github.com/KRTirtho/spotube/commit/ddc1c5f373a4d72cc231c35dd70c3d577b84f7f5))
* add generated to playlist(s) ([c91d8c8](https://github.com/KRTirtho/spotube/commit/c91d8c8efa8b526c64881fa829992b8c250e7c89))
* add german locale ([ba3f428](https://github.com/KRTirtho/spotube/commit/ba3f4281f1a6bc7ddf38775e9d40dad863ed3692))
* add piped search mode ([17a25a5](https://github.com/KRTirtho/spotube/commit/17a25a501e0d5e2512d8de0921fd602ea906d30f))
* add sleep timer support ([4a75f3d](https://github.com/KRTirtho/spotube/commit/4a75f3dbd1e7e6f68899de001df70e809533f142))
* adjust lyric page blurriness and player playbutton ([54d5907](https://github.com/KRTirtho/spotube/commit/54d5907f14df04f3f983ba2b0401ba05785da03b))
* album art dominant color as accent color ([#447](https://github.com/KRTirtho/spotube/issues/447)) ([31b9249](https://github.com/KRTirtho/spotube/commit/31b9249cc8f7313a132a514a9ca825c2ae1e2256))
* **android:** add splash screen ([c232fcc](https://github.com/KRTirtho/spotube/commit/c232fcc6dd1479ed33a8baa9887de2702a8ea22e))
* **android:** disable battery optimization for better playback ([fe5b429](https://github.com/KRTirtho/spotube/commit/fe5b429ddacc576fc9fdb5e66718782cda163b27))
* artist card redesign ([92a418c](https://github.com/KRTirtho/spotube/commit/92a418c8a8a9df99e27407b628e5e3cc9ccb4115))
* Better download manager with download progress ([6752adc](https://github.com/KRTirtho/spotube/commit/6752adc9398818f51b69fced226b4b8410fb9e9b))
* better language picker, adaptive select tile and settings section contrast ([6430a25](https://github.com/KRTirtho/spotube/commit/6430a2587075aa24483ab26ce6f0f6b2b630e139))
* cache encryption for sensitive data ([b110d83](https://github.com/KRTirtho/spotube/commit/b110d834561ac53129ac9ec80238c014c84832ec))
* color scheme picker dialog vertical list view instead of wrap ([bb60b01](https://github.com/KRTirtho/spotube/commit/bb60b01ef2f2ba3da8f6fe3a19add168b2ee8a4e))
* compact and adaptive playbutton card design ([eeb8cab](https://github.com/KRTirtho/spotube/commit/eeb8cabf491d5242bd434b3c71c39363f24bdcf9))
* compact button tabbar ([67380f6](https://github.com/KRTirtho/spotube/commit/67380f68765f18e4dcd3d60117083c7e9c6761c2))
* create a basic installer script ([1763a36](https://github.com/KRTirtho/spotube/commit/1763a36a262178306df61d4588c17ff795a32790))
* curved navigation bar ([776edf8](https://github.com/KRTirtho/spotube/commit/776edf84afcf99f96cf6e337b0c84ed89034ca8e))
* custom error toast ([96f04c1](https://github.com/KRTirtho/spotube/commit/96f04c17565c0ab7f115d5c1f167f6660a69480d))
* custom playlist generator ([f4b0d13](https://github.com/KRTirtho/spotube/commit/f4b0d134ca724b75bf65b885bce4dba206f1e090))
* desktop mini player support ([471812d](https://github.com/KRTirtho/spotube/commit/471812d789eb2c861268ab8451d50104ac2fbe2e))
* **desktop:** close button for minimize notification ([1688f99](https://github.com/KRTirtho/spotube/commit/1688f99096af940ead65e67832c6f061a6f635ac))
* **desktop:** show minimized to system tray notification ([296f96c](https://github.com/KRTirtho/spotube/commit/296f96cf17cf21ff09b406cc24952ff60da52d5e))
* disable/enable smtc on demand ([7fa50e5](https://github.com/KRTirtho/spotube/commit/7fa50e5c5ee9ca3ba95dca55f8a4831047f17570))
* download button on each track ([925fa86](https://github.com/KRTirtho/spotube/commit/925fa86271fa10ff77b8137ba8d09b8067d0e819))
* enable caching of queue ([ec11af5](https://github.com/KRTirtho/spotube/commit/ec11af53a16c435fbea3d0c81910dca371be9ce7))
* heart button animation ([8432dc6](https://github.com/KRTirtho/spotube/commit/8432dc6286fbdfda52bbeb39c6d4ababa05881bc))
* improved track item API and UI ([617aa89](https://github.com/KRTirtho/spotube/commit/617aa89409ce29eb3c197aee5c1189d763a3913c))
* **installer:** get latest version from Github API ([957c085](https://github.com/KRTirtho/spotube/commit/957c085e1243ec0af5bc45d38691c74e2ba91ad8))
* **local_tracks:** delete local track ([#484](https://github.com/KRTirtho/spotube/issues/484)) ([52835b2](https://github.com/KRTirtho/spotube/commit/52835b2ce2212925f80e1a1595c0ca30e6860a8d))
* locale category/genre title ([88137f0](https://github.com/KRTirtho/spotube/commit/88137f01b27150b306327a01e67ec8a35a60e82e))
* **locale:** add bengali translations for search page ([a1cdbad](https://github.com/KRTirtho/spotube/commit/a1cdbad18782a74b43f0625facfe3e35c516bf43))
* **locale:** localize search, library, lyrics, artist with both Bengali and English ([11fe9ec](https://github.com/KRTirtho/spotube/commit/11fe9ec74462441b67a0ab0df73824f36dd15e2d))
* **locale:** player, playlist view, track tile bengali and english translations ([c55133d](https://github.com/KRTirtho/spotube/commit/c55133dc8bba307823e5b67a30cfab03c923cb7f))
* localize settings, about, login, player queue with Bengali and English translations ([a5c36bb](https://github.com/KRTirtho/spotube/commit/a5c36bbb20cc69d609bfb5ab973c7e288c1ea9de))
* logs page in settings ([b78e7f5](https://github.com/KRTirtho/spotube/commit/b78e7f57a05db344aae59206cbb0f43b3ee199a9))
* macos title bar spacing and lyrics page margin separate ([a0b3771](https://github.com/KRTirtho/spotube/commit/a0b377104f9822561d3b46dbc6551bb561842480))
* make snackbar floating ([9dbb817](https://github.com/KRTirtho/spotube/commit/9dbb8171a6d6b81120ca7ccd74577e5c890ff930))
* merge floating player with nav bar and nav bar translucent bg ([a90261e](https://github.com/KRTirtho/spotube/commit/a90261ed199f6cff9e8d0fe24934f8f1d8e9ed98))
* **mini_player:** remove window shadow ([6259014](https://github.com/KRTirtho/spotube/commit/625901482ada4b441b838f640c0ab7167119b321))
* **mini_player:** show/hide UI on hover toggle ([2e8b647](https://github.com/KRTirtho/spotube/commit/2e8b647a51f87840c2bd39f0a1dc25ddc91528fc))
* new sidebar widget and translucent bottom player ([4ba1e70](https://github.com/KRTirtho/spotube/commit/4ba1e70636b4ba43697663128fc5422b1d0b2a2f))
* newly released albums of user followed artist ([33cb794](https://github.com/KRTirtho/spotube/commit/33cb7947d63d0a2692a004f87a2ccd5777bf054e))
* optimize image load + genre page and reduce page size of loaded categories ([7131efa](https://github.com/KRTirtho/spotube/commit/7131efa07fdbcf17965fc59ff635a6198b0e5e25))
* persistent volume percentage ([3724bd5](https://github.com/KRTirtho/spotube/commit/3724bd5a10eef7a099d6f596fd038e6fea228359))
* personal playlist recommendations ([ae820a2](https://github.com/KRTirtho/spotube/commit/ae820a22f291082c49554d621c25cc62212a6708))
* piped instance picker on settings ([bed0d3b](https://github.com/KRTirtho/spotube/commit/bed0d3bd70438df413633ee03fd258a2ca4a1688))
* platform specific title bar buttons ([6267720](https://github.com/KRTirtho/spotube/commit/62677209a23172162defb7a8e542d981569eba08))
* **playback:** integrate android, ios, macos with JustAudio ([d487fe5](https://github.com/KRTirtho/spotube/commit/d487fe55630993e2b729050ebd0bf4e1e4be1fb3))
* **playback:** use assets_audio_player to fix macos double duration problems and android high loading latency ([1fff0f1](https://github.com/KRTirtho/spotube/commit/1fff0f1bd0d811c348f293f733cbcb7cd57e02f8))
* player details dialog and separate location of lyrics button in player page ([ce38233](https://github.com/KRTirtho/spotube/commit/ce38233de8f4775018a1d01e951b1635776fe743))
* **player:** add playlist related methods to audio player ([f1080e1](https://github.com/KRTirtho/spotube/commit/f1080e1675aee1208d05658adfabfbed04ff45b6))
* **player:** animated gradient background ([49b5d0e](https://github.com/KRTirtho/spotube/commit/49b5d0e6948d80abb8ee09203e0d655d68377245))
* **player:** custom playlist implementation for media_kit to replace unpredictable playlist of mpv ([eaf65b6](https://github.com/KRTirtho/spotube/commit/eaf65b6db208aaad745821d4d42afc05f51cee7c))
* **player:** proper coloring of elements ([b2c4ea1](https://github.com/KRTirtho/spotube/commit/b2c4ea13f6157c2b7bec3957e1f7f50fbf0002c7))
* **player:** replace bg blur with gradient, proper fg color and align title and artist name ([159f03e](https://github.com/KRTirtho/spotube/commit/159f03e7ca62e6b3b86389e2795da84de61fba78))
* playlist create support for generated playlist ([91c72f9](https://github.com/KRTirtho/spotube/commit/91c72f9ec9556f301c5d129fc82e19e791a02fbe))
* playlist generation all parameters support ([9877d5f](https://github.com/KRTirtho/spotube/commit/9877d5f51736db03d5839dadf164d11d0cce82f0))
* **playlist,album page:** play and shuffle take full width on smaller screens, add new xs breakpoint ([dce1b88](https://github.com/KRTirtho/spotube/commit/dce1b88694cfcb6b7e63d6ee614ac1dbbd017f6e))
* **queue:** add track(s) for playing next ([#460](https://github.com/KRTirtho/spotube/issues/460)) ([cac8ea6](https://github.com/KRTirtho/spotube/commit/cac8ea638812f5d9cb4305144b6351141a2cf407))
* **queue:** reorder tracks support ([441b43b](https://github.com/KRTirtho/spotube/commit/441b43bef6b92fd7df6c4e1bef39d67b4a76cd22))
* re-designed playlist/album page ([0cedc7a](https://github.com/KRTirtho/spotube/commit/0cedc7a4187771efce8152003f890e242116c78c))
* re-introduce youtube API along with piped ([b54ee96](https://github.com/KRTirtho/spotube/commit/b54ee96233b29d7517eba66e3f8dd9270c2790df))
* reactive volume slider and slicker bottom bar with lowered height ([9d14517](https://github.com/KRTirtho/spotube/commit/9d14517202d5c9d993a947808bf0c6520ed54ea3))
* remove SponsorBlock in favor of YT Music and remove pocketbase backend track support ([fb780da](https://github.com/KRTirtho/spotube/commit/fb780da327a213d7a82cbc3b567ece858dc2f0e8))
* repeat button all 3 mode and disable player controls when track is fetching ([1418378](https://github.com/KRTirtho/spotube/commit/14183781dd3f1e16c121e78ad637a326de7b5dcf))
* replace YouTube API with piped API ([1ecc36d](https://github.com/KRTirtho/spotube/commit/1ecc36da57af61fd9c2ca928589088cd4325f605))
* responsive playlist generate page and scrollable multi autocomplete ([d57aad5](https://github.com/KRTirtho/spotube/commit/d57aad5612f7622dcd638ea8c0ec4d96f741de2b))
* search alternative track source ([dfea195](https://github.com/KRTirtho/spotube/commit/dfea195ec178de733717cfe3226cede7521ee2d3))
* setup localization (l10n) and language switcher, add sidebar and navbar locale ([f12d812](https://github.com/KRTirtho/spotube/commit/f12d81259f9e7005e681a7ca9867291d9228a8b1))
* show album release year ([#387](https://github.com/KRTirtho/spotube/issues/387)) and fix layout of artist's album ([6a6ddf6](https://github.com/KRTirtho/spotube/commit/6a6ddf6e1f6dc72b794cae49adf8348da272babd))
* show country code piped instance list ([60328a6](https://github.com/KRTirtho/spotube/commit/60328a6bafcbff1b7d0ee5099825f0e3d545b60f))
* show loading when track metadata is being fetched, android, ios, macos enable shuffling ([bf59570](https://github.com/KRTirtho/spotube/commit/bf59570251720a80efe0aa6be481899864da5079))
* sort tracks by newest and oldest dates ([b4713e3](https://github.com/KRTirtho/spotube/commit/b4713e377a938cbebe70089874216f86fe550c34))
* supabase integration ([8bcce92](https://github.com/KRTirtho/spotube/commit/8bcce9282eae08c5996a27f16f89cbc187a06823))
* system tray support ([#31](https://github.com/KRTirtho/spotube/issues/31)) ([06a0437](https://github.com/KRTirtho/spotube/commit/06a043764d9f65fb448fcf088ccf2737145e23e8))
* track populate sibling support ([3aeb026](https://github.com/KRTirtho/spotube/commit/3aeb026776716b6e2eb89c8406a4996a86c7ca60))
* **translation:** add hindi and french translations using ChatGPT ([6d836bd](https://github.com/KRTirtho/spotube/commit/6d836bdb658c180ca8e2c71e7e290fafa3520727))
* **translation:** add Japanase locale ([4b52a71](https://github.com/KRTirtho/spotube/commit/4b52a71c0914bda6c831d8f637a5934f7bcf8fcb))
* use system color scheme ([862c4b8](https://github.com/KRTirtho/spotube/commit/862c4b8faf2c751d803e373e29981a116bf08ed5))
* volume slider in player page ([7abe2c1](https://github.com/KRTirtho/spotube/commit/7abe2c10735bc38c644487139557a731d25e80e6))
* windows OS media control panel support ([f0b426a](https://github.com/KRTirtho/spotube/commit/f0b426ae89f2e01f4a9c8757ef4e0b4a21b50c7b))
### Bug Fixes
* add to playlist dialog not showing playlist name ([8944581](https://github.com/KRTirtho/spotube/commit/8944581c09eec0162220e7ff684205484fafb599))
* album sync not working ([74906f3](https://github.com/KRTirtho/spotube/commit/74906f393250934c36530a73ad7312f59f8627ed))
* alternative track source not playing new source ([a9b5a71](https://github.com/KRTirtho/spotube/commit/a9b5a714e47d40407d799966ae95f84338f9b59a))
* **android:** use multi assetAudioPlayer instance fix patch and disable Pre-download and play by default in Android too ([cdb3268](https://github.com/KRTirtho/spotube/commit/cdb32685e4bbb899706ed16d58ef9a3a074e283a))
* **artist:** follower count shows as float when < 1000 ([#482](https://github.com/KRTirtho/spotube/issues/482)) ([fd1846e](https://github.com/KRTirtho/spotube/commit/fd1846eecf9632e59e4b70fb70e97c556b6374f5))
* bottom navbar first item icon color not changing on primary color change ([6eb4244](https://github.com/KRTirtho/spotube/commit/6eb4244f3244a96fe6858261534cc03eb3de803c))
* cached currently playing track infinite loading ([9401718](https://github.com/KRTirtho/spotube/commit/94017189c6b9bf55ec62cbf29cd6b0e9fffca42a))
* cached queue tracks expired stream ([ed29ab5](https://github.com/KRTirtho/spotube/commit/ed29ab5137416d9fb2e7e9fe840f56ef52df6f61))
* collection currently playing state persist on restart ([1c89e3e](https://github.com/KRTirtho/spotube/commit/1c89e3efb0f05c648fc1c8e09039e62333de18d1))
* color not syncing and add new screenshot ([6205501](https://github.com/KRTirtho/spotube/commit/62055018feade0b895663a0bfc5f85f265ae2154))
* content going below bottom player or nav bar ([1bdce9f](https://github.com/KRTirtho/spotube/commit/1bdce9fe964de88a667bb160846c11dc70b77c00))
* disable background_downloader due to android build failures ([7d23bee](https://github.com/KRTirtho/spotube/commit/7d23beec5ef07c4d649185a69e7a2b9697dc6953))
* disable play when loading track and buffering event ([30c933c](https://github.com/KRTirtho/spotube/commit/30c933cdf3d4524be164e171094afdd27b0252b7))
* error log ([e3d8239](https://github.com/KRTirtho/spotube/commit/e3d8239b9f5700bfb17c4758d95ba1db1f0e718a))
* excessive repaints caused by Player progress bar ([09b24cf](https://github.com/KRTirtho/spotube/commit/09b24cf1fd1644c549f85904545db54b39cc2431))
* failed download no error icon ([1266a3f](https://github.com/KRTirtho/spotube/commit/1266a3f1607de11e793a294071850996527d494a))
* **home:** bottom player transparency ([20c424c](https://github.com/KRTirtho/spotube/commit/20c424c77fe273a693213ebf88d50e4025bc8608))
* language changer not working ([7b7b1f2](https://github.com/KRTirtho/spotube/commit/7b7b1f2647591b7cd4cc7841526716c6a2877e55))
* less frequent position updates ([0a49b56](https://github.com/KRTirtho/spotube/commit/0a49b56566abd00cf7703e4207cfa90f93c381fd))
* linux mpris not showing up and overall media notification service ([1abcad1](https://github.com/KRTirtho/spotube/commit/1abcad1de510c209a34196f2de17045af4dd3bc2))
* local tracks getting fetched on first load ([73c012c](https://github.com/KRTirtho/spotube/commit/73c012c71ab5050636f79e010d654b4390978ee7))
* local tracks not working when there's a invalid music file in the folder ([5855820](https://github.com/KRTirtho/spotube/commit/5855820569dfad7cd26f1e0f0c985babd0d9485d))
* lyrics page blur in player and cut off text when line too big ([6b4584e](https://github.com/KRTirtho/spotube/commit/6b4584e91bd4f4aee0c56e48a7aec7015c7c418b))
* macos build by removing media_kit native event loop ([62fc773](https://github.com/KRTirtho/spotube/commit/62fc7739b508f0e874978408a2bab0a1d422deb6))
* macos build error, mobile player duration and playing state and background disposal of player ([be91e33](https://github.com/KRTirtho/spotube/commit/be91e33828630c7062886cd15e4d57496daaa4d5))
* **macos,ios:** use regular shared prefs ([1b5bfec](https://github.com/KRTirtho/spotube/commit/1b5bfec27fbcfe9faabff64d46296bdeebe00161))
* memoize child of animated widget and make player bg animation faster ([fcb5c8f](https://github.com/KRTirtho/spotube/commit/fcb5c8f8dabd0d4e3033f80ea3e5d006243cdfb5))
* mini player not working in release mode ([28ff321](https://github.com/KRTirtho/spotube/commit/28ff3216efee81184798eedfbb10ba66395bbf36))
* **mkPlayer:** remove method and wrong active index on modifying playlist ([3bafa7b](https://github.com/KRTirtho/spotube/commit/3bafa7b80c963fa52b90ed4cb1393fb121cac713))
* mobile audio notification not working ([8f9303b](https://github.com/KRTirtho/spotube/commit/8f9303bc0fddb9d179303a1f0eb76dd5b02410e7))
* multiple instance of theme ([4ec0424](https://github.com/KRTirtho/spotube/commit/4ec04240a5bde6af5c920a61ab6260e7a93bfc54))
* navigation to settings not working ([ce10aa1](https://github.com/KRTirtho/spotube/commit/ce10aa1fe2c95d4738835687f613930cf7829f3a))
* no progress update when track changed ([6ae8964](https://github.com/KRTirtho/spotube/commit/6ae896441a787fce1bc6e5eb5379856dc2f4e96d))
* null exception on proxy playlist and audio player ([a455a89](https://github.com/KRTirtho/spotube/commit/a455a89c5861fd455f6950c7b68beae24bdcc6ed))
* overflowing clickable artists links ([4077fac](https://github.com/KRTirtho/spotube/commit/4077fac39fb667b87e959e53d2dcaceefb63cd2d))
* personalized playlists not loading ([caa3408](https://github.com/KRTirtho/spotube/commit/caa340803fdf3859fe5a8a996abae1502ef2e4e7))
* playback not moving to next track after a track ends ([27e8acb](https://github.com/KRTirtho/spotube/commit/27e8acbfe75a37c0a8fa69a444fdd86e92dbe4f0))
* **player:** gradient bg not taking full height ([62ad86e](https://github.com/KRTirtho/spotube/commit/62ad86e88d74b5114af78138b221314192e5a801))
* **player:** playback element placement ([5e47faa](https://github.com/KRTirtho/spotube/commit/5e47faa6060d7a8aa0d143060e812dc06b8dd790))
* **player:** queue button not showing when not logged in ([6c2d655](https://github.com/KRTirtho/spotube/commit/6c2d65587b0e6e167be1d0b086df103c7e72d4b2))
* **player:** volume slider, prefetching of media_kit and stuttering on sponsorblock skip ([1f32554](https://github.com/KRTirtho/spotube/commit/1f3255481f058c50968561db88172e56b58494f4))
* playlist generate slider shape ([2b35c04](https://github.com/KRTirtho/spotube/commit/2b35c044adb15a97a58692e7880694a251899732))
* pop sheet list not scrollable ([cca5625](https://github.com/KRTirtho/spotube/commit/cca5625df7e432da8581a4504306baad154deb48))
* re-enable add to queue and play next support, favorite button query exceptions ([e529c79](https://github.com/KRTirtho/spotube/commit/e529c79c4f0cd964b7d89e010d3fe51378ea7222))
* re-enable download manager ([ea45c4f](https://github.com/KRTirtho/spotube/commit/ea45c4f42ae89b8991e470e84a5290b3be3b0f36))
* remove unnecessary broadcast stream conversions ([bf04962](https://github.com/KRTirtho/spotube/commit/bf04962e90ea5345a2bc0d4793999f7db712cab2))
* remove useBreakpoints as it clogs up memory with unnecessary state updates ([e1c0f5c](https://github.com/KRTirtho/spotube/commit/e1c0f5cf1e4cece2c4aa235bfbf8511ad7b1fe59))
* replace download multiple pops and add translations ([4a21249](https://github.com/KRTirtho/spotube/commit/4a21249ee386426b0974451542a93e84f532fb3f))
* screen breakpoints and persist lyrics delay across screens ([df79638](https://github.com/KRTirtho/spotube/commit/df79638fb622a55aaa2b36c9a1425c2d9c4a8e52))
* sidebar task counter badge and bottom player play button progress color ([af278d8](https://github.com/KRTirtho/spotube/commit/af278d8feaa08c14528627766bce6d724b846954))
* status bar color of playlist/album page ([65fa3cb](https://github.com/KRTirtho/spotube/commit/65fa3cb624c240360de5a06778a1f72ad10bbe2d))
* system color scheme not persisting on restart when system color scheme changed ([e04515d](https://github.com/KRTirtho/spotube/commit/e04515d8e213b4c7f85d11385959a33b042bd9b1))
* track collection view status bar not transparent ([9251121](https://github.com/KRTirtho/spotube/commit/9251121ba0154599975e33819a43719477c644f8))
* track doesn't play after change ([17e5ab6](https://github.com/KRTirtho/spotube/commit/17e5ab611cc417cce7c17cafc7045f5aa2eb970e))
* track stops at last second ([f554f6d](https://github.com/KRTirtho/spotube/commit/f554f6d43bb714f662a27977f501d7ad44b070c3))
* **track_collection_view:** keyboard focus on scroll and no space for search results in playlist/album ([7a8bd92](https://github.com/KRTirtho/spotube/commit/7a8bd921047e3766dbbf24449e2873afe3dbecf8))
* track_table_view table headers ([d88d287](https://github.com/KRTirtho/spotube/commit/d88d287fc586ec33351de9f3b4359f189054868b))
* track_tile active and blacklist color, playbutton card action positioning ([3f5a1b9](https://github.com/KRTirtho/spotube/commit/3f5a1b9587efe9b7b2c69008345867933f79ec67))
* use id based source getters instead of index ([a074463](https://github.com/KRTirtho/spotube/commit/a0744630ba2dc713babdb8db6500f9dd0f1e6096))
### [2.7.1](https://github.com/KRTirtho/spotube/compare/v2.7.0...v2.7.1) (2023-04-10) ### [2.7.1](https://github.com/KRTirtho/spotube/compare/v2.7.0...v2.7.1) (2023-04-10)

View File

@ -118,26 +118,26 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt
Do the following: Do the following:
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support - Download the latest Flutter SDK (>=3.10.0) & enable desktop support
- Install Development dependencies in linux - Install Development dependencies in linux
- Debian/Ubuntu - Debian (>=12/Bookworm)/Ubuntu
```bash ```bash
$ apt-get install libjsoncpp1 libjsoncpp-dev libsecret-1-0 libsecret-1-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev
``` ```
- Use `libjsoncpp25` instead of `libjsoncpp1` (for Ubuntu >= 22.04) - Use `libjsoncpp25` instead of `libjsoncpp1` (for Ubuntu >= 22.04)
- Arch/Manjaro - Arch/Manjaro
```bash ```bash
yay -S libsecret jsoncpp gstreamer gst-libav gst-plugins-base gst-plugins-good yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify
``` ```
- Fedora - Fedora
```bash ```bash
dnf install libsecret libsecret-devel jsoncpp gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-doc gstreamer1-plugins-base-devel gstreamer1-plugins-good gstreamer1-plugins-good-extras dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel
``` ```
- Clone the Repo - Clone the Repo
- Create a `.env` in root of the project following the `.env.example` template - Create a `.env` in root of the project following the `.env.example` template
- Now run the following to bootstrap the project - Now run the following to bootstrap the project
```bash ```bash
flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs flutter pub get && dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
``` ```
- Finally run these following commands in the root of the project to start the Spotube Locally - Finally run these following commands in the root of the project to start the Spotube Locally
```bash ```bash

View File

@ -1,6 +1,6 @@
BSD-4-Clause License BSD-4-Clause License
Copyright (c) 2022 Kingkor Roy Tirtho. All rights reserved. Copyright (c) 2023 Kingkor Roy Tirtho. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

336
README.md
View File

@ -1,158 +1,214 @@
<p align="center"><img width="700" src="assets/spotube_banner.png" alt="Spotube Logo"></p> <div align="center">
<img width="600" src="assets/spotube_banner.png" alt="Spotube Logo">
<p align="center"> An open source, cross-platform Spotify client that doesn't require Premium nor uses Electron!<br />
<a href="https://spotube.netlify.app/">spotube.netlify.app</a>
</p>
<p align="center"> <a href="https://spotube.netlify.app"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
<a href="https://github.com/KRTirtho/spotube/actions/workflows/spotube-release-binary.yml"> <a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?color=1585be&style=flat-square">
</a>
<a href="https://github.com/KRTirtho/Spotube/releases">
<img alt="GitHub release" src="https://img.shields.io/github/v/release/KRTirtho/spotube?color=1585be&style=flat-square"/>
</a>
<a href="LICENSE">
<img alt="License" src="https://img.shields.io/aur/license/spotube-bin?color=1585be&style=flat-square"/>
</a>
<a href="https://github.com/KRTirtho">
<img alt="Maintainer" src="https://img.shields.io/badge/Maintainer-KRTirtho-1585be?style=flat-square"/>
</a>
<a href="https://opencollective.com/spotube">
<img alt="Open Collective backers and sponsors" src="https://img.shields.io/opencollective/all/spotube?color=1585be&style=flat-square"/>
</a>
<a href="https://discord.gg/uJ94vxB6vg">
<img alt="Discord" src="https://img.shields.io/discord/1012234096237350943?color=1585be&label=Discord%20Server&logoColor=1585be&style=flat-square">
</a>
</p>
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
<a href="https://www.buymeacoffee.com/krtirtho"><img alt="Buy me a Coffee" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg"></a>
Spotube is a [Flutter](https://flutter.dev) based lightweight Spotify client. It utilizes the power of Spotify & Youtube's public APIs to create a reliable, robust & resource-light user experience. <a href="https://opencollective.com/spotube"><img src="https://opencollective.com/spotube/donate/button.png?color=blue" alt="Donate to our Open Collective" height="45"></a>
#### <p align="center">Desktop</p> ---
![Application Desktop Screenshot](assets/spotube-screenshot.jpg) <img src="assets/spotube-screenshot.png" alt="Spotube Desktop" width="600"> <img src="assets/mobile-screenshots/player-view.jpg" alt="Spotube Mobile" height="340">
#### <p align="center">Mobile</p> </div>
![Application Mobile Screenshot](assets/mobile-screenshots/mobile-combined.jpg) ## 🌃 Features
<p align="center"> - 🚫 No ads, thanks to the use of public & free Spotify and YT Music APIs¹
<a href="https://discord.gg/uJ94vxB6vg"> - ⬇️ Downloadable tracks
<img src="https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2"> - 🖥️ 📱 Cross-platform support
</a> - 🪶 Small size & less data usage
</p> - 🕵️ Anonymous/guest login
- 🕒 Time synced lyrics
- ✋ No telemetry, diagnostics or user data collection
- 🚀 Native performance
- 📖 Open source/libre software
- 🔉 Playback control is done locally, not on the server
# Features **¹** It is still **recommended** to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify (or purchasing a Spotify Premium subscription too).
Following are the features that currently Spotube offers: ### ❌ Unsupported features
- Open source/libre software - 🗣️ **Spotify Shows & Podcasts:** Shows and Podcasts can **never be supported** because the audio tracks are _only_ available on Spotify and accessing them would require Spotify Premium.
- Anonymous/guest login - 🎧 **Spotify Listen Along:** [Coming soon!](https://github.com/KRTirtho/spotube/issues/8)
- Cross platform support
- No telemetry, diagnostics or user data collection
- Lightweight & resource-friendly
- Native performance (Thanks to Flutter+Skia)
- Playback control is done locally instead of on the server
- Small size & less data usage
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support their valuable creations.)
- Time synced lyrics
- Downloadable tracks
# Support development ## 📜 ⬇️ Installation guide
<a href="https://patreon.com/krtirtho"><img src="https://user-images.githubusercontent.com/61944859/180249027-678b01b8-c336-451e-b147-6d84a5b9d0e7.png" width="250"/></a> New releases usually appear after 3-4 months.<br />
[!["Donate to out Collective"](https://opencollective.com/spotube/donate/button.png?color=blue)](https://opencollective.com/spotube) This handy table lists all methods you can use to install Spotube:
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/krtirtho)
# Installation <table>
<tr>
<th>Platform</th>
<th>Package/Installation Method</th>
</tr>
<tr>
<td>Windows</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe">
<img width="220" alt="Windows Download" src="https://get.todoist.help/hc/article_attachments/4403191721234/WindowsButton.svg">
</a>
</tr>
<tr>
<td>MacOS</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-macos-universal.dmg">
<img width="220" alt="MacOS Download" src="https://reachify.io/wp-content/uploads/2018/09/mac-download-button-1.png">
</a>
</td>
</tr>
<tr>
<td>Android</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk">
<img width="220" alt="APK download" src="https://user-images.githubusercontent.com/114044633/223920025-83687de0-e463-4c5d-8122-e06e4bb7d40c.png">
</a>
<br/>
<a href="https://f-droid.org/packages/oss.krtirtho.spotube">
<img width="220" alt="Download from F-Droid" src="https://user-images.githubusercontent.com/61944859/174589876-bace24c0-b3fd-4c4a-bdb4-6fa82b5853ec.png">
</a>
</td>
</tr>
<tr>
<td>Flatpak</td>
<td>
<p><code>flatpak install com.github.KRTirtho.Spotube</code></p>
<a href="https://flathub.org/apps/details/com.github.KRTirtho.Spotube">
<img width="220" alt="Download on Flathub" src="https://flathub.org/assets/badges/flathub-badge-en.png">
</a>
</td>
</tr>
<tr>
<td>AppImage</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage">
<img width="220" alt="Download AppImage" src="https://user-images.githubusercontent.com/61944859/169455015-13385466-8901-48fe-ba90-b62d58b0be64.png">
</a>
<p><b>Note:</b> <a href="https://github.com/TheAssassin/AppImageLauncher">AppimageLauncher</a> is required!</p>
</td>
</tr>
<tr>
<td>Debian/Ubuntu</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.deb">
<img width="220" alt="Debian/Ubuntu Download" src="https://user-images.githubusercontent.com/61944859/169097994-e92aff78-fd75-4c93-b6e4-f072a4b5a7ed.png">
</a>
<p>Then run: <code>sudo apt install Spotube-linux-x86_64.deb</code></p>
</td>
</tr>
<tr>
<td>Arch/Manjaro</td>
<td>
<p>With pamac: <code>sudo pamac install spotube-bin</code></p>
<p>With yay: <code>yay -Sy spotube-bin</code></p>
</td>
</tr>
<tr>
<td>Fedora/OpenSuse</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.rpm">
<img width="220" alt="Fedora/OpenSuse Download" src="https://user-images.githubusercontent.com/61944859/223638350-5926b9da-04d6-4edd-931d-ad533e4ff058.png">
</a>
<p>For Fedora: <code>sudo dnf install ./Spotube-linux-x86_64.rpm</code></p>
<p>For OpenSuse: <code>sudo zypper in ./Spotube-linux-x86_64.rpm</code></p>
</td>
</tr>
<tr>
<td>Linux (tarball)</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest">
<img width="220" alt="Tarball Download" src="https://user-images.githubusercontent.com/61944859/169456985-e0ba1fd4-10e8-4cc0-ab94-337acc6e0295.png">
</a>
</td>
</tr>
<tr>
<td>Windows - <a href="https://chocolatey.org">Chocolatey</a></td>
<td>
<p><code>choco install spotube</code></p>
</td>
</tr>
<tr>
<td>Windows - <a href="https://scoop.sh">Scoop</a></td>
<td>
<p><code>scoop bucket add extras</code></p>
<p><code>scoop install spotube</code></p>
</td>
</tr>
<tr>
<td>Windows - <a href="https://github.com/microsoft/winget-cli">WinGet</a></td>
<td>
<p><code>winget install --id KRTirtho.Spotube</code></p>
</td>
</tr>
</table>
I'm always releasing newer versions of binaries of the software every 2-3 months with minor changes & every 6-8 months with major changes. Grab them! ### 🔄 Nightly Builds
| Platform | Package/Installation Method | Grab the latest nightly builds of Spotube [from the GitHub Releases](https://github.com/KRTirtho/spotube/releases/tag/nightly).
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Android | [<img width='220' alt='Android Download' src='https://user-images.githubusercontent.com/114044633/223920025-83687de0-e463-4c5d-8122-e06e4bb7d40c.png'/>][android-dlink]<br/>[<img width='220' alt='Android Download' src='https://user-images.githubusercontent.com/61944859/174589876-bace24c0-b3fd-4c4a-bdb4-6fa82b5853ec.png'/>][fdroid-dlink] |
| Debian/Ubuntu | [<img width='220' 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` |
| Fedora/OpenSuse | [<img width='220' alt='Linux Fedora/OpenSuse Download' src='https://user-images.githubusercontent.com/61944859/223638350-5926b9da-04d6-4edd-931d-ad533e4ff058.png'/>][rpm-dlink] <br/> For Fedora: `sudo dnf install ./Spotube-linux-x86_64.rpm`<br/> For OpenSuse: `sudo zypper in ./Spotube-linux-x86_64.rpm` |
| Flatpak | `flatpak install com.github.KRTirtho.Spotube` <br/> <a href='https://flathub.org/apps/details/com.github.KRTirtho.Spotube'><img width='220' 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='220' 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='220' alt='Tarball Download' src='https://user-images.githubusercontent.com/61944859/169456985-e0ba1fd4-10e8-4cc0-ab94-337acc6e0295.png'/>][linux-dlink] |
| Windows | [<img width='220' alt='Windows Download' src='https://get.todoist.help/hc/article_attachments/4403191721234/WindowsButton.svg'/>][win32-dlink] |
| Windows (<a href="https://chocolatey.org">Chocolatey</a>) | `choco install spotube` |
| Windows (<a href="https://scoop.sh/">Scoop</a>) | `scoop bucket add extras` <br/> `scoop install spotube` |
| Windows (<a href="https://github.com/microsoft/winget-cli">WinGet</a>) | `winget install --id KRTirtho.Spotube` |
| MacOS | [<img width='220' alt='MacOS Download' src='https://reachify.io/wp-content/uploads/2018/09/mac-download-button-1.png'/>][mac-dlink] |
> **Note!:** If you don't understand this download table. You can read [installation instructions][wiki-installation-instructions] from the wiki ## 🕳️ Building from source
## Nightly Builds <a href="https://github.com/KRTirtho/spotube/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?+label=Build%20Status"></a>
Get the latest nightly builds of Spotube [here](https://github.com/KRTirtho/spotube/releases/tag/nightly).
# TODO: You can compile Spotube's source code by [following these instructions](CONTRIBUTION.md#your-first-code-contribution).
- [ ] Windows OS Media Control & Media Keys Support
- [ ] Spotify Listen Along
- [x] Skip non-music sections from Audio Track
- [ ] Language Translations/Localization
# Building from source ## 👥 The Spotube team
You can find the details [here](CONTRIBUTION.md#your-first-code-contribution). - [Kingkor Roy Tirtho](https://github.com/KRTirtho) - The Founder, Maintainer and Lead Developer
- [Owen Connor](https://github.com/owencz1998) - The Cool Discord Moderator
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
- [RaptaG](https://github.com/RaptaG) - The GitHub Moderator and Community Manager
- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy
# Things that do not work ## 💼 License
- Shows & Podcasts are not supported, as a premium subscription would be needed for that functionality. Spotube is open source and licensed under the [BSD-4-Clause](/LICENSE) License.
# License If you are concerned, feel free to [read the reason of choosing this license](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p).
[BSD-4-Clause](/LICENSE) <details>
<summary>
<h2><code>[Click to show]</code> 🙏 Library/Plugin/Framework Credits</h2>
</summary>
But why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p). 1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
# Financial contributors 1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
## Backers 1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
![Backers](https://opencollective.com/spotube/backer.svg?button=false) 1. [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
![Donors](https://opencollective.com/spotube/tiers/donor.svg?button=false) 1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
## Sponsors 1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
<p align="center">
<img src="https://opencollective.com/spotube/tiers/sponsor.svg" alt="Sponsor Spotube" width="69%"> <!-- nice. -->
</p>
# Library/Plugin/Framework Credits
1. [Flutter](https://flutter.dev/) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
1. [Spotify API](https://developer.spotify.com/documentation/web-api/) - The Spotify Web API is a RESTful API that provides access to Spotify data
1. [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
1. [AUR](https://aur.archlinux.org/) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
1. [Flatpak](https://flatpak.org/) - Flatpak is a utility for software deployment and package management for Linux
1. [rentanadviser](https://www.rentanadviser.com/) - Generous Synced lyrics API provider service
1. [SponsorBlock](https://sponsor.ajay.app/) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan 1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
1. [F-Droid](https://f-droid.org/) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device 1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device
1. [adwaita](https://github.com/gtk-flutter/adwaita) - Adwaita style - The default theme for GTK+ for your Flutter app.
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. 1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library. 1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
1. [audio_service](https://github.com/ryanheise/audio_service/tree/master/audio_service) - Flutter plugin to play audio in the background while the screen is off. 1. [audio_service](https://github.com/ryanheise/audio_service/tree/master/audio_service) - Flutter plugin to play audio in the background while the screen is off.
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour. 1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
1. [audioplayers](https://github.com/bluefireteam/audioplayers) - A Flutter plugin to play multiple audio files simultaneously
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds. 1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
1. [badges](https://pub.dev/packages/badges) - A package for creating badges. Badges can be used for an additional marker for any widget, e.g. show a number of items in a shopping cart. 1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets. 1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons 1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop. 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration.
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. 1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter 1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter
1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter 1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
1. [fluent_ui](https://github.com/bdlukaa/fluent_ui) - Implements Windows UI in Flutter. Based on the official documentation 1. [fl_query_connectivity_plus_adapter](https://fl-query.vercel.app) - Connectivity Plus adapter for FlQuery Connectivity
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. 1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/master/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. 1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/master/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
1. [flutter_dotenv](https://github.com/java-james/flutter_dotenv) - Easily configure any flutter application with global variables using a `.env` file.
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability. 1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. 1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. 1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
1. [flutter_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable. 1. [flutter_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! 1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
@ -161,13 +217,16 @@ But why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sou
1. [hooks_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable. 1. [hooks_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser. 1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests. 1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities 1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package. 1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
1. [libadwaita](https://github.com/gtk-flutter/libadwaita) - Libadwaita's widgets for Flutter. Following Gnome HIG and available on all platforms.
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs. 1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
1. [macos_ui](https://macosui.dev) - Flutter widgets and themes implementing the current macOS design language. 1. [media_kit](https://github.com/alexmercerind/media_kit) - A complete video & audio playback library for Flutter & Dart. Performant, stable, feature-proof & modular.
1. [marquee](https://pub.dev/packages/marquee) - A Flutter widget that scrolls text infinitely. Provides many customizations including custom scroll directions, durations, curves as well as pauses after every round. 1. [media_kit_libs_android_audio](https://github.com/alexmercerind/media_kit.git) - Android package providing audio (only) native libraries for package:media_kit.
1. [media_kit_libs_ios_audio](https://github.com/alexmercerind/media_kit.git) - iOS package providing audio native libraries for package:media_kit.
1. [media_kit_libs_linux](https://github.com/alexmercerind/media_kit.git) - GNU/Linux dependency package for package:media_kit. Necessary for initialization.
1. [media_kit_libs_macos_audio](https://github.com/alexmercerind/media_kit.git) - macOS package providing audio native libraries for package:media_kit.
1. [media_kit_libs_windows_audio](https://github.com/alexmercerind/media_kit.git) - Windows package providing audio (only) native libraries for package:media_kit.
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
@ -175,54 +234,43 @@ But why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sou
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. 1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. 1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
1. [pocketbase](https://pub.dev/packages/pocketbase) - Multi-platform Dart SDK for PocketBase, an open source realtime backend in 1 file.
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
1. [queue](https://github.com/rknell/dart_queue) - Queue up futures from multiple sources and await their return anywhere in your code.
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter 1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. 1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community. 1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
1. [tuple](https://pub.dev/packages/tuple) - A library providing a tuple data structure. 1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. 1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
1. [uuid](https://github.com/Daegalus/dart-uuid) - RFC4122 (v1, v4, v5) UUID Generator and Parser for all Dart platforms (Web, VM, Flutter) 1. [uuid](https://github.com/Daegalus/dart-uuid) - RFC4122 (v1, v4, v5) UUID Generator and Parser for all Dart platforms (Web, VM, Flutter)
1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/ 1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/
1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. 1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. 1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
1. [supabase_flutter](https://supabase.com) - Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.
1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device.
1. [media_kit_native_event_loop](https://github.com/alexmercerind/media_kit) - Platform specific threaded event handling for media_kit. Enables support for higher number of concurrent instances.
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop.
1. [background_downloader](https://pub.dev/packages/background_downloader) - A multi-platform background file downloader and uploader. Define the task, enqueue and monitor progress
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
1. [flutter_distributor](https://github.com/leanflutter/flutter_distributor) - A complete tool for packaging and publishing your Flutter apps. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps.
1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs. 1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. 1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices. 1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class. 1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. 1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. 1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
1. [catcher](https://github.com/jhomlala/catcher) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. 1. [catcher](https://github.com/jhomlala/catcher) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
1. [platform_ui](https://github.com/KRTirtho/platform_ui) - Platform specific Widgets and UI toolkit 1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. 1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
# The team </details>
- [Kingkor Roy Tirtho](https://github.com/KRTirtho) - The Founder, Maintainer and Lead Developer <div align="center"><h4>© Copyright Spotube 2023</h4></div>
- [Owen Conor](https://github.com/owencz1998) - The Cool Discord Moderator
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
- [RaptaG](https://github.com/raptag) - The GitHub Moderator and Community Manager
- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy
# Social platforms
Follow me on [Twitter](https://twitter.com/@krtirtho), or join our amazing [Discord](https://discord.gg/uJ94vxB6vg) server to always get newer updates about the application.
<b><p align="center">&copy; 2023 Spotube</p></b>
<!-- 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
[rpm-dlink]: https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.rpm
[linux-dlink]: https://github.com/KRTirtho/spotube/releases/latest
[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
[fdroid-dlink]: https://f-droid.org/packages/oss.krtirtho.spotube/
[wiki-installation-instructions]: https://github.com/KRTirtho/spotube/wiki/Installation-Instrcutions

View File

@ -28,3 +28,7 @@ linter:
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options
analyzer:
enable-experiment:
- records
- patterns

View File

@ -34,6 +34,8 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 33 compileSdkVersion 33
ndkVersion "21.4.7075529"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -50,7 +52,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "oss.krtirtho.spotube" applicationId "oss.krtirtho.spotube"
minSdkVersion 19 minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@ -77,6 +79,17 @@ flutter {
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
}
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}
}
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
// other deps so just ignore
implementation 'com.android.support:multidex:2.0.1' implementation 'com.android.support:multidex:2.0.1'
} }

View File

@ -24,7 +24,7 @@
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
> >
<activity <activity
android:name="com.ryanheise.audioservice.AudioServiceActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
@ -49,17 +49,21 @@
</activity> </activity>
<service android:name="com.ryanheise.audioservice.AudioService" android:exported="false"> <!-- AudioService Config -->
<service android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- =================== -->
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" /> <item>
</item> --> <bitmap android:gravity="bottom" android:src="@drawable/branding"/>
</item>
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" /> <item>
</item> --> <bitmap android:gravity="bottom" android:src="@drawable/branding"/>
</item>
</layer-list> </layer-list>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenBrandingImage">@drawable/android12branding</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#000000</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenBrandingImage">@drawable/android12branding</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#000000</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --> Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.21' ext.kotlin_version = '1.8.22'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

BIN
assets/branding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

After

Width:  |  Height:  |  Size: 410 KiB

BIN
assets/spotube-logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

View File

@ -5,10 +5,11 @@ pkgbase = spotube-bin
url = https://github.com/KRTirtho/spotube/ url = https://github.com/KRTirtho/spotube/
arch = x86_64 arch = x86_64
license = BSD-4-Clause license = BSD-4-Clause
depends = gstreamer depends = mpv
depends = gst-libav depends = libappindicator-gtk3
depends = gst-plugins-base depends = libsecret
depends = gst-plugins-good depends = jsoncpp
depends = libnotify
source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz
md5sums = 8cd6a7385c5c75d203dccd762f1d63ec md5sums = 8cd6a7385c5c75d203dccd762f1d63ec

View File

@ -8,7 +8,7 @@ arch=(x86_64)
url="https://github.com/KRTirtho/spotube/" url="https://github.com/KRTirtho/spotube/"
license=('BSD-4-Clause') license=('BSD-4-Clause')
groups=() groups=()
depends=('gstreamer' 'gst-libav' 'gst-plugins-base' 'gst-plugins-good') depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify')
makedepends=() makedepends=()
checkdepends=() checkdepends=()
optdepends=() optdepends=()

View File

@ -0,0 +1,43 @@
import 'dart:convert';
import 'dart:io';
/// Generate JSON output for untranslated messages with English values
/// for quick translation in ChatGPT
///
/// Usage: dart bin/untranslated_messages.dart [locale?]
///
/// Example: dart bin/untranslated_messages.dart
///
/// or with specific locale (e.g. bn (Bengali))
///
/// Example: dart bin/untranslated_messages.dart bn
void main(List<String> args) {
final file = jsonDecode(
File('untranslated_messages.json').readAsStringSync(),
) as Map<String, dynamic>;
final englishMessages =
jsonDecode(File('lib/l10n/app_en.arb').readAsStringSync())
as Map<String, dynamic>;
final messagesWithValues = <String, dynamic>{};
for (final MapEntry(key: locale, value: messages) in file.entries) {
messagesWithValues[locale] = Map.fromEntries(
messages
.map(
(message) =>
MapEntry<String, dynamic>(message, englishMessages[message]),
)
.toList()
.cast<MapEntry<String, dynamic>>(),
);
}
print(
const JsonEncoder.withIndent(' ').convert(
args.isNotEmpty ? messagesWithValues[args.first] : messagesWithValues,
),
);
}

5
build.yaml Normal file
View File

@ -0,0 +1,5 @@
targets:
$default:
sources:
exclude:
- bin/*.dart

View File

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:spotube/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () {
testWidgets('check if app is successfully starting', (tester) async {
await app.main([]);
await tester.pumpAndSettle();
expect(find.byType(MaterialApp), findsOneWidget);
});
});
}

View File

@ -37,6 +37,7 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
# Just Audio Config
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)', '$(inherited)',

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "BrandingImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "BrandingImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "BrandingImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -16,13 +16,22 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</imageView> <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="BrandingImage" translatesAutoresizingMaskIntoConstraints="NO" id="Uyq-Kz-ftE"/>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstAttribute="bottom" secondItem="Uyq-Kz-ftE" secondAttribute="bottom" id="8Yb-q4-8bl"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="Uyq-Kz-ftE" firstAttribute="centerX" secondItem="YRO-k0-Ey4" secondAttribute="centerX" id="3kg-TC-cPP"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@ -32,6 +41,8 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="168" height="185"/> <image name="LaunchImage" width="509" height="509"/>
<image name="LaunchBackground" width="1" height="1"/>
<image name="BrandingImage" width="1" height="1"/>
</resources> </resources>
</document> </document>

View File

@ -1,56 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Sptube</string> <string>Sptube</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>spotube</string> <string>spotube</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true /> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true /> <true/>
<key>NSAllowsArbitraryLoadsForMedia</key> <key>NSAllowsArbitraryLoadsForMedia</key>
<true /> <true/>
</dict> </dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> <key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

4
l10n.yaml Normal file
View File

@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: untranslated_messages.json

View File

@ -30,16 +30,22 @@ class Assets {
static const AssetGenImage albumPlaceholder = static const AssetGenImage albumPlaceholder =
AssetGenImage('assets/album-placeholder.png'); AssetGenImage('assets/album-placeholder.png');
static const AssetGenImage bengaliPatternsBg =
AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
static const AssetGenImage placeholder = static const AssetGenImage placeholder =
AssetGenImage('assets/placeholder.png'); AssetGenImage('assets/placeholder.png');
static const AssetGenImage spotubeLogoForeground = static const AssetGenImage spotubeLogoForeground =
AssetGenImage('assets/spotube-logo-foreground.jpg'); AssetGenImage('assets/spotube-logo-foreground.jpg');
static const String spotubeLogoIco = 'assets/spotube-logo.ico';
static const AssetGenImage spotubeLogoPng = static const AssetGenImage spotubeLogoPng =
AssetGenImage('assets/spotube-logo.png'); AssetGenImage('assets/spotube-logo.png');
static const String spotubeLogoSvg = 'assets/spotube-logo.svg'; static const String spotubeLogoSvg = 'assets/spotube-logo.svg';
static const AssetGenImage spotubeLogoAndroid12 =
AssetGenImage('assets/spotube-logo_android12.png');
static const AssetGenImage spotubeScreenshot = static const AssetGenImage spotubeScreenshot =
AssetGenImage('assets/spotube-screenshot.jpg'); AssetGenImage('assets/spotube-screenshot.png');
static const AssetGenImage spotubeBanner = static const AssetGenImage spotubeBanner =
AssetGenImage('assets/spotube_banner.png'); AssetGenImage('assets/spotube_banner.png');
static const AssetGenImage success = AssetGenImage('assets/success.png'); static const AssetGenImage success = AssetGenImage('assets/success.png');
@ -50,11 +56,15 @@ class Assets {
/// List of all assets /// List of all assets
List<dynamic> get values => [ List<dynamic> get values => [
albumPlaceholder, albumPlaceholder,
bengaliPatternsBg,
branding,
emptyBox, emptyBox,
placeholder, placeholder,
spotubeLogoForeground, spotubeLogoForeground,
spotubeLogoIco,
spotubeLogoPng, spotubeLogoPng,
spotubeLogoSvg, spotubeLogoSvg,
spotubeLogoAndroid12,
spotubeScreenshot, spotubeScreenshot,
spotubeBanner, spotubeBanner,
success, success,

View File

@ -4,14 +4,11 @@ part 'env.g.dart';
@Envied(obfuscate: true, requireEnvFile: true, path: ".env") @Envied(obfuscate: true, requireEnvFile: true, path: ".env")
abstract class Env { abstract class Env {
@EnviedField(varName: 'POCKETBASE_URL', defaultValue: 'http://127.0.0.1:8090') @EnviedField(varName: 'SUPABASE_URL')
static final pocketbaseUrl = _Env.pocketbaseUrl; static final supabaseUrl = _Env.supabaseUrl;
@EnviedField(varName: 'USERNAME', defaultValue: 'root') @EnviedField(varName: 'SUPABASE_API_KEY')
static final username = _Env.username; static final supabaseAnonKey = _Env.supabaseAnonKey;
@EnviedField(varName: 'PASSWORD', defaultValue: '12345678')
static final password = _Env.password;
@EnviedField(varName: 'SPOTIFY_SECRETS') @EnviedField(varName: 'SPOTIFY_SECRETS')
static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) { static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) {
@ -21,4 +18,9 @@ abstract class Env {
"clientSecret": secrets.last, "clientSecret": secrets.last,
}; };
}).toList(); }).toList();
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
static final _enableUpdateChecker = _Env._enableUpdateChecker;
static bool get enableUpdateChecker => _enableUpdateChecker == "1";
} }

View File

@ -5,8 +5,8 @@ import 'package:go_router/go_router.dart';
import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -23,14 +23,11 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
if (PlayerControls.focusNode.canRequestFocus) { if (PlayerControls.focusNode.canRequestFocus) {
PlayerControls.focusNode.requestFocus(); PlayerControls.focusNode.requestFocus();
} }
final playlist = intent.ref.read(PlaylistQueueNotifier.provider);
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier); if (!audioPlayer.isPlaying) {
if (playlist == null) { await audioPlayer.resume();
return null;
} else if (!PlaylistQueueNotifier.isPlaying) {
await playlistNotifier.play();
} else { } else {
await playlistNotifier.pause(); await audioPlayer.pause();
} }
return null; return null;
} }
@ -93,9 +90,8 @@ class SeekIntent extends Intent {
class SeekAction extends Action<SeekIntent> { class SeekAction extends Action<SeekIntent> {
@override @override
invoke(intent) async { invoke(intent) async {
final playlist = intent.ref.read(PlaylistQueueNotifier.provider); final playlist = intent.ref.read(ProxyPlaylistNotifier.provider);
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier); if (playlist.isFetching) {
if (playlist == null || playlist.isLoading) {
DirectionalFocusAction().invoke( DirectionalFocusAction().invoke(
DirectionalFocusIntent( DirectionalFocusIntent(
intent.forward ? TraversalDirection.right : TraversalDirection.left, intent.forward ? TraversalDirection.right : TraversalDirection.left,
@ -103,9 +99,8 @@ class SeekAction extends Action<SeekIntent> {
); );
return null; return null;
} }
final position = final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
(await audioPlayer.getCurrentPosition() ?? Duration.zero).inSeconds; await audioPlayer.seek(
await playlistNotifier.seek(
Duration( Duration(
seconds: intent.forward ? position + 5 : position - 5, seconds: intent.forward ? position + 5 : position - 5,
), ),

View File

@ -0,0 +1,752 @@
class ISOLanguageName {
final String name;
final String nativeName;
const ISOLanguageName({
required this.name,
required this.nativeName,
});
}
// Uncomment the languages as we add support for them
// Currently supported: bn,en,fr,hi
abstract class LanguageLocals {
static final Map isoLangs = {
// "ab": const ISOLanguageName(
// name: "Abkhaz",
// nativeName: "аҧсуа",
// ),
// "aa": const ISOLanguageName(
// name: "Afar",
// nativeName: "Afaraf",
// ),
// "af": const ISOLanguageName(
// name: "Afrikaans",
// nativeName: "Afrikaans",
// ),
// "ak": const ISOLanguageName(
// name: "Akan",
// nativeName: "Akan",
// ),
// "sq": const ISOLanguageName(
// name: "Albanian",
// nativeName: "Shqip",
// ),
// "am": const ISOLanguageName(
// name: "Amharic",
// nativeName: "አማርኛ",
// ),
// "ar": const ISOLanguageName(
// name: "Arabic",
// nativeName: "العربية",
// ),
// "an": const ISOLanguageName(
// name: "Aragonese",
// nativeName: "Aragonés",
// ),
// "hy": const ISOLanguageName(
// name: "Armenian",
// nativeName: "Հայերեն",
// ),
// "as": const ISOLanguageName(
// name: "Assamese",
// nativeName: "অসমীয়া",
// ),
// "av": const ISOLanguageName(
// name: "Avaric",
// nativeName: "авар мацӀ, магӀарул мацӀ",
// ),
// "ae": const ISOLanguageName(
// name: "Avestan",
// nativeName: "avesta",
// ),
// "ay": const ISOLanguageName(
// name: "Aymara",
// nativeName: "aymar aru",
// ),
// "az": const ISOLanguageName(
// name: "Azerbaijani",
// nativeName: "azərbaycan dili",
// ),
// "bm": const ISOLanguageName(
// name: "Bambara",
// nativeName: "bamanankan",
// ),
// "ba": const ISOLanguageName(
// name: "Bashkir",
// nativeName: "башҡорт теле",
// ),
// "eu": const ISOLanguageName(
// name: "Basque",
// nativeName: "euskara,",
// ),
// "be": const ISOLanguageName(
// name: "Belarusian",
// nativeName: "Беларуская",
// ),
"bn": const ISOLanguageName(
name: "Bengali",
nativeName: "বাংলা",
),
// "bh": const ISOLanguageName(
// name: "Bihari",
// nativeName: "भोजपुरी",
// ),
// "bi": const ISOLanguageName(
// name: "Bislama",
// nativeName: "Bislama",
// ),
// "bs": const ISOLanguageName(
// name: "Bosnian",
// nativeName: "bosanski jezik",
// ),
// "br": const ISOLanguageName(
// name: "Breton",
// nativeName: "brezhoneg",
// ),
// "bg": const ISOLanguageName(
// name: "Bulgarian",
// nativeName: "български език",
// ),
// "my": const ISOLanguageName(
// name: "Burmese",
// nativeName: "ဗမာစာ",
// ),
// "ca": const ISOLanguageName(
// name: "Catalan; Valencian",
// nativeName: "Català",
// ),
// "ch": const ISOLanguageName(
// name: "Chamorro",
// nativeName: "Chamoru",
// ),
// "ce": const ISOLanguageName(
// name: "Chechen",
// nativeName: "нохчийн мотт",
// ),
// "ny": const ISOLanguageName(
// name: "Chichewa",
// nativeName: "chiCheŵa",
// ),
// "zh": const ISOLanguageName(
// name: "Chinese",
// nativeName: "汉语",
// ),
// "cv": const ISOLanguageName(
// name: "Chuvash",
// nativeName: "чӑваш чӗлхи",
// ),
// "kw": const ISOLanguageName(
// name: "Cornish",
// nativeName: "Kernewek",
// ),
// "co": const ISOLanguageName(
// name: "Corsican",
// nativeName: "lingua corsa",
// ),
// "cr": const ISOLanguageName(
// name: "Cree",
// nativeName: "ᓀᐦᐃᔭᐍᐏᐣ",
// ),
// "hr": const ISOLanguageName(
// name: "Croatian",
// nativeName: "hrvatski",
// ),
// "cs": const ISOLanguageName(
// name: "Czech",
// nativeName: "česky, čeština",
// ),
// "da": const ISOLanguageName(
// name: "Danish",
// nativeName: "dansk",
// ),
// "dv": const ISOLanguageName(
// name: "Maldivian;",
// nativeName: "ދިވެހި",
// ),
// "nl": const ISOLanguageName(
// name: "Dutch",
// nativeName: "Vlaams",
// ),
"en": const ISOLanguageName(
name: "English",
nativeName: "English",
),
// "eo": const ISOLanguageName(
// name: "Esperanto",
// nativeName: "Esperanto",
// ),
// "et": const ISOLanguageName(
// name: "Estonian",
// nativeName: "eesti",
// ),
// "ee": const ISOLanguageName(
// name: "Ewe",
// nativeName: "Eʋegbe",
// ),
// "fo": const ISOLanguageName(
// name: "Faroese",
// nativeName: "føroyskt",
// ),
// "fj": const ISOLanguageName(
// name: "Fijian",
// nativeName: "vosa Vakaviti",
// ),
// "fi": const ISOLanguageName(
// name: "Finnish",
// nativeName: "suomi",
// ),
"fr": const ISOLanguageName(
name: "French",
nativeName: "français",
),
// "ff": const ISOLanguageName(
// name: "Fula; Fulah; Pulaar; Pular",
// nativeName: "Fulfulde, Pulaar, Pular",
// ),
// "gl": const ISOLanguageName(
// name: "Galician",
// nativeName: "Galego",
// ),
// "ka": const ISOLanguageName(
// name: "Georgian",
// nativeName: "ქართული",
// ),
"de": const ISOLanguageName(
name: "German",
nativeName: "Deutsch",
),
// "el": const ISOLanguageName(
// name: "Greek, Modern",
// nativeName: "Ελληνικά",
// ),
// "gn": const ISOLanguageName(
// name: "Guaraní",
// nativeName: "Avañeẽ",
// ),
// "gu": const ISOLanguageName(
// name: "Gujarati",
// nativeName: "ગુજરાતી",
// ),
// "ht": const ISOLanguageName(
// name: "Haitian; Haitian Creole",
// nativeName: "Kreyòl ayisyen",
// ),
// "ha": const ISOLanguageName(
// name: "Hausa",
// nativeName: "Hausa, هَوُسَ",
// ),
// "he": const ISOLanguageName(
// name: "Hebrew (modern)",
// nativeName: "עברית",
// ),
// "hz": const ISOLanguageName(
// name: "Herero",
// nativeName: "Otjiherero",
// ),
"hi": const ISOLanguageName(
name: "Hindi",
nativeName: "हिन्दी, हिंदी",
),
// "ho": const ISOLanguageName(
// name: "Hiri Motu",
// nativeName: "Hiri Motu",
// ),
// "hu": const ISOLanguageName(
// name: "Hungarian",
// nativeName: "Magyar",
// ),
// "ia": const ISOLanguageName(
// name: "Interlingua",
// nativeName: "Interlingua",
// ),
// "id": const ISOLanguageName(
// name: "Indonesian",
// nativeName: "Bahasa Indonesia",
// ),
// "ie": const ISOLanguageName(
// name: "Interlingue",
// nativeName: "Occidental",
// ),
// "ga": const ISOLanguageName(
// name: "Irish",
// nativeName: "Gaeilge",
// ),
// "ig": const ISOLanguageName(
// name: "Igbo",
// nativeName: "Asụsụ Igbo",
// ),
// "ik": const ISOLanguageName(
// name: "Inupiaq",
// nativeName: "Iñupiaq, Iñupiatun",
// ),
// "io": const ISOLanguageName(
// name: "Ido",
// nativeName: "Ido",
// ),
// "is": const ISOLanguageName(
// name: "Icelandic",
// nativeName: "Íslenska",
// ),
// "it": const ISOLanguageName(
// name: "Italian",
// nativeName: "Italiano",
// ),
// "iu": const ISOLanguageName(
// name: "Inuktitut",
// nativeName: "ᐃᓄᒃᑎᑐᑦ",
// ),
"ja": const ISOLanguageName(
name: "Japanese",
nativeName: "日本語",
),
// "jv": const ISOLanguageName(
// name: "Javanese",
// nativeName: "basa Jawa",
// ),
// "kl": const ISOLanguageName(
// name: "Kalaallisut, Greenlandic",
// nativeName: "kalaallisut, kalaallit oqaasii",
// ),
// "kn": const ISOLanguageName(
// name: "Kannada",
// nativeName: "ಕನ್ನಡ",
// ),
// "kr": const ISOLanguageName(
// name: "Kanuri",
// nativeName: "Kanuri",
// ),
// "ks": const ISOLanguageName(
// name: "Kashmiri",
// nativeName: "कश्मीरी, كشميري‎",
// ),
// "kk": const ISOLanguageName(
// name: "Kazakh",
// nativeName: "Қазақ тілі",
// ),
// "km": const ISOLanguageName(
// name: "Khmer",
// nativeName: "ភាសាខ្មែរ",
// ),
// "ki": const ISOLanguageName(
// name: "Kikuyu, Gikuyu",
// nativeName: "Gĩkũyũ",
// ),
// "rw": const ISOLanguageName(
// name: "Kinyarwanda",
// nativeName: "Ikinyarwanda",
// ),
// "ky": const ISOLanguageName(
// name: "Kirghiz, Kyrgyz",
// nativeName: "кыргыз тили",
// ),
// "kv": const ISOLanguageName(
// name: "Komi",
// nativeName: "коми кыв",
// ),
// "kg": const ISOLanguageName(
// name: "Kongo",
// nativeName: "KiKongo",
// ),
// "ko": const ISOLanguageName(
// name: "Korean",
// nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
// ),
// "ku": const ISOLanguageName(
// name: "Kurdish",
// nativeName: "Kurdî, كوردی‎",
// ),
// "kj": const ISOLanguageName(
// name: "Kwanyama, Kuanyama",
// nativeName: "Kuanyama",
// ),
// "la": const ISOLanguageName(
// name: "Latin",
// nativeName: "latine, lingua latina",
// ),
// "lb": const ISOLanguageName(
// name: "Luxembourgish, Letzeburgesch",
// nativeName: "Lëtzebuergesch",
// ),
// "lg": const ISOLanguageName(
// name: "Luganda",
// nativeName: "Luganda",
// ),
// "li": const ISOLanguageName(
// name: "Limburgish, Limburgan, Limburger",
// nativeName: "Limburgs",
// ),
// "ln": const ISOLanguageName(
// name: "Lingala",
// nativeName: "Lingála",
// ),
// "lo": const ISOLanguageName(
// name: "Lao",
// nativeName: "ພາສາລາວ",
// ),
// "lt": const ISOLanguageName(
// name: "Lithuanian",
// nativeName: "lietuvių kalba",
// ),
// "lu": const ISOLanguageName(
// name: "Luba-Katanga",
// nativeName: "",
// ),
// "lv": const ISOLanguageName(
// name: "Latvian",
// nativeName: "latviešu valoda",
// ),
// "gv": const ISOLanguageName(
// name: "Manx",
// nativeName: "Gaelg, Gailck",
// ),
// "mk": const ISOLanguageName(
// name: "Macedonian",
// nativeName: "македонски јазик",
// ),
// "mg": const ISOLanguageName(
// name: "Malagasy",
// nativeName: "Malagasy fiteny",
// ),
// "ms": const ISOLanguageName(
// name: "Malay",
// nativeName: "bahasa Melayu, بهاس ملايو‎",
// ),
// "ml": const ISOLanguageName(
// name: "Malayalam",
// nativeName: "മലയാളം",
// ),
// "mt": const ISOLanguageName(
// name: "Maltese",
// nativeName: "Malti",
// ),
// "mi": const ISOLanguageName(
// name: "Māori",
// nativeName: "te reo Māori",
// ),
// "mr": const ISOLanguageName(
// name: "Marathi (Marāṭhī)",
// nativeName: "मराठी",
// ),
// "mh": const ISOLanguageName(
// name: "Marshallese",
// nativeName: "Kajin M̧ajeļ",
// ),
// "mn": const ISOLanguageName(
// name: "Mongolian",
// nativeName: "монгол",
// ),
// "na": const ISOLanguageName(
// name: "Nauru",
// nativeName: "Ekakairũ Naoero",
// ),
// "nv": const ISOLanguageName(
// name: "Navajo, Navaho",
// nativeName: "Diné bizaad, Dinékʼehǰí",
// ),
// "nb": const ISOLanguageName(
// name: "Norwegian Bokmål",
// nativeName: "Norsk bokmål",
// ),
// "nd": const ISOLanguageName(
// name: "North Ndebele",
// nativeName: "isiNdebele",
// ),
// "ne": const ISOLanguageName(
// name: "Nepali",
// nativeName: "नेपाली",
// ),
// "ng": const ISOLanguageName(
// name: "Ndonga",
// nativeName: "Owambo",
// ),
// "nn": const ISOLanguageName(
// name: "Norwegian Nynorsk",
// nativeName: "Norsk nynorsk",
// ),
// "no": const ISOLanguageName(
// name: "Norwegian",
// nativeName: "Norsk",
// ),
// "ii": const ISOLanguageName(
// name: "Nuosu",
// nativeName: "ꆈꌠ꒿ Nuosuhxop",
// ),
// "nr": const ISOLanguageName(
// name: "South Ndebele",
// nativeName: "isiNdebele",
// ),
// "oc": const ISOLanguageName(
// name: "Occitan",
// nativeName: "Occitan",
// ),
// "oj": const ISOLanguageName(
// name: "Ojibwe, Ojibwa",
// nativeName: "ᐊᓂᔑᓈᐯᒧᐎᓐ",
// ),
// "cu": const ISOLanguageName(
// name: "Old Church Slavonic",
// nativeName: "ѩзыкъ словѣньскъ",
// ),
// "om": const ISOLanguageName(
// name: "Oromo",
// nativeName: "Afaan Oromoo",
// ),
// "or": const ISOLanguageName(
// name: "Oriya",
// nativeName: "ଓଡ଼ିଆ",
// ),
// "os": const ISOLanguageName(
// name: "Ossetian, Ossetic",
// nativeName: "ирон æвзаг",
// ),
// "pa": const ISOLanguageName(
// name: "Panjabi, Punjabi",
// nativeName: "ਪੰਜਾਬੀ, پنجابی‎",
// ),
// "pi": const ISOLanguageName(
// name: "Pāli",
// nativeName: "पाऴि",
// ),
// "fa": const ISOLanguageName(
// name: "Persian",
// nativeName: "فارسی",
// ),
// "pl": const ISOLanguageName(
// name: "Polish",
// nativeName: "polski",
// ),
// "ps": const ISOLanguageName(
// name: "Pashto, Pushto",
// nativeName: "پښتو",
// ),
// "pt": const ISOLanguageName(
// name: "Portuguese",
// nativeName: "Português",
// ),
// "qu": const ISOLanguageName(
// name: "Quechua",
// nativeName: "Runa Simi, Kichwa",
// ),
// "rm": const ISOLanguageName(
// name: "Romansh",
// nativeName: "rumantsch grischun",
// ),
// "rn": const ISOLanguageName(
// name: "Kirundi",
// nativeName: "kiRundi",
// ),
// "ro": const ISOLanguageName(
// name: "Romanian, Moldavian, Moldovan",
// nativeName: "română",
// ),
// "ru": const ISOLanguageName(
// name: "Russian",
// nativeName: "русский язык",
// ),
// "sa": const ISOLanguageName(
// name: "Sanskrit (Saṁskṛta)",
// nativeName: "संस्कृतम्",
// ),
// "sc": const ISOLanguageName(
// name: "Sardinian",
// nativeName: "sardu",
// ),
// "sd": const ISOLanguageName(
// name: "Sindhi",
// nativeName: "सिन्धी, سنڌي، سندھی‎",
// ),
// "se": const ISOLanguageName(
// name: "Northern Sami",
// nativeName: "Davvisámegiella",
// ),
// "sm": const ISOLanguageName(
// name: "Samoan",
// nativeName: "gagana faa Samoa",
// ),
// "sg": const ISOLanguageName(
// name: "Sango",
// nativeName: "yângâ tî sängö",
// ),
// "sr": const ISOLanguageName(
// name: "Serbian",
// nativeName: "српски језик",
// ),
// "gd": const ISOLanguageName(
// name: "Scottish Gaelic; Gaelic",
// nativeName: "Gàidhlig",
// ),
// "sn": const ISOLanguageName(
// name: "Shona",
// nativeName: "chiShona",
// ),
// "si": const ISOLanguageName(
// name: "Sinhala, Sinhalese",
// nativeName: "සිංහල",
// ),
// "sk": const ISOLanguageName(
// name: "Slovak",
// nativeName: "slovenčina",
// ),
// "sl": const ISOLanguageName(
// name: "Slovene",
// nativeName: "slovenščina",
// ),
// "so": const ISOLanguageName(
// name: "Somali",
// nativeName: "Soomaaliga, af Soomaali",
// ),
// "st": const ISOLanguageName(
// name: "Southern Sotho",
// nativeName: "Sesotho",
// ),
// "es": const ISOLanguageName(
// name: "Spanish; Castilian",
// nativeName: "español, castellano",
// ),
// "su": const ISOLanguageName(
// name: "Sundanese",
// nativeName: "Basa Sunda",
// ),
// "sw": const ISOLanguageName(
// name: "Swahili",
// nativeName: "Kiswahili",
// ),
// "ss": const ISOLanguageName(
// name: "Swati",
// nativeName: "SiSwati",
// ),
// "sv": const ISOLanguageName(
// name: "Swedish",
// nativeName: "svenska",
// ),
// "ta": const ISOLanguageName(
// name: "Tamil",
// nativeName: "தமிழ்",
// ),
// "te": const ISOLanguageName(
// name: "Telugu",
// nativeName: "తెలుగు",
// ),
// "tg": const ISOLanguageName(
// name: "Tajik",
// nativeName: "тоҷикӣ, toğikī, تاجیکی‎",
// ),
// "th": const ISOLanguageName(
// name: "Thai",
// nativeName: "ไทย",
// ),
// "ti": const ISOLanguageName(
// name: "Tigrinya",
// nativeName: "ትግርኛ",
// ),
// "bo": const ISOLanguageName(
// name: "Tibetan Standard, Tibetan, Central",
// nativeName: "བོད་ཡིག",
// ),
// "tk": const ISOLanguageName(
// name: "Turkmen",
// nativeName: "Türkmen, Түркмен",
// ),
// "tl": const ISOLanguageName(
// name: "Tagalog",
// nativeName: "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔",
// ),
// "tn": const ISOLanguageName(
// name: "Tswana",
// nativeName: "Setswana",
// ),
// "to": const ISOLanguageName(
// name: "Tonga (Tonga Islands)",
// nativeName: "faka Tonga",
// ),
// "tr": const ISOLanguageName(
// name: "Turkish",
// nativeName: "Türkçe",
// ),
// "ts": const ISOLanguageName(
// name: "Tsonga",
// nativeName: "Xitsonga",
// ),
// "tt": const ISOLanguageName(
// name: "Tatar",
// nativeName: "татарча, tatarça, تاتارچا‎",
// ),
// "tw": const ISOLanguageName(
// name: "Twi",
// nativeName: "Twi",
// ),
// "ty": const ISOLanguageName(
// name: "Tahitian",
// nativeName: "Reo Tahiti",
// ),
// "ug": const ISOLanguageName(
// name: "Uighur, Uyghur",
// nativeName: "Uyƣurqə, ئۇيغۇرچە‎",
// ),
// "uk": const ISOLanguageName(
// name: "Ukrainian",
// nativeName: "українська",
// ),
// "ur": const ISOLanguageName(
// name: "Urdu",
// nativeName: "اردو",
// ),
// "uz": const ISOLanguageName(
// name: "Uzbek",
// nativeName: "zbek, Ўзбек, أۇزبېك‎",
// ),
// "ve": const ISOLanguageName(
// name: "Venda",
// nativeName: "Tshivenḓa",
// ),
// "vi": const ISOLanguageName(
// name: "Vietnamese",
// nativeName: "Tiếng Việt",
// ),
// "vo": const ISOLanguageName(
// name: "Volapük",
// nativeName: "Volapük",
// ),
// "wa": const ISOLanguageName(
// name: "Walloon",
// nativeName: "Walon",
// ),
// "cy": const ISOLanguageName(
// name: "Welsh",
// nativeName: "Cymraeg",
// ),
// "wo": const ISOLanguageName(
// name: "Wolof",
// nativeName: "Wollof",
// ),
// "fy": const ISOLanguageName(
// name: "Western Frisian",
// nativeName: "Frysk",
// ),
// "xh": const ISOLanguageName(
// name: "Xhosa",
// nativeName: "isiXhosa",
// ),
// "yi": const ISOLanguageName(
// name: "Yiddish",
// nativeName: "ייִדיש",
// ),
// "yo": const ISOLanguageName(
// name: "Yoruba",
// nativeName: "Yorùbá",
// ),
// "za": const ISOLanguageName(
// name: "Zhuang, Chuang",
// nativeName: "Saɯ cueŋƅ, Saw cuengh",
// )
};
static ISOLanguageName getDisplayLanguage(key) {
if (isoLangs.containsKey(key)) {
return isoLangs[key]!;
} else {
throw Exception("Language key incorrect");
}
}
}

View File

@ -3,9 +3,12 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/components/shared/spotube_page_route.dart';
import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/album/album.dart';
@ -20,6 +23,8 @@ import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart';
import '../pages/library/playlist_generate/playlist_generate_result.dart';
final rootNavigatorKey = Catcher.navigatorKey; final rootNavigatorKey = Catcher.navigatorKey;
final shellRouteNavigatorKey = GlobalKey<NavigatorState>(); final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter( final router = GoRouter(
@ -31,41 +36,63 @@ final router = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: "/", path: "/",
pageBuilder: (context, state) => SpotubePage(child: const HomePage()), pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
), ),
GoRoute( GoRoute(
path: "/search", path: "/search",
name: "Search", name: "Search",
pageBuilder: (context, state) => pageBuilder: (context, state) =>
SpotubePage(child: const SearchPage()), const SpotubePage(child: SearchPage()),
), ),
GoRoute( GoRoute(
path: "/library", path: "/library",
name: "Library", name: "Library",
pageBuilder: (context, state) => pageBuilder: (context, state) =>
SpotubePage(child: const LibraryPage()), const SpotubePage(child: LibraryPage()),
), routes: [
GoRoute(
path: "generate",
pageBuilder: (context, state) =>
const SpotubePage(child: PlaylistGeneratorPage()),
routes: [
GoRoute(
path: "result",
pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage(
state:
state.extra as PlaylistGenerateResultRouteState,
),
),
),
]),
]),
GoRoute( GoRoute(
path: "/lyrics", path: "/lyrics",
name: "Lyrics", name: "Lyrics",
pageBuilder: (context, state) => pageBuilder: (context, state) =>
SpotubePage(child: const LyricsPage()), const SpotubePage(child: LyricsPage()),
), ),
GoRoute( GoRoute(
path: "/settings", path: "/settings",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => const SpotubePage(
child: const SettingsPage(), child: SettingsPage(),
), ),
routes: [ routes: [
GoRoute( GoRoute(
path: "blacklist", path: "blacklist",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => SpotubeSlidePage(
child: const BlackListPage(), child: const BlackListPage(),
), ),
), ),
GoRoute(
path: "logs",
pageBuilder: (context, state) => SpotubeSlidePage(
child: const LogsPage(),
),
),
GoRoute( GoRoute(
path: "about", path: "about",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => SpotubeSlidePage(
child: const AboutSpotube(), child: const AboutSpotube(),
), ),
), ),
@ -96,6 +123,13 @@ final router = GoRouter(
), ),
], ],
), ),
GoRoute(
path: "/mini-player",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: MiniLyricsPage(),
),
),
GoRoute( GoRoute(
path: "/login", path: "/login",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
@ -106,16 +140,16 @@ final router = GoRouter(
GoRoute( GoRoute(
path: "/login-tutorial", path: "/login-tutorial",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => const SpotubePage(
child: const LoginTutorial(), child: LoginTutorial(),
), ),
), ),
GoRoute( GoRoute(
path: "/player", path: "/player",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) { pageBuilder: (context, state) {
return SpotubePage( return const SpotubePage(
child: const PlayerView(), child: PlayerView(),
); );
}, },
), ),

View File

@ -1,22 +1,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SideBarTiles { class SideBarTiles {
final IconData icon; final IconData icon;
final String title; final String title;
SideBarTiles({required this.icon, required this.title}); final String id;
SideBarTiles({required this.icon, required this.title, required this.id});
} }
List<SideBarTiles> sidebarTileList = [ List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
SideBarTiles(icon: SpotubeIcons.home, title: "Browse"), SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
SideBarTiles(icon: SpotubeIcons.search, title: "Search"), SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
SideBarTiles(icon: SpotubeIcons.library, title: "Library"), SideBarTiles(
SideBarTiles(icon: SpotubeIcons.music, title: "Lyrics") id: "library", icon: SpotubeIcons.library, title: l10n.library),
]; SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics),
];
List<SideBarTiles> navbarTileList = [ List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
SideBarTiles(icon: SpotubeIcons.home, title: "Browse"), SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
SideBarTiles(icon: SpotubeIcons.search, title: "Search"), SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
SideBarTiles(icon: SpotubeIcons.library, title: "Library"), SideBarTiles(
SideBarTiles(icon: SpotubeIcons.settings, title: "Settings") id: "library",
]; icon: SpotubeIcons.library,
title: l10n.library,
),
SideBarTiles(
id: "settings",
icon: SpotubeIcons.settings,
title: l10n.settings,
)
];

View File

@ -1,188 +1,187 @@
// Country Codes contributed by momobobe <https://github.com/momobobe> // Country Codes contributed by momobobe <https://github.com/momobobe>
final spotifyMarkets = [ final spotifyMarkets = [
["AL", "Albania (AL)"], ("AL", "Albania (AL)"),
["DZ", "Algeria (DZ)"], ("DZ", "Algeria (DZ)"),
["AD", "Andorra (AD)"], ("AD", "Andorra (AD)"),
["AO", "Angola (AO)"], ("AO", "Angola (AO)"),
["AG", "Antigua and Barbuda (AG)"], ("AG", "Antigua and Barbuda (AG)"),
["AR", "Argentina (AR)"], ("AR", "Argentina (AR)"),
["AM", "Armenia (AM)"], ("AM", "Armenia (AM)"),
["AU", "Australia (AU)"], ("AU", "Australia (AU)"),
["AT", "Austria (AT)"], ("AT", "Austria (AT)"),
["AZ", "Azerbaijan (AZ)"], ("AZ", "Azerbaijan (AZ)"),
["BH", "Bahrain (BH)"], ("BH", "Bahrain (BH)"),
["BD", "Bangladesh (BD)"], ("BD", "Bangladesh (BD)"),
["BB", "Barbados (BB)"], ("BB", "Barbados (BB)"),
["BY", "Belarus (BY)"], ("BY", "Belarus (BY)"),
["BE", "Belgium (BE)"], ("BE", "Belgium (BE)"),
["BZ", "Belize (BZ)"], ("BZ", "Belize (BZ)"),
["BJ", "Benin (BJ)"], ("BJ", "Benin (BJ)"),
["BT", "Bhutan (BT)"], ("BT", "Bhutan (BT)"),
["BO", "Bolivia (BO)"], ("BO", "Bolivia (BO)"),
["BA", "Bosnia and Herzegovina (BA)"], ("BA", "Bosnia and Herzegovina (BA)"),
["BW", "Botswana (BW)"], ("BW", "Botswana (BW)"),
["BR", "Brazil (BR)"], ("BR", "Brazil (BR)"),
["BN", "Brunei Darussalam (BN)"], ("BN", "Brunei Darussalam (BN)"),
["BG", "Bulgaria (BG)"], ("BG", "Bulgaria (BG)"),
["BF", "Burkina Faso (BF)"], ("BF", "Burkina Faso (BF)"),
["BI", "Burundi (BI)"], ("BI", "Burundi (BI)"),
["CV", "Cabo Verde / Cape Verde (CV)"], ("CV", "Cabo Verde / Cape Verde (CV)"),
["KH", "Cambodia (KH)"], ("KH", "Cambodia (KH)"),
["CM", "Cameroon (CM)"], ("CM", "Cameroon (CM)"),
["CA", "Canada (CA)"], ("CA", "Canada (CA)"),
["TD", "Chad (TD)"], ("TD", "Chad (TD)"),
["CL", "Chile (CL)"], ("CL", "Chile (CL)"),
["CO", "Colombia (CO)"], ("CO", "Colombia (CO)"),
["KM", "Comoros (KM)"], ("KM", "Comoros (KM)"),
["CR", "Costa Rica (CR)"], ("CR", "Costa Rica (CR)"),
["HR", "Croatia (HR)"], ("HR", "Croatia (HR)"),
["CW", "Curaçao (CW)"], ("CW", "Curaçao (CW)"),
["CY", "Cyprus (CY)"], ("CY", "Cyprus (CY)"),
["CZ", "Czech Republic (CZ)"], ("CZ", "Czech Republic (CZ)"),
["CI", "Côte d'Ivoire / Ivory Coast (CI)"], ("CI", "Ivory Coast (CI)"),
["CD", "Democratic Republic of the Congo (CD)"], ("CD", "Congo (CD)"),
["DK", "Denmark (DK)"], ("DK", "Denmark (DK)"),
["DJ", "Djibouti (DJ)"], ("DJ", "Djibouti (DJ)"),
["DM", "Dominica (DM)"], ("DM", "Dominica (DM)"),
["DO", "Dominican Republic (DO)"], ("DO", "Dominican Republic (DO)"),
["EC", "Ecuador (EC)"], ("EC", "Ecuador (EC)"),
["EG", "Egypt (EG)"], ("EG", "Egypt (EG)"),
["SV", "El Salvador (SV)"], ("SV", "El Salvador (SV)"),
["GQ", "Equatorial Guinea (GQ)"], ("GQ", "Equatorial Guinea (GQ)"),
["EE", "Estonia (EE)"], ("EE", "Estonia (EE)"),
["SZ", "Eswatini (SZ)"], ("SZ", "Eswatini (SZ)"),
["FJ", "Fiji (FJ)"], ("FJ", "Fiji (FJ)"),
["FI", "Finland (FI)"], ("FI", "Finland (FI)"),
["FR", "France (FR)"], ("FR", "France (FR)"),
["GA", "Gabon (GA)"], ("GA", "Gabon (GA)"),
["GE", "Georgia (GE)"], ("GE", "Georgia (GE)"),
["DE", "Germany (DE)"], ("DE", "Germany (DE)"),
["GH", "Ghana (GH)"], ("GH", "Ghana (GH)"),
["GR", "Greece (GR)"], ("GR", "Greece (GR)"),
["GD", "Grenada (GD)"], ("GD", "Grenada (GD)"),
["GT", "Guatemala (GT)"], ("GT", "Guatemala (GT)"),
["GN", "Guinea (GN)"], ("GN", "Guinea (GN)"),
["GW", "Guinea-Bissau (GW)"], ("GW", "Guinea-Bissau (GW)"),
["GY", "Guyana (GY)"], ("GY", "Guyana (GY)"),
["HT", "Haiti (HT)"], ("HT", "Haiti (HT)"),
["HN", "Honduras (HN)"], ("HN", "Honduras (HN)"),
["HK", "Hong Kong (HK)"], ("HK", "Hong Kong (HK)"),
["HU", "Hungary (HU)"], ("HU", "Hungary (HU)"),
["IS", "Iceland (IS)"], ("IS", "Iceland (IS)"),
["IN", "India (IN)"], ("IN", "India (IN)"),
["ID", "Indonesia (ID)"], ("ID", "Indonesia (ID)"),
["IQ", "Iraq (IQ)"], ("IQ", "Iraq (IQ)"),
["IE", "Ireland (IE)"], ("IE", "Ireland (IE)"),
["IL", "Israel (IL)"], ("IL", "Israel (IL)"),
["IT", "Italy (IT)"], ("IT", "Italy (IT)"),
["JM", "Jamaica (JM)"], ("JM", "Jamaica (JM)"),
["JP", "Japan (JP)"], ("JP", "Japan (JP)"),
["JO", "Jordan (JO)"], ("JO", "Jordan (JO)"),
["KZ", "Kazakhstan (KZ)"], ("KZ", "Kazakhstan (KZ)"),
["KE", "Kenya (KE)"], ("KE", "Kenya (KE)"),
["KI", "Kiribati (KI)"], ("KI", "Kiribati (KI)"),
["XK", "Kosovo (XK)"], ("XK", "Kosovo (XK)"),
["KW", "Kuwait (KW)"], ("KW", "Kuwait (KW)"),
["KG", "Kyrgyzstan (KG)"], ("KG", "Kyrgyzstan (KG)"),
["LA", "Laos (LA)"], ("LA", "Laos (LA)"),
["LV", "Latvia (LV)"], ("LV", "Latvia (LV)"),
["LB", "Lebanon (LB)"], ("LB", "Lebanon (LB)"),
["LS", "Lesotho (LS)"], ("LS", "Lesotho (LS)"),
["LR", "Liberia (LR)"], ("LR", "Liberia (LR)"),
["LY", "Libya (LY)"], ("LY", "Libya (LY)"),
["LI", "Liechtenstein (LI)"], ("LI", "Liechtenstein (LI)"),
["LT", "Lithuania (LT)"], ("LT", "Lithuania (LT)"),
["LU", "Luxembourg (LU)"], ("LU", "Luxembourg (LU)"),
["MO", "Macao / Macau (MO)"], ("MO", "Macao / Macau (MO)"),
["MG", "Madagascar (MG)"], ("MG", "Madagascar (MG)"),
["MW", "Malawi (MW)"], ("MW", "Malawi (MW)"),
["MY", "Malaysia (MY)"], ("MY", "Malaysia (MY)"),
["MV", "Maldives (MV)"], ("MV", "Maldives (MV)"),
["ML", "Mali (ML)"], ("ML", "Mali (ML)"),
["MT", "Malta (MT)"], ("MT", "Malta (MT)"),
["MH", "Marshall Islands (MH)"], ("MH", "Marshall Islands (MH)"),
["MR", "Mauritania (MR)"], ("MR", "Mauritania (MR)"),
["MU", "Mauritius (MU)"], ("MU", "Mauritius (MU)"),
["MX", "Mexico (MX)"], ("MX", "Mexico (MX)"),
["FM", "Micronesia (FM)"], ("FM", "Micronesia (FM)"),
["MD", "Moldova (MD)"], ("MD", "Moldova (MD)"),
["MC", "Monaco (MC)"], ("MC", "Monaco (MC)"),
["MN", "Mongolia (MN)"], ("MN", "Mongolia (MN)"),
["ME", "Montenegro (ME)"], ("ME", "Montenegro (ME)"),
["MA", "Morocco (MA)"], ("MA", "Morocco (MA)"),
["MZ", "Mozambique (MZ)"], ("MZ", "Mozambique (MZ)"),
["NA", "Namibia (NA)"], ("NA", "Namibia (NA)"),
["NR", "Nauru (NR)"], ("NR", "Nauru (NR)"),
["NP", "Nepal (NP)"], ("NP", "Nepal (NP)"),
["NL", "Netherlands (NL)"], ("NL", "Netherlands (NL)"),
["NZ", "New Zealand (NZ)"], ("NZ", "New Zealand (NZ)"),
["NI", "Nicaragua (NI)"], ("NI", "Nicaragua (NI)"),
["NE", "Niger (NE)"], ("NE", "Niger (NE)"),
["NG", "Nigeria (NG)"], ("NG", "Nigeria (NG)"),
["MK", "North Macedonia (MK)"], ("MK", "North Macedonia (MK)"),
["NO", "Norway (NO)"], ("NO", "Norway (NO)"),
["OM", "Oman (OM)"], ("OM", "Oman (OM)"),
["PK", "Pakistan (PK)"], ("PK", "Pakistan (PK)"),
["PW", "Palau (PW)"], ("PW", "Palau (PW)"),
["PS", "Palestine (PS)"], ("PS", "Palestine (PS)"),
["PA", "Panama (PA)"], ("PA", "Panama (PA)"),
["PG", "Papua New Guinea (PG)"], ("PG", "Papua New Guinea (PG)"),
["PY", "Paraguay (PY)"], ("PY", "Paraguay (PY)"),
["PE", "Peru (PE)"], ("PE", "Peru (PE)"),
["PH", "Philippines (PH)"], ("PH", "Philippines (PH)"),
["PL", "Poland (PL)"], ("PL", "Poland (PL)"),
["PT", "Portugal (PT)"], ("PT", "Portugal (PT)"),
["QA", "Qatar (QA)"], ("QA", "Qatar (QA)"),
["CG", "Republic of the Congo (CG)"], ("CG", "Congo (CG)"),
["RO", "Romania (RO)"], ("RO", "Romania (RO)"),
["RU", "Russia (RU)"], ("RU", "Russia (RU)"),
["RW", "Rwanda (RW)"], ("RW", "Rwanda (RW)"),
["WS", "Samoa (WS)"], ("WS", "Samoa (WS)"),
["SM", "San Marino (SM)"], ("SM", "San Marino (SM)"),
["SA", "Saudi Arabia (SA)"], ("SA", "Saudi Arabia (SA)"),
["SN", "Senegal (SN)"], ("SN", "Senegal (SN)"),
["RS", "Serbia (RS)"], ("RS", "Serbia (RS)"),
["SC", "Seychelles (SC)"], ("SC", "Seychelles (SC)"),
["SL", "Sierra Leone (SL)"], ("SL", "Sierra Leone (SL)"),
["SG", "Singapore (SG)"], ("SG", "Singapore (SG)"),
["SK", "Slovakia (SK)"], ("SK", "Slovakia (SK)"),
["SI", "Slovenia (SI)"], ("SI", "Slovenia (SI)"),
["SB", "Solomon Islands (SB)"], ("SB", "Solomon Islands (SB)"),
["ZA", "South Africa (ZA)"], ("ZA", "South Africa (ZA)"),
["KR", "South Korea (KR)"], ("KR", "South Korea (KR)"),
["ES", "Spain (ES)"], ("ES", "Spain (ES)"),
["LK", "Sri Lanka (LK)"], ("LK", "Sri Lanka (LK)"),
["VC", "St Vincent and the Grenadines (VC)"], ("KN", "St. Kitts and Nevis (KN)"),
["KN", "St. Kitts and Nevis (KN)"], ("LC", "St. Lucia (LC)"),
["LC", "St. Lucia (LC)"], ("SR", "Suriname (SR)"),
["SR", "Suriname (SR)"], ("SE", "Sweden (SE)"),
["SE", "Sweden (SE)"], ("CH", "Switzerland (CH)"),
["CH", "Switzerland (CH)"], ("ST", "São Tomé and Príncipe (ST)"),
["ST", "São Tomé and Príncipe (ST)"], ("TW", "Taiwan (TW)"),
["TW", "Taiwan (TW)"], ("TJ", "Tajikistan (TJ)"),
["TJ", "Tajikistan (TJ)"], ("TZ", "Tanzania (TZ)"),
["TZ", "Tanzania (TZ)"], ("TH", "Thailand (TH)"),
["TH", "Thailand (TH)"], ("BS", "The Bahamas (BS)"),
["BS", "The Bahamas (BS)"], ("GM", "The Gambia (GM)"),
["GM", "The Gambia (GM)"], ("TL", "East Timor (TL)"),
["TL", "Timor-Leste / East Timor (TL)"], ("TG", "Togo (TG)"),
["TG", "Togo (TG)"], ("TO", "Tonga (TO)"),
["TO", "Tonga (TO)"], ("TT", "Trinidad and Tobago (TT)"),
["TT", "Trinidad and Tobago (TT)"], ("TN", "Tunisia (TN)"),
["TN", "Tunisia (TN)"], ("TR", "Turkey (TR)"),
["TR", "Turkey (TR)"], ("TV", "Tuvalu (TV)"),
["TV", "Tuvalu (TV)"], ("UG", "Uganda (UG)"),
["UG", "Uganda (UG)"], ("UA", "Ukraine (UA)"),
["UA", "Ukraine (UA)"], ("AE", "United Arab Emirates (AE)"),
["AE", "United Arab Emirates (AE)"], ("GB", "United Kingdom (GB)"),
["GB", "United Kingdom (GB)"], ("US", "United States (US)"),
["US", "United States (US)"], ("UY", "Uruguay (UY)"),
["UY", "Uruguay (UY)"], ("UZ", "Uzbekistan (UZ)"),
["UZ", "Uzbekistan (UZ)"], ("VU", "Vanuatu (VU)"),
["VU", "Vanuatu (VU)"], ("VE", "Venezuela (VE)"),
["VE", "Venezuela (VE)"], ("VN", "Vietnam (VN)"),
["VN", "Vietnam (VN)"], ("ZM", "Zambia (ZM)"),
["ZM", "Zambia (ZM)"], ("ZW", "Zimbabwe (ZW)"),
["Z", "Zimbabwe (ZW)"],
]; ];

View File

@ -1,13 +1,13 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
abstract class SpotubeIcons { abstract class SpotubeIcons {
static const home = FluentIcons.home; static const home = FluentIcons.home_12_regular;
static const search = FeatherIcons.search; static const search = FeatherIcons.search;
static const library = FluentIcons.library; static const library = FluentIcons.library_16_regular;
static const music = FeatherIcons.music; static const music = FeatherIcons.music;
static const play = FluentIcons.play; static const play = FluentIcons.play_12_regular;
static const pause = FeatherIcons.pause; static const pause = FeatherIcons.pause;
static const skipForward = FeatherIcons.skipForward; static const skipForward = FeatherIcons.skipForward;
static const skipBack = FeatherIcons.skipBack; static const skipBack = FeatherIcons.skipBack;
@ -16,8 +16,8 @@ abstract class SpotubeIcons {
static const refresh = FeatherIcons.refreshCw; static const refresh = FeatherIcons.refreshCw;
static const settings = FeatherIcons.settings; static const settings = FeatherIcons.settings;
static const shuffle = FeatherIcons.shuffle; static const shuffle = FeatherIcons.shuffle;
static const repeat = FluentIcons.repeat_all; static const repeat = FluentIcons.arrow_repeat_all_16_regular;
static const repeatOne = FluentIcons.repeat_one; static const repeatOne = Icons.repeat_one_rounded;
static const remove = FeatherIcons.minus; static const remove = FeatherIcons.minus;
static const removeFilled = FeatherIcons.minusCircle; static const removeFilled = FeatherIcons.minusCircle;
static const add = FeatherIcons.plus; static const add = FeatherIcons.plus;
@ -67,4 +67,28 @@ abstract class SpotubeIcons {
static const genres = FeatherIcons.music; static const genres = FeatherIcons.music;
static const zoomIn = FeatherIcons.zoomIn; static const zoomIn = FeatherIcons.zoomIn;
static const zoomOut = FeatherIcons.zoomOut; static const zoomOut = FeatherIcons.zoomOut;
static const tray = FeatherIcons.chevronDown;
static const miniPlayer = Icons.picture_in_picture_rounded;
static const maximize = FeatherIcons.maximize2;
static const pinOn = Icons.push_pin_rounded;
static const pinOff = Icons.push_pin_outlined;
static const hoverOn = Icons.back_hand_rounded;
static const hoverOff = Icons.back_hand_outlined;
static const dragHandle = Icons.drag_indicator;
static const lightning = Icons.flash_on_rounded;
static const colorSync = FeatherIcons.activity;
static const language = FeatherIcons.globe;
static const error = FeatherIcons.alertTriangle;
static const piped = FeatherIcons.cloud;
static const magic = Icons.auto_fix_high_outlined;
static const selectionCheck = Icons.checklist_rounded;
static const volumeHigh = FeatherIcons.volume2;
static const volumeMedium = FeatherIcons.volume1;
static const volumeLow = FeatherIcons.volume;
static const volumeMute = FeatherIcons.volumeX;
static const timer = FeatherIcons.clock;
static const logs = FeatherIcons.fileText;
static const clipboard = FeatherIcons.clipboard;
static const youtube = FeatherIcons.youtube;
static const skip = FeatherIcons.fastForward;
} }

View File

@ -5,8 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -33,29 +34,24 @@ enum AlbumType {
class AlbumCard extends HookConsumerWidget { class AlbumCard extends HookConsumerWidget {
final Album album; final Album album;
final PlaybuttonCardViewType viewType;
const AlbumCard( const AlbumCard(
this.album, { this.album, {
Key? key, Key? key,
this.viewType = PlaybuttonCardViewType.square,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(PlaylistQueueNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playing = useStream(PlaylistQueueNotifier.playing).data ?? final playing =
PlaylistQueueNotifier.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final queryClient = useQueryClient(); final queryClient = useQueryClient();
final query = queryClient
.getQuery<List<TrackSimple>, dynamic>("album-tracks/${album.id}");
bool isPlaylistPlaying = useMemoized( bool isPlaylistPlaying = useMemoized(
() => () => playlist.containsCollection(album.id!),
playlistNotifier.isPlayingPlaylist(query?.data ?? album.tracks ?? []), [playlist, album.id],
[playlistNotifier, query?.data, album.tracks],
); );
final int marginH = final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
@ -65,30 +61,33 @@ class AlbumCard extends HookConsumerWidget {
album.images, album.images,
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
), ),
viewType: viewType,
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isPlaylistPlaying && playlist?.isLoading == true, isLoading: isPlaylistPlaying && playlist.isFetching == true,
title: album.name!, title: album.name!,
description: description:
"${AlbumType.from(album.albumType!).formatted}${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}", "${AlbumType.from(album.albumType!).formatted}${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
onTap: () { onTap: () {
ServiceUtils.navigate(context, "/album/${album.id}", extra: album); ServiceUtils.push(context, "/album/${album.id}", extra: album);
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {
updating.value = true; updating.value = true;
try { try {
if (isPlaylistPlaying && playing) { if (isPlaylistPlaying && playing) {
return playlistNotifier.pause(); return audioPlayer.pause();
} else if (isPlaylistPlaying && !playing) { } else if (isPlaylistPlaying && !playing) {
return playlistNotifier.resume(); return audioPlayer.resume();
} }
await playlistNotifier.loadAndPlay(album.tracks await playlistNotifier.load(
?.map((e) => album.tracks
TypeConversionUtils.simpleTrack_X_Track(e, album)) ?.map((e) =>
.toList() ?? TypeConversionUtils.simpleTrack_X_Track(e, album))
[]); .toList() ??
[],
autoPlay: true,
);
playlistNotifier.addCollection(album.id!);
} finally { } finally {
updating.value = false; updating.value = false;
} }
@ -117,16 +116,16 @@ class AlbumCard extends HookConsumerWidget {
); );
if (fetchedTracks == null || fetchedTracks.isEmpty) return; if (fetchedTracks == null || fetchedTracks.isEmpty) return;
playlistNotifier.add( playlistNotifier.addTracks(fetchedTracks);
fetchedTracks, playlistNotifier.addCollection(album.id!);
);
if (context.mounted) { if (context.mounted) {
final snackbar = SnackBar( final snackbar = SnackBar(
content: Text("Added ${album.tracks?.length} tracks to queue"), content: Text("Added ${album.tracks?.length} tracks to queue"),
action: SnackBarAction( action: SnackBarAction(
label: "Undo", label: "Undo",
onPressed: () { onPressed: () {
playlistNotifier.remove(fetchedTracks); playlistNotifier
.removeTracks(fetchedTracks.map((e) => e.id!));
}, },
), ),
); );

View File

@ -33,35 +33,36 @@ class ArtistAlbumList extends HookConsumerWidget {
? false ? false
: (albumsQuery.pages.last.items?.length ?? 0) == 5; : (albumsQuery.pages.last.items?.length ?? 0) == 5;
return SizedBox( return Column(
height: 300, children: [
child: ScrollConfiguration( ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith( behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: { dragDevices: {
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
}, },
), ),
child: Scrollbar( child: Scrollbar(
interactive: false, interactive: false,
controller: scrollController,
child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: albumsQuery.fetchNext, child: Waypoint(
child: ListView.builder(
itemCount: albums.length,
controller: scrollController, controller: scrollController,
scrollDirection: Axis.horizontal, onTouchEdge: albumsQuery.fetchNext,
itemBuilder: (context, index) { child: SingleChildScrollView(
if (index == albums.length - 1 && hasNextPage) { controller: scrollController,
return const ShimmerPlaybuttonCard(count: 1); scrollDirection: Axis.horizontal,
} child: Row(
return AlbumCard(albums[index]); crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
...albums.map((album) => AlbumCard(album)),
if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
],
),
),
), ),
), ),
), ),
), ],
); );
} }
} }

View File

@ -1,12 +1,12 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:fluent_ui/fluent_ui.dart' hide Colors;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/hover_builder.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_platform_property.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/hooks/use_brightness_value.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -17,6 +17,7 @@ class ArtistCard extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final backgroundImage = UniversalImage.imageProvider( final backgroundImage = UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString( TypeConversionUtils.image_X_UrlString(
artist.images, artist.images,
@ -30,87 +31,55 @@ class ArtistCard extends HookConsumerWidget {
), ),
), ),
); );
final boxShadow = usePlatformProperty<BoxShadow?>(
(context) => PlatformProperty( final radius = BorderRadius.circular(15);
android: BoxShadow(
blurRadius: 10, final double size = useBreakpointValue<double>(
offset: const Offset(0, 3), xs: 130,
spreadRadius: 5, sm: 130,
color: Theme.of(context).colorScheme.shadow, md: 150,
), others: 170,
ios: null,
macos: null,
linux: BoxShadow(
blurRadius: 6,
color: Theme.of(context).shadowColor.withOpacity(0.3),
),
windows: null,
),
); );
final splash = usePlatformProperty<InteractiveInkFeatureFactory?>( return Container(
(context) => PlatformProperty.only( width: size,
android: InkRipple.splashFactory, margin: const EdgeInsets.symmetric(vertical: 5),
other: NoSplash.splashFactory, child: Material(
), shadowColor: theme.colorScheme.background,
); color: Color.lerp(
theme.colorScheme.surfaceVariant,
return SizedBox( theme.colorScheme.surface,
height: 240, useBrightnessValue(.9, .7),
width: 200,
child: InkWell(
splashFactory: splash,
onTap: () {
ServiceUtils.navigate(context, "/artist/${artist.id}");
},
customBorder: platform == TargetPlatform.windows
? Border.all(
color: FluentTheme.maybeOf(context)
?.micaBackgroundColor
.withOpacity(.7) ??
Colors.transparent,
width: 1,
)
: null,
borderRadius: BorderRadius.circular(
platform == TargetPlatform.windows ? 5 : 8,
), ),
child: HoverBuilder(builder: (context, isHovering) { elevation: 3,
return Ink( shape: RoundedRectangleBorder(
width: 200, borderRadius: radius,
decoration: BoxDecoration( side: isBlackListed
color: PlatformTheme.of(context).secondaryBackgroundColor, ? const BorderSide(
borderRadius: BorderRadius.circular( color: Colors.red,
platform == TargetPlatform.windows ? 5 : 8, width: 2,
), )
boxShadow: [ : BorderSide.none,
if (boxShadow != null) boxShadow, ),
], child: InkWell(
border: isBlackListed onTap: () {
? Border.all( ServiceUtils.push(context, "/artist/${artist.id}");
color: Colors.red[400]!, },
width: 2, borderRadius: radius,
)
: [TargetPlatform.windows, TargetPlatform.macOS]
.contains(platform)
? Border.all(
color: PlatformTheme.of(context).borderColor ??
Colors.transparent,
width: 1,
)
: null,
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(12),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Stack( Stack(
children: [ children: [
CircleAvatar( ConstrainedBox(
maxRadius: 80, constraints: BoxConstraints(
minRadius: 20, maxHeight: size,
backgroundImage: backgroundImage, ),
child: CircleAvatar(
backgroundImage: backgroundImage,
radius: size / 2,
),
), ),
Positioned( Positioned(
right: 0, right: 0,
@ -122,9 +91,9 @@ class ArtistCard extends HookConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: Colors.blue,
borderRadius: BorderRadius.circular(50)), borderRadius: BorderRadius.circular(50)),
child: const Text( child: Text(
"Artist", context.l10n.artist,
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -134,19 +103,19 @@ class ArtistCard extends HookConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 10),
AutoSizeText( AutoSizeText(
artist.name!, artist.name!,
maxLines: 2, maxLines: 1,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: PlatformTextTheme.of(context).body?.copyWith( overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.bold, style: theme.textTheme.bodyMedium?.copyWith(
), fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
), )),
);
}),
), ),
); );
} }

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
class TokenLoginForm extends HookConsumerWidget { class TokenLoginForm extends HookConsumerWidget {
@ -25,27 +26,31 @@ class TokenLoginForm extends HookConsumerWidget {
), ),
child: Column( child: Column(
children: [ children: [
PlatformTextField( TextField(
controller: directCodeController, controller: directCodeController,
placeholder: "Spotify \"sp_dc\" Cookie", decoration: InputDecoration(
label: "sp_dc Cookie", hintText: context.l10n.spotify_cookie("\"sp_dc\""),
labelText: context.l10n.cookie_name_cookie("sp_dc"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
PlatformTextField( TextField(
controller: keyCodeController, controller: keyCodeController,
placeholder: "Spotify \"sp_key\" Cookie", decoration: InputDecoration(
label: "sp_key Cookie", hintText: context.l10n.spotify_cookie("\"sp_key\""),
labelText: context.l10n.cookie_name_cookie("sp_key"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
PlatformFilledButton( FilledButton(
onPressed: () async { onPressed: () async {
if (keyCodeController.text.isEmpty || if (keyCodeController.text.isEmpty ||
directCodeController.text.isEmpty) { directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: PlatformText("Please fill in all fields"), content: Text(context.l10n.fill_in_all_fields),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
@ -61,7 +66,7 @@ class TokenLoginForm extends HookConsumerWidget {
onDone?.call(); onDone?.call();
} }
}, },
child: const PlatformText("Submit"), child: Text(context.l10n.submit),
) )
], ],
), ),

View File

@ -1,12 +1,13 @@
import 'package:flutter/gestures.dart'; import 'dart:ui';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
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:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -27,58 +28,51 @@ class CategoryCard extends HookConsumerWidget {
category.id!, category.id!,
); );
final playlists = playlistQuery.pages if (playlistQuery.hasErrors && !playlistQuery.hasPageData) {
.expand( return const SizedBox.shrink();
(page) => page.items ?? const Iterable.empty(), }
) final playlists = playlistQuery.pages.expand(
.toList(); (page) {
return page.items?.where((i) => i != null) ?? const Iterable.empty();
return Column( },
children: [ ).toList();
Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Column(
children: [ mainAxisSize: MainAxisSize.min,
PlatformText.headline(category.name ?? "Unknown"), crossAxisAlignment: CrossAxisAlignment.start,
], children: [
Text(
category.name!,
style: Theme.of(context).textTheme.titleMedium,
), ),
), ScrollConfiguration(
playlistQuery.hasPageError && !playlistQuery.hasPageData behavior: ScrollConfiguration.of(context).copyWith(
? PlatformText( dragDevices: {
"Something Went Wrong\n${playlistQuery.errors.first}") PointerDeviceKind.touch,
: SizedBox( PointerDeviceKind.mouse,
height: 245, },
child: ScrollConfiguration( ),
behavior: ScrollConfiguration.of(context).copyWith( child: Waypoint(
dragDevices: { controller: scrollController,
PointerDeviceKind.touch, onTouchEdge: playlistQuery.fetchNext,
PointerDeviceKind.mouse, child: SingleChildScrollView(
}, scrollDirection: Axis.horizontal,
), controller: scrollController,
child: Scrollbar( padding: const EdgeInsets.symmetric(vertical: 8.0),
controller: scrollController, child: Row(
interactive: false, crossAxisAlignment: CrossAxisAlignment.start,
child: Waypoint( children: [
controller: scrollController, ...playlists.map((playlist) => PlaylistCard(playlist)),
onTouchEdge: () { if (playlistQuery.hasNextPage)
playlistQuery.fetchNext(); const ShimmerPlaybuttonCard(count: 1),
}, ],
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController,
children: [
...playlists
.map((playlist) => PlaylistCard(playlist)),
if (playlistQuery.hasNextPage)
const ShimmerPlaybuttonCard(count: 1),
],
),
),
),
), ),
), ),
], ),
),
],
),
); );
} }
} }

View File

@ -0,0 +1,272 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
class MultiSelectField<T> extends HookWidget {
final List<T> options;
final List<T> selectedOptions;
final Widget Function(T option, VoidCallback onSelect)? optionBuilder;
final Widget Function(T option)? selectedOptionBuilder;
final ValueChanged<List<T>> onSelected;
final Widget? dialogTitle;
final Object Function(T option) getValueForOption;
final Widget label;
final String? helperText;
final bool enabled;
const MultiSelectField({
Key? key,
required this.options,
required this.selectedOptions,
required this.getValueForOption,
required this.label,
this.optionBuilder,
this.selectedOptionBuilder,
required this.onSelected,
this.dialogTitle,
this.helperText,
this.enabled = true,
}) : super(key: key);
Widget defaultSelectedOptionBuilder(T option) {
return Chip(
label: Text(option.toString()),
onDeleted: () {
onSelected(
selectedOptions.where((e) => e != getValueForOption(option)).toList(),
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
MaterialButton(
elevation: 0,
focusElevation: 0,
hoverElevation: 0,
disabledElevation: 0,
highlightElevation: 0,
padding: const EdgeInsets.symmetric(vertical: 22),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(
color: enabled
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withOpacity(0.1),
),
),
mouseCursor: MaterialStateMouseCursor.textable,
onPressed: !enabled
? null
: () async {
final selected = await showDialog<List<T>>(
context: context,
builder: (context) {
return _MultiSelectDialog<T>(
dialogTitle: dialogTitle,
options: options,
getValueForOption: getValueForOption,
optionBuilder: optionBuilder,
initialSelection: selectedOptions,
helperText: helperText,
);
},
);
if (selected != null) {
onSelected(selected);
}
},
child: Container(
alignment: Alignment.centerLeft,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: DefaultTextStyle(
style: theme.textTheme.titleMedium!,
child: label,
),
),
),
if (helperText != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
helperText!,
style: Theme.of(context).textTheme.bodySmall,
),
),
Wrap(
children: [
...selectedOptions.map(
(option) => Padding(
padding: const EdgeInsets.all(4.0),
child: (selectedOptionBuilder ??
defaultSelectedOptionBuilder)(option),
),
),
],
)
],
);
}
}
class _MultiSelectDialog<T> extends HookWidget {
final Widget? dialogTitle;
final List<T> options;
final Widget Function(T option, VoidCallback onSelect)? optionBuilder;
final Object Function(T option) getValueForOption;
final List<T> initialSelection;
final String? helperText;
const _MultiSelectDialog({
Key? key,
required this.dialogTitle,
required this.options,
required this.getValueForOption,
this.optionBuilder,
this.initialSelection = const [],
this.helperText,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final selected = useState(initialSelection.map(getValueForOption));
final searchController = useTextEditingController();
// creates render update
useValueListenable(searchController);
final filteredOptions = useMemoized(
() {
if (searchController.text.isEmpty) {
return options;
}
return options
.map((e) => (
weightedRatio(
getValueForOption(e).toString(), searchController.text),
e
))
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList();
},
[searchController.text, options, getValueForOption],
);
Widget defaultOptionBuilder(T option, VoidCallback onSelect) {
final isSelected = selected.value.contains(getValueForOption(option));
return ChoiceChip(
label: Text("${!isSelected ? " " : ""}${option.toString()}"),
selected: isSelected,
side: BorderSide.none,
onSelected: (_) {
onSelect();
},
);
}
return AlertDialog(
scrollable: true,
title: dialogTitle ?? const Text('Select'),
contentPadding: mediaQuery.mdAndUp ? null : const EdgeInsets.all(16),
insetPadding: const EdgeInsets.all(16),
actions: [
OutlinedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(context.l10n.cancel),
),
ElevatedButton(
onPressed: () {
Navigator.pop(
context,
options
.where(
(option) =>
selected.value.contains(getValueForOption(option)),
)
.toList(),
);
},
child: Text(context.l10n.done),
),
],
content: SizedBox(
height: mediaQuery.size.height * 0.5,
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
autofocus: true,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search,
prefixIcon: const Icon(SpotubeIcons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
const SizedBox(height: 10),
Expanded(
child: SingleChildScrollView(
child: Wrap(
spacing: 5,
runSpacing: 5,
children: [
...filteredOptions.map(
(option) => Padding(
padding: const EdgeInsets.all(4.0),
child: (optionBuilder ?? defaultOptionBuilder)(
option,
() {
final value = getValueForOption(option);
if (selected.value.contains(value)) {
selected.value = selected.value
.where((e) => e != value)
.toList();
} else {
selected.value = [...selected.value, value];
}
},
),
),
),
],
),
),
),
if (helperText != null)
Text(
helperText!,
style: Theme.of(context).textTheme.labelMedium,
),
],
),
),
);
}
}

View File

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
typedef RecommendationAttribute = ({double min, double target, double max});
RecommendationAttribute lowValues(double base) =>
(min: 1 * base, target: 0.3 * base, max: 0.3 * base);
RecommendationAttribute moderateValues(double base) =>
(min: 0.5 * base, target: 1 * base, max: 0.5 * base);
RecommendationAttribute highValues(double base) =>
(min: 0.3 * base, target: 0.3 * base, max: 1 * base);
class RecommendationAttributeDials extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final double base;
const RecommendationAttributeDials({
Key? key,
required this.values,
required this.onChanged,
required this.title,
this.base = 1,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final animation = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500,
);
final minSlider = Row(
children: [
Text(context.l10n.min, style: labelStyle),
Expanded(
child: Slider.adaptive(
value: values.min / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: value * base,
target: values.target,
max: values.max,
)),
),
),
],
);
final targetSlider = Row(
children: [
Text(context.l10n.target, style: labelStyle),
Expanded(
child: Slider.adaptive(
value: values.target / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: value * base,
max: values.max,
)),
),
),
],
);
final maxSlider = Row(
children: [
Text(context.l10n.max, style: labelStyle),
Expanded(
child: Slider.adaptive(
value: values.max / base,
min: 0,
max: 1,
onChanged: (value) => onChanged((
min: values.min,
target: values.target,
max: value * base,
)),
),
),
],
);
return LayoutBuilder(builder: (context, constrain) {
return Card(
child: ExpansionTile(
title: DefaultTextStyle(
style: Theme.of(context).textTheme.titleSmall!,
child: title,
),
shape: const Border(),
leading: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.rotate(
angle: (animation.value * 3.14) / 2,
child: child,
);
},
child: const Icon(Icons.chevron_right),
),
trailing: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ToggleButtons(
borderRadius: BorderRadius.circular(8),
textStyle: labelStyle,
isSelected: [
values == lowValues(base),
values == moderateValues(base),
values == highValues(base),
],
onPressed: (index) {
RecommendationAttribute newValues = zeroValues;
switch (index) {
case 0:
newValues = lowValues(base);
break;
case 1:
newValues = moderateValues(base);
break;
case 2:
newValues = highValues(base);
break;
}
if (newValues == values) {
onChanged(zeroValues);
} else {
onChanged(newValues);
}
},
children: [
Text(context.l10n.low),
Text(" ${context.l10n.moderate} "),
Text(context.l10n.high),
],
),
),
onExpansionChanged: (value) {
if (value) {
animation.forward();
} else {
animation.reverse();
}
},
children: [
if (constrain.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minSlider),
Expanded(child: targetSlider),
Expanded(child: maxSlider),
],
)
else
Padding(
padding: const EdgeInsets.only(left: 16),
child: Column(
children: [
minSlider,
targetSlider,
maxSlider,
],
),
),
],
),
);
});
}
}

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
class RecommendationAttributeFields extends HookWidget {
final Widget title;
final RecommendationAttribute values;
final ValueChanged<RecommendationAttribute> onChanged;
final Map<String, RecommendationAttribute>? presets;
const RecommendationAttributeFields({
Key? key,
required this.values,
required this.onChanged,
required this.title,
this.presets,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final animation = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500,
);
final minController = useTextEditingController(text: values.min.toString());
final targetController =
useTextEditingController(text: values.target.toString());
final maxController = useTextEditingController(text: values.max.toString());
useEffect(() {
listener() {
onChanged((
min: double.tryParse(minController.text) ?? 0,
target: double.tryParse(targetController.text) ?? 0,
max: double.tryParse(maxController.text) ?? 0,
));
}
minController.addListener(listener);
targetController.addListener(listener);
maxController.addListener(listener);
return () {
minController.removeListener(listener);
targetController.removeListener(listener);
maxController.removeListener(listener);
};
}, [values]);
final minField = TextField(
controller: minController,
decoration: InputDecoration(
labelText: context.l10n.min,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
final targetField = TextField(
controller: targetController,
decoration: InputDecoration(
labelText: context.l10n.target,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
final maxField = TextField(
controller: maxController,
decoration: InputDecoration(
labelText: context.l10n.max,
isDense: true,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
);
return LayoutBuilder(builder: (context, constrain) {
return Card(
child: ExpansionTile(
title: DefaultTextStyle(
style: Theme.of(context).textTheme.titleSmall!,
child: title,
),
shape: const Border(),
leading: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.rotate(
angle: (animation.value * 3.14) / 2,
child: child,
);
},
child: const Icon(Icons.chevron_right),
),
trailing: presets == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ToggleButtons(
borderRadius: BorderRadius.circular(8),
textStyle: labelStyle,
isSelected: presets!.values
.map((value) => value == values)
.toList(),
onPressed: (index) {
RecommendationAttribute newValues =
presets!.values.elementAt(index);
if (newValues == values) {
onChanged(zeroValues);
minController.text = zeroValues.min.toString();
targetController.text = zeroValues.target.toString();
maxController.text = zeroValues.max.toString();
} else {
onChanged(newValues);
minController.text = newValues.min.toString();
targetController.text = newValues.target.toString();
maxController.text = newValues.max.toString();
}
},
children: presets!.keys.map((key) => Text(key)).toList(),
),
),
onExpansionChanged: (value) {
if (value) {
animation.forward();
} else {
animation.reverse();
}
},
children: [
const SizedBox(height: 8),
if (constrain.mdAndUp)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(width: 16),
Expanded(child: minField),
const SizedBox(width: 16),
Expanded(child: targetField),
const SizedBox(width: 16),
Expanded(child: maxField),
const SizedBox(width: 16),
],
)
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
minField,
const SizedBox(height: 16),
targetField,
const SizedBox(height: 16),
maxField,
],
),
),
const SizedBox(height: 8),
],
),
);
});
}
}

View File

@ -0,0 +1,144 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
enum SelectedItemDisplayType {
wrap,
list,
}
class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
final ValueNotifier<List<T>> seeds;
final FutureOr<List<T>> Function(TextEditingValue textEditingValue)
fetchSeeds;
final Widget Function(T option, ValueChanged<T> onSelected)
autocompleteOptionBuilder;
final Widget Function(T option) selectedSeedBuilder;
final String Function(T option) displayStringForOption;
final InputDecoration? inputDecoration;
final bool enabled;
final SelectedItemDisplayType selectedItemDisplayType;
const SeedsMultiAutocomplete({
Key? key,
required this.seeds,
required this.fetchSeeds,
required this.autocompleteOptionBuilder,
required this.displayStringForOption,
required this.selectedSeedBuilder,
this.inputDecoration,
this.enabled = true,
this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
useValueListenable(seeds);
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final seedController = useTextEditingController();
final containerKey = useRef(GlobalKey());
final box =
containerKey.value.currentContext?.findRenderObject() as RenderBox?;
final position = box?.localToGlobal(Offset.zero); //this is global position
final containerYPos = position?.dy ?? 0; //th
final containerHeight = box?.size.height ?? 0;
final listHeight = mediaQuery.size.height -
(containerYPos + containerHeight) -
// bottom player bar height
(mediaQuery.mdAndUp ? 80 : 0);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LayoutBuilder(builder: (context, constrains) {
return Container(
key: containerKey.value,
child: Autocomplete<T>(
optionsBuilder: (textEditingValue) async {
if (textEditingValue.text.isEmpty) return [];
return fetchSeeds(textEditingValue);
},
onSelected: (value) {
seeds.value = [...seeds.value, value];
seedController.clear();
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Container(
constraints: BoxConstraints(
maxWidth: constrains.maxWidth,
),
height: max(listHeight, 0),
child: Card(
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return autocompleteOptionBuilder(option, onSelected);
},
),
),
),
);
},
displayStringForOption: displayStringForOption,
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextFormField(
controller: seedController,
onChanged: (value) => textEditingController.text = value,
focusNode: focusNode,
onFieldSubmitted: (_) => onFieldSubmitted(),
enabled: enabled,
decoration: inputDecoration,
);
},
),
);
}),
const SizedBox(height: 8),
switch (selectedItemDisplayType) {
SelectedItemDisplayType.wrap => Wrap(
spacing: 4,
runSpacing: 4,
children: seeds.value.map(selectedSeedBuilder).toList(),
),
SelectedItemDisplayType.list => Card(
margin: EdgeInsets.zero,
child: Column(
children: [
for (final seed in seeds.value) ...[
selectedSeedBuilder(seed),
if (seeds.value.length > 1 && seed != seeds.value.last)
Divider(
color: theme.colorScheme.primaryContainer,
height: 1,
indent: 12,
endIndent: 12,
),
],
],
),
),
},
],
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class SimpleTrackTile extends HookWidget {
final Track track;
final VoidCallback? onDelete;
const SimpleTrackTile({
Key? key,
required this.track,
this.onDelete,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: UniversalImage(
path: TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.artist,
),
height: 40,
width: 40,
),
),
horizontalTitleGap: 10,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(track.name!),
trailing: onDelete == null
? null
: IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: onDelete,
),
subtitle: Text(
track.artists?.map((e) => e.name).join(", ") ?? track.album?.name ?? "",
),
);
}
}

View File

@ -3,18 +3,17 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class UserAlbums extends HookConsumerWidget { class UserAlbums extends HookConsumerWidget {
const UserAlbums({Key? key}) : super(key: key); const UserAlbums({Key? key}) : super(key: key);
@ -25,12 +24,10 @@ class UserAlbums extends HookConsumerWidget {
final albumsQuery = useQueries.album.ofMine(ref); final albumsQuery = useQueries.album.ofMine(ref);
final spacing = useBreakpointValue<double>( final spacing = useBreakpointValue<double>(
xs: 0,
sm: 0, sm: 0,
others: 20, others: 20,
); );
final viewType = MediaQuery.of(context).size.width < 480
? PlaybuttonCardViewType.list
: PlaybuttonCardViewType.square;
final searchText = useState(''); final searchText = useState('');
@ -39,13 +36,13 @@ class UserAlbums extends HookConsumerWidget {
return albumsQuery.data?.toList() ?? []; return albumsQuery.data?.toList() ?? [];
} }
return albumsQuery.data return albumsQuery.data
?.map((e) => Tuple2( ?.map((e) => (
weightedRatio(e.name!, searchText.value), weightedRatio(e.name!, searchText.value),
e, e,
)) ))
.sorted((a, b) => b.item1.compareTo(a.item1)) .sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.item1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.item2) .map((e) => e.$2)
.toList() ?? .toList() ??
[]; [];
}, [albumsQuery.data, searchText.value]); }, [albumsQuery.data, searchText.value]);
@ -53,9 +50,6 @@ class UserAlbums extends HookConsumerWidget {
if (auth == null) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }
if (albumsQuery.isLoading || !albumsQuery.hasData) {
return const Center(child: ShimmerPlaybuttonCard(count: 7));
}
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@ -63,30 +57,40 @@ class UserAlbums extends HookConsumerWidget {
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Material( child: Padding(
type: MaterialType.transparency, padding: const EdgeInsets.all(8.0),
textStyle: PlatformTheme.of(context).textTheme!.body!, child: SafeArea(
color: PlatformTheme.of(context).scaffoldBackgroundColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PlatformTextField( SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
prefixIcon: SpotubeIcons.filter, leading: const Icon(SpotubeIcons.filter),
placeholder: 'Filter Albums...', hintText: context.l10n.filter_albums,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Wrap( AnimatedCrossFade(
spacing: spacing, // gap between adjacent chips duration: const Duration(milliseconds: 300),
runSpacing: 20, // gap between lines firstChild: Container(
alignment: WrapAlignment.center, alignment: Alignment.topLeft,
children: albums padding: const EdgeInsets.all(16.0),
.map((album) => AlbumCard( child: const ShimmerPlaybuttonCard(count: 7),
viewType: viewType, ),
TypeConversionUtils.simpleAlbum_X_Album(album), secondChild: Wrap(
)) spacing: spacing, // gap between adjacent chips
.toList(), runSpacing: 20, // gap between lines
alignment: WrapAlignment.center,
children: albums
.map((album) => AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
))
.toList(),
),
crossFadeState: albumsQuery.isLoading ||
!albumsQuery.hasData ||
searchText.value.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
), ),
], ],
), ),

View File

@ -3,48 +3,44 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:tuple/tuple.dart';
class UserArtists extends HookConsumerWidget { class UserArtists extends HookConsumerWidget {
const UserArtists({Key? key}) : super(key: key); const UserArtists({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final artistQuery = useQueries.artist.followedByMe(ref); final artistQuery = useQueries.artist.followedByMeAll(ref);
final hasNextPage = artistQuery.pages.isEmpty
? false
: (artistQuery.pages.last.items?.length ?? 0) == 15;
final searchText = useState(''); final searchText = useState('');
final filteredArtists = useMemoized(() { final filteredArtists = useMemoized(() {
final artists = artistQuery.pages final artists = artistQuery.data ?? [];
.expand<Artist>((page) => page.items ?? const Iterable.empty());
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return artists.toList(); return artists.toList();
} }
return artists return artists
.map((e) => Tuple2( .map((e) => (
weightedRatio(e.name!, searchText.value), weightedRatio(e.name!, searchText.value),
e, e,
)) ))
.sorted((a, b) => b.item1.compareTo(a.item1)) .sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.item1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.item2) .map((e) => e.$2)
.toList(); .toList();
}, [artistQuery.pages, searchText.value]); }, [artistQuery.data, searchText.value]);
final controller = useScrollController();
if (auth == null) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
@ -56,57 +52,48 @@ class UserArtists extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ColoredBox( child: ColoredBox(
color: PlatformTheme.of(context).scaffoldBackgroundColor!, color: theme.scaffoldBackgroundColor,
child: PlatformTextField( child: SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
prefixIcon: SpotubeIcons.filter, leading: const Icon(SpotubeIcons.filter),
placeholder: 'Filter artists...', hintText: context.l10n.filter_artist,
), ),
), ),
), ),
), ),
backgroundColor: PlatformTheme.of(context).scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
body: artistQuery.pages.isEmpty body: artistQuery.data?.isEmpty == true
? Padding( ? Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: [
PlatformCircularProgressIndicator(), const CircularProgressIndicator(),
SizedBox(width: 10), const SizedBox(width: 10),
PlatformText("Loading..."), Text(context.l10n.loading),
], ],
), ),
) )
: RefreshIndicator( : RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await artistQuery.refreshAll(); await artistQuery.refresh();
}, },
child: GridView.builder( child: SingleChildScrollView(
itemCount: filteredArtists.length, controller: controller,
physics: const AlwaysScrollableScrollPhysics(), child: SizedBox(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( width: double.infinity,
maxCrossAxisExtent: 200, child: SafeArea(
mainAxisExtent: 250, child: Center(
crossAxisSpacing: 20, child: Wrap(
mainAxisSpacing: 20, spacing: 15,
runSpacing: 5,
children: filteredArtists
.mapIndexed((index, artist) => ArtistCard(artist))
.toList(),
),
),
),
), ),
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
return HookBuilder(builder: (context) {
if (index == artistQuery.pages.length - 1 && hasNextPage) {
return Waypoint(
controller: useScrollController(),
isGrid: true,
onTouchEdge: () {
artistQuery.fetchNext();
},
child: ArtistCard(filteredArtists[index]),
);
}
return ArtistCard(filteredArtists[index]);
});
},
), ),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More