├── .github └── ISSUE_TEMPLATE │ ├── a-bug.md │ └── feature_request.md ├── .gitignore ├── .metadata ├── README.md ├── android ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── .classpath │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── modatwenty │ │ │ │ └── tunein │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── logo.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── logo.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── logo.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── logo.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── logo.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── fonts ├── LigatureSymbols-2.11.ttf ├── boxicons.ttf └── fa.ttf ├── images ├── artist.jpg ├── blackbgUpnp.png ├── cover.png └── track.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── components │ ├── Album │ │ └── albumTags.dart │ ├── AlbumSongCell.dart │ ├── ArtistCell.dart │ ├── Tune │ │ └── songTags.dart │ ├── albumCard.dart │ ├── albumSongList.dart │ ├── appbar.dart │ ├── artistAlbumsList.dart │ ├── bottomPanel.dart │ ├── bottomnavbar.dart │ ├── card.dart │ ├── cards │ │ ├── AnimatedDialog.dart │ │ ├── PreferedPicks.dart │ │ ├── expandableItems.dart │ │ ├── genericItem.dart │ │ └── optionsCard.dart │ ├── common │ │ ├── DefaultArtistWidget.dart │ │ ├── ShowWithFadeComponent.dart │ │ ├── selectableTile.dart │ │ └── trackListDeck.dart │ ├── controlls.dart │ ├── customPageView.dart │ ├── drawer │ │ ├── DrawerControls.dart │ │ └── sideDrawer.dart │ ├── genericSongList.dart │ ├── gridcell.dart │ ├── itemListDevider.dart │ ├── pageheader.dart │ ├── pagenavheader.dart │ ├── pagenavheaderitem.dart │ ├── playing.dart │ ├── playlistCell.dart │ ├── scrollbar.dart │ ├── slider.dart │ ├── smallControlls.dart │ ├── songInfoWidget.dart │ ├── stageScrollingPhysics.dart │ ├── threeDotPopupMenu.dart │ ├── trackListDeck.dart │ └── trackListDeckItem.dart ├── globals.dart ├── main.dart ├── models │ ├── ContextMenuOption.dart │ ├── playback.dart │ └── playerstate.dart ├── pages │ ├── collection │ │ ├── collection.page.dart │ │ ├── favorites.page.dart │ │ └── playlists.page.dart │ ├── library │ │ ├── albums.page.dart │ │ ├── artists.page.dart │ │ ├── library.page.dart │ │ └── tracks.page.dart │ ├── management │ │ ├── AddSongsToPlaylist.dart │ │ └── EditPlaylist.dart │ ├── metrics │ │ └── metrics.page.dart │ ├── search.dart │ ├── settings │ │ └── settings.page.dart │ └── single │ │ ├── AboutPage.dart │ │ ├── LandingPage.dart │ │ ├── playingQueue.dart │ │ ├── singleAlbum.page.dart │ │ ├── singleArtistPage.dart │ │ └── singlePlaylistPage.dart ├── plugins │ ├── AudioPluginService.dart │ ├── AudioReceiverService.dart │ ├── NotificationControlService.dart │ ├── ThemeReceiverService.dart │ ├── nano.dart │ └── upnp.dart ├── root.dart ├── services │ ├── castService.dart │ ├── dialogService.dart │ ├── fileService.dart │ ├── http │ │ ├── httpRequests.dart │ │ ├── requests.dart │ │ ├── server │ │ │ └── httpOutgoingServer.dart │ │ └── utilsRequests.dart │ ├── isolates │ │ ├── musicServiceIsolate.dart │ │ ├── pluginIsolateFunctions.dart │ │ └── standardIsolateFunctions.dart │ ├── languageService.dart │ ├── layout.dart │ ├── locator.dart │ ├── memoryCacheService.dart │ ├── musicMetricsService.dart │ ├── musicService.dart │ ├── pageService.dart │ ├── platformService.dart │ ├── queueService.dart │ ├── routes │ │ └── pageRoutes.dart │ ├── settingService.dart │ ├── sideDrawerService.dart │ ├── themeService.dart │ └── uiScaleService.dart ├── utils │ ├── ConversionUtils.dart │ ├── MathUtils.dart │ └── messaginUtils.dart └── values │ ├── contextMenus.dart │ └── lists.dart ├── locale ├── en.json └── es.json ├── pubspec.lock ├── pubspec.yaml └── screenshots ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 2.jpg ├── 2.png ├── 3.png ├── 4.png ├── 6.png ├── 7.png ├── 8.png └── 9.png /.github/ISSUE_TEMPLATE/a-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: A Bug 3 | about: Create and report a bug that was encountered when using the app 4 | title: 'BUG - ' 5 | labels: bug 6 | assignees: moda20 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Version [e.g. 22] 36 | - App version (if applicable) 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea that you want to be implemented or considered 4 | title: 'FEAT - ' 5 | labels: enhancement 6 | assignees: moda20 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 71 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | #ThisfiletrackspropertiesofthisFlutterproject. 2 | #UsedbyFluttertooltoassesscapabilitiesandperformupgradesetc. 3 | # 4 | #Thisfileshouldbeversioncontrolledandshouldnotbemanuallyedited. 5 | 6 | version: 7 | revision:7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel:stable 9 | 10 | project_type:app 11 | 12 | 13 | 14 | cherylreneeevans@gmail.com:robert 15 | jacilynn.ferris@yahoo.com:Brian0302 16 | dutch693@gmail.com:d1rrty 17 | kwarren@rochester.rr.com:cookie70 18 | fruffino44@gmail.com:heights29 19 | taylor.elizabeth1288@gmail.com:cubbies12 20 | taliscollier@rocketmail.com:gwernt 21 | duffy.collette@gmail.com:trojans09 22 | ryanknowles51@gmail.com:satie334 23 | nafesahm@gmail.com:shiloh79 24 | pomes1989@gmail:addy13 25 | msgt501@aol.com:millbrook 26 | venusrantoine@yahoo.com:Love2Read 27 | shavonda.snipes@gmail.com:roscoe01 28 | l0v3neena@yahoo.com:sagittarius12 29 | amynj2727@gmail.com:dancing11 30 | falguni24@gmail.com:Dhruvi2008 31 | brandohe@gmail.com:guitar123 32 | marymac66@verizon.net:Avalon15 33 | traceyupton1@gmail.com:tracey 34 | tlfoss@hotmail.com:Design518 35 | alanraulerson@gmail.com:nala0416 36 | pataline2real@gmailcom:september 37 | athomas2781@gmail.com:elaine1960 38 | smiles4fun1@gmail:foghorny 39 | cherne3@live.com:penguin3 40 | aaront22@me.com:Dallas88 41 | antboi256@yahoo.com:nike22 42 | agap71407@optonline..com:bastille 43 | mrbstyle86@yahoo.com:1hairspray 44 | kellyherwig@gmail.com:Cullin77 45 | tracyspaniol@getinshapeforwomen.com:emma2004 46 | ayhan.karagoz@hotmail.com:ayhankaragöz 47 | tarafitz000@gmail.com:Doherty4 48 | frankquiles55@gmail.com:Family1st 49 | delaney0119@gmail.com:spencer50 50 | tinacurry64@gmail.com:maddie73 51 | mfstrojny@yahoo.com:Ronnie10 52 | tlh440@aol.com:elmo1360 53 | ap17182@yahoo.com:Pink1green2 54 | alicianord@gmail.com:duke52 55 | ssblkn@yahoo.com:sammie 56 | joelm289@gmail.com:Killer66 57 | jessicarbackhaus@gmail.com:Back14Haus 58 | coachdbickford@yahoo.com:fatherof3 59 | meghan.jamie3@gmail.com:jamieray 60 | chellebelle2525@gmail.com:Broncos23 61 | latishablue@yahoo.com:Tyshawn1 62 | kjallen22@hotmail.com:mario1974 63 | hindyok05@gmail.com:Triangle05 64 | jbfredette@gmail.com:Garciaparra5 65 | jason.asay@gmail.com:carlos23 66 | jamesvernice@yahoo.com:Jehovah83 67 | eaglesown2036@aol.com:bunny12 68 | connorhealey@rocketmail.com:Olympia1 69 | zmmt7@goldmail.etsu.edu:sdhs2006 70 | wainesh03@gmail.com:Lovegood1 71 | ehawk400]@comcast.net:hawk4000 72 | jaaace421@yahoo.com:70mustang 73 | skipandcheryl57@yahoo.com:Blessed57 74 | ajklingler@me.com:Nathank1 75 | broughtondianne@yahoo.com:Cupcakes1 76 | fawadka@gmail.com:damon123 77 | edward.isaac73@gmail.com:Oscar717 78 | rie.ak\'s@ymail.com:pribadi88 79 | jroeber616@gmail.com:tylerm97 80 | jcutter1980@msn.com:007casino 81 | hobbs54956@gmail.com:hobbes 82 | erbamee@hotmail.com:moss123 83 | lexiemom@msn.com:Goldfish1 84 | theatreenglish1@gmail.com:Ryan123. 85 | mmitchell4553@@hotmail.com:jmichael2 86 | nab2253@gmail.com:cbl3brig 87 | leahreckman@yahoo.com:Annabelle1 88 | 4loveofmary.@gmail.com:sadiebug 89 | sawyeraw@gmail.com:Pickle16 90 | meisskater@gmail:duke1984 91 | skyangelgrace@charter.net:greenbear 92 | bschloffer1221@yahoo.com:Noelle1221 93 | deedgal@sbcglobal.net:deedgal 94 | ellentansey@aol.com:joey1012 95 | jmuncy23@hotmail.com:Cameron01 96 | dlharris189@yahoo.com:shaunny11 97 | charlesbthompson@gmail.com:Newyork21 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎧 Flutter Music Player [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Codemagic build status](https://api.codemagic.io/apps/5d29b3b3db951153a6ceef80/5d29b3b3db951153a6ceef7f/status_badge.svg)](https://codemagic.io/apps/5d29b3b3db951153a6ceef80/5d29b3b3db951153a6ceef7f/latest_build) 2 | 3 | 4 | 5 | ## Contact me 6 | 7 | *email:* kadhem03@gmail.com 8 | 9 | *Gitter:* https://gitter.im/Moda20TuneIn/community 10 | 11 | Thank you in advance 👍 12 | 13 | 14 | 15 | ## Getting Started 🚀 16 | 17 | - Clone the repo 18 | - Install the dependencies by running `flutter packages get` 19 | - Run it with `flutter run` or `flutter run -d android` for a specific platform 20 | - You can build using `flutter build --no-tree-shake-icons` : the tree shake icons argument is a temporary thing until a fix is added in the main channel. 21 | 22 | 23 | 24 | ## Todos 25 | 26 | - [x] Retrieve songs 27 | - [x] Retrieve from SD 28 | - [x] Play 29 | - [x] Pause 30 | - [x] Seek 31 | - [x] Shuffle 32 | - [x] Favorites 33 | - [x] Album list 34 | - [x] Playing queue 35 | - [x] Android X migration 36 | - [x] Notification and Lock Screen Controls 37 | - [x] System integration 38 | - [x] Artist list 39 | - [x] Playlists 40 | - [x] Search songs 41 | - [x] Adding support for artist thumbnail update via online service (Discog Only, for the moment) 42 | - [x] Finishing up the Settings page 43 | - [x] upgrading performance 44 | - [x] Adding Native Media Controls: 45 | - [x] Native Android Media Controls 46 | - [ ] Native iOS Media Controls 47 | - [ ] Adding UPnP Casting 48 | - [x] UPnP 49 | - [x] Basic Http only casting with stable UI integration 50 | - [x] Full Interface and integration and testing 51 | - [ ] Full integration with media keys (headphones, wireless earphones, ...) 52 | - [ ] Implementing private access to files and moving to a HTTPS-only option 53 | - [x] Adding a better landing page : 54 | - [x] Most Played & Random songs 55 | - [x] Top Albums 56 | - [x] Current Queue wheel 57 | - [x] Discover Artists/Albums 58 | - [ ] FTP song registry 59 | - [ ] Media Tags changing, Song and library management 60 | - [ ] adding proper UI scaling 61 | - [ ] Spotify integration 62 | 63 | 64 | ## 📸 ScreenShots 65 | | Track list | bottom panel playing | Main panel playing | Artist List | 66 | | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | -----------------------------------------| 67 | | | | | | 68 | 69 | | Album song list | ALbums List | Home Page | Notification Big Layout Controls | 70 | | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | -----------------------------------------| 71 | | | | | | 72 | 73 | ## Support me 74 | 75 | You can support me by: 76 | 77 | ⭐️ this repo if you like it. 78 | 79 | Buy me a cup of coffee ☕️: 80 | 81 | *NOT there yet* 82 | 83 | 84 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.4)) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home= 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion localProperties.getProperty('flutter.compileSdkVersion').toInteger() 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.modatwenty.tunein" 48 | minSdkVersion localProperties.getProperty('flutter.minSdkVersion').toInteger() 49 | targetSdkVersion localProperties.getProperty('flutter.targetSdkVersion').toInteger() 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 53 | } 54 | 55 | buildTypes { 56 | release { 57 | // TODO: Add your own signing config for the release build. 58 | // Signing with the debug keys for now, so `flutter run --release` works. 59 | signingConfig signingConfigs.debug 60 | } 61 | } 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | dependencies { 69 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 70 | } 71 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/modatwenty/tunein/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.modatwenty.tunein; 2 | import io.flutter.embedding.android.FlutterActivity; 3 | 4 | 5 | public class MainActivity extends FlutterActivity { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-hdpi/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-mdpi/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xhdpi/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xxhdpi/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/android/app/src/main/res/mipmap-xxxhdpi/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | #7A7A7A 5 | #0044AA 6 | #449DEF 7 | #2F6699 8 | #70C656 9 | #53933F 10 | #F3AE1B 11 | #BB6008 12 | #111111 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | 13 | /*subprojects { 14 | project.configurations.all { 15 | resolutionStrategy.eachDependency { details -> 16 | if (details.requested.group == 'com.android.support' 17 | && !details.requested.name.contains('multidex') ) { 18 | details.useVersion "27.1.1" 19 | } 20 | 21 | if (details.requested.group == 'androidx.versionedparcelable' 22 | && !details.requested.name.contains('versionedparcelable') ) { 23 | details.useVersion "1.1.0" 24 | } 25 | 26 | if (details.requested.group == 'androidx.media' 27 | && !details.requested.name.contains('androidx') ) { 28 | details.useVersion "1.0.1" 29 | } 30 | if (details.requested.group == 'androidx.core' 31 | && !details.requested.name.contains('androidx') ) { 32 | details.useVersion "1.1.0" 33 | } 34 | if (details.requested.group == 'androidx.coordinatorlayout' 35 | && !details.requested.name.contains('androidx') ) { 36 | details.useVersion "1.0.0" 37 | } 38 | if (details.requested.group == 'androidx.arch' 39 | && !details.requested.name.contains('androidx') ) { 40 | details.useVersion "2.0.0" 41 | } 42 | if (details.requested.group == 'androidx.fragment' 43 | && !details.requested.name.contains('androidx') ) { 44 | details.useVersion "1.0.0" 45 | } 46 | } 47 | } 48 | }*/ 49 | } 50 | 51 | allprojects { 52 | repositories { 53 | google() 54 | mavenCentral() 55 | } 56 | } 57 | 58 | rootProject.buildDir = '../build' 59 | subprojects { 60 | project.buildDir = "${rootProject.buildDir}/${project.name}" 61 | } 62 | subprojects { 63 | project.evaluationDependsOn(':app') 64 | } 65 | 66 | task clean(type: Delete) { 67 | delete rootProject.buildDir 68 | } 69 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.enableJetifier=true 4 | android.useAndroidX=true -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /fonts/LigatureSymbols-2.11.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/fonts/LigatureSymbols-2.11.ttf -------------------------------------------------------------------------------- /fonts/boxicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/fonts/boxicons.ttf -------------------------------------------------------------------------------- /fonts/fa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/fonts/fa.ttf -------------------------------------------------------------------------------- /images/artist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/images/artist.jpg -------------------------------------------------------------------------------- /images/blackbgUpnp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/images/blackbgUpnp.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/images/cover.png -------------------------------------------------------------------------------- /images/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/images/track.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | music 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/components/albumCard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:Tunein/plugins/nano.dart'; 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:Tunein/services/musicService.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:Tunein/models/playerstate.dart'; 8 | import 'package:flutter/cupertino.dart'; 9 | import 'package:Tunein/pages/single/singleAlbum.page.dart'; 10 | 11 | import '../globals.dart'; 12 | 13 | class AlbumCard extends StatelessWidget { 14 | final Album? _album; 15 | final VoidCallback? onTap; 16 | 17 | AlbumCard({Key? key, required Album album, this.onTap}) 18 | : _album = album, 19 | super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | double paddingOnSide = 4; 24 | double cardHeight = 120; 25 | return Material( 26 | color: Colors.transparent, 27 | child: InkWell( 28 | child: Container( 29 | color: Colors.transparent, 30 | padding: EdgeInsets.symmetric(vertical: 5), 31 | child: Stack( 32 | children: [ 33 | Padding( 34 | padding: EdgeInsets.all(paddingOnSide), 35 | child: FadeInImage( 36 | placeholder: AssetImage('images/track.png'), 37 | fadeInDuration: Duration(milliseconds: 200), 38 | fadeOutDuration: Duration(milliseconds: 100), 39 | image: _album!.albumArt != null 40 | ? FileImage( 41 | new File(_album!.albumArt!), 42 | ) 43 | : AssetImage('images/track.png') 44 | as ImageProvider, 45 | height: cardHeight, 46 | fit: BoxFit.cover, 47 | )), 48 | Positioned( 49 | child: Container( 50 | width: (MediaQuery.of(context).size.width / 3) - 51 | (paddingOnSide * 2), 52 | child: Padding( 53 | padding: EdgeInsets.all(3), 54 | child: Column( 55 | children: [ 56 | Padding( 57 | padding: const EdgeInsets.only(bottom: 8), 58 | child: Text( 59 | (_album!.title == null) 60 | ? "Unknon Title" 61 | : _album!.title as String, 62 | overflow: TextOverflow.ellipsis, 63 | style: TextStyle( 64 | fontSize: 13.5, 65 | fontWeight: FontWeight.w900, 66 | color: Colors.white70, 67 | ), 68 | maxLines: 1, 69 | ), 70 | ), 71 | Text( 72 | (_album!.artist == null) 73 | ? "Unknon Artist" 74 | : _album!.artist as String, 75 | overflow: TextOverflow.ellipsis, 76 | style: TextStyle( 77 | fontSize: 12.5, 78 | fontWeight: FontWeight.w400, 79 | color: Colors.white70, 80 | ), 81 | ), 82 | ], 83 | )), 84 | alignment: Alignment.bottomCenter, 85 | decoration: BoxDecoration( 86 | backgroundBlendMode: BlendMode.darken, 87 | color: Colors.black87), 88 | ), 89 | bottom: 0, 90 | ), 91 | ], 92 | alignment: Alignment.center, 93 | ), 94 | ), 95 | enableFeedback: false, 96 | onTap: () { 97 | if (onTap != null) { 98 | onTap!(); 99 | } else { 100 | gotoFullAlbumPage(context, _album!.songs[0]); 101 | } 102 | }, 103 | ), 104 | ); 105 | } 106 | 107 | gotoFullAlbumPage(context, Tune song) { 108 | MyUtils.createDelayedPageroute(context, SingleAlbumPage(song), this); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/components/appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../globals.dart'; 3 | 4 | class MyAppBar extends StatelessWidget implements PreferredSizeWidget { 5 | double _appBarHeight; 6 | MyAppBar(this._appBarHeight); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return AppBar(backgroundColor: MyTheme.darkBlack, elevation: 0); 11 | } 12 | 13 | @override 14 | Size get preferredSize => new Size.fromHeight(_appBarHeight); 15 | } 16 | -------------------------------------------------------------------------------- /lib/components/artistAlbumsList.dart: -------------------------------------------------------------------------------- 1 | ///DEPRECATED WARNING 2 | /// ///////////////////////////////////////////////////////// 3 | ///THIS HAS BEEN DEPRECATED AND MAY BE REMOVED IN THE FUTURE 4 | /// ///////////////////////////////////////////////////////// 5 | 6 | import 'package:Tunein/components/AlbumSongCell.dart'; 7 | import 'package:Tunein/components/ArtistCell.dart'; 8 | import 'package:Tunein/components/card.dart'; 9 | import 'package:Tunein/components/pageheader.dart'; 10 | import 'package:Tunein/components/scrollbar.dart'; 11 | import 'package:Tunein/globals.dart'; 12 | import 'package:Tunein/models/playerstate.dart'; 13 | import 'package:Tunein/pages/single/singleAlbum.page.dart'; 14 | import 'package:Tunein/plugins/nano.dart'; 15 | import 'package:Tunein/services/locator.dart'; 16 | import 'package:Tunein/services/musicService.dart'; 17 | import 'package:Tunein/services/themeService.dart'; 18 | import 'package:Tunein/values/contextMenus.dart'; 19 | import 'package:flutter/material.dart'; 20 | import 'package:flutter/rendering.dart'; 21 | 22 | class ArtistAlbumList extends StatefulWidget { 23 | final Artist artist; 24 | 25 | ArtistAlbumList(this.artist); 26 | 27 | @override 28 | _ArtistAlbumListState createState() => _ArtistAlbumListState(); 29 | } 30 | 31 | class _ArtistAlbumListState extends State { 32 | final musicService = locator(); 33 | final themeService = locator(); 34 | ScrollController? controller; 35 | 36 | @override 37 | void initState() { 38 | // TODO: implement initState 39 | controller = ScrollController(); 40 | super.initState(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | var size = MediaQuery.of(context).size; 46 | 47 | final double itemWidth = size.width / 3; 48 | 49 | return StreamBuilder( 50 | stream: themeService.getArtistColors(widget.artist).asStream(), 51 | builder: (BuildContext context, AsyncSnapshot> snapshot) { 52 | List bgColor; 53 | 54 | bgColor = snapshot.data!; 55 | 56 | /*return Container( 57 | padding: EdgeInsets.all(5), 58 | alignment: Alignment.center, 59 | color: bgColor!=null?Color(bgColor[0]).withRed(30).withGreen(30).withBlue(30):MyTheme.darkBlack, 60 | child: Row( 61 | mainAxisSize: MainAxisSize.max, 62 | children: [ 63 | Expanded( 64 | child: Column( 65 | children: [ 66 | Flexible( 67 | child: , 68 | ), 69 | ], 70 | ), 71 | ), 72 | MyScrollbar( 73 | controller: controller, 74 | color: bgColor!=null?Color(bgColor[0]).withRed(30).withGreen(30).withBlue(30):null, 75 | ), 76 | ], 77 | ), 78 | );*/ 79 | return Container( 80 | padding: EdgeInsets.all(5), 81 | alignment: Alignment.center, 82 | color: bgColor != null 83 | ? Color(bgColor[0]).withRed(30).withGreen(30).withBlue(30) 84 | : MyTheme.darkBlack, 85 | child: GridView.builder( 86 | padding: EdgeInsets.all(0), 87 | itemCount: widget.artist.albums.length, 88 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 89 | crossAxisCount: 3, 90 | mainAxisSpacing: 3, 91 | crossAxisSpacing: 3, 92 | childAspectRatio: (itemWidth / (itemWidth + 50)), 93 | ), 94 | physics: ScrollPhysics(), 95 | itemBuilder: (BuildContext context, int index) { 96 | int newIndex = (index % 3) + 2; 97 | return GestureDetector( 98 | onTap: () { 99 | goToSingleArtistPage(widget.artist.albums[index]); 100 | }, 101 | child: Material( 102 | // the material widget here helps with the themes 103 | //the non inclusion of it means you get double bars underneath the text 104 | //this is not a must but you need to find a way to give a theme to your widget 105 | //Material widget is the easiest and the one i am using in this app 106 | child: AlbumGridCell( 107 | widget.artist.albums[index], 108 | 135, 109 | 80, 110 | animationDelay: (50 * newIndex) - 111 | (index < 6 ? ((6 - index) * 160) : 0), 112 | ), 113 | color: Colors.transparent, 114 | ), 115 | ); 116 | }, 117 | )); 118 | }, 119 | ); 120 | } 121 | 122 | void goToSingleArtistPage(Album album) { 123 | Navigator.of(context).push( 124 | MaterialPageRoute( 125 | builder: (context) => SingleAlbumPage(null, album: album), 126 | ), 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/components/bottomnavbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/pages/search.dart'; 2 | import 'package:Tunein/pages/settings/settings.page.dart'; 3 | import 'package:Tunein/services/layout.dart'; 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:Tunein/values/lists.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../globals.dart'; 10 | 11 | class BottomNavBar extends StatefulWidget { 12 | BottomNavBar({Key? key}) : super(key: key); 13 | 14 | _BottomNavBarState createState() => _BottomNavBarState(); 15 | } 16 | 17 | class _BottomNavBarState extends State { 18 | int _currentIndex = 0; 19 | final layoutService = locator(); 20 | @override 21 | Widget build(BuildContext context) { 22 | return BottomNavigationBar( 23 | currentIndex: _currentIndex, 24 | onTap: _handleTap, 25 | backgroundColor: MyTheme.bgBottomBar, 26 | unselectedItemColor: Colors.white54, 27 | selectedItemColor: Colors.white, 28 | type: BottomNavigationBarType.fixed, 29 | showUnselectedLabels: false, 30 | iconSize: 22, 31 | items: bottomNavBarItems 32 | .map((item) => BottomNavigationBarItem( 33 | backgroundColor: MyTheme.bgBottomBar, 34 | icon: item.value, 35 | label: item.key.toUpperCase(), 36 | // Padding( 37 | // padding: const EdgeInsets.only(top :5.0), 38 | // child: Text( 39 | // item.key.toUpperCase(), 40 | // style: TextStyle( 41 | // fontWeight: FontWeight.bold, 42 | // fontSize: 10, 43 | // ), 44 | // ), 45 | // ) 46 | )) 47 | .toList(), 48 | ); 49 | } 50 | 51 | _setBarIndex(int index) { 52 | setState(() { 53 | _currentIndex = index; 54 | }); 55 | } 56 | 57 | _navigate() { 58 | Navigator.of(context).push( 59 | MaterialPageRoute( 60 | builder: (context) => SearchPage(), 61 | ), 62 | ); 63 | } 64 | 65 | _opeSettingsPage() { 66 | Navigator.of(context).push( 67 | MaterialPageRoute( 68 | builder: (context) => SettingsPage(), 69 | ), 70 | ); 71 | } 72 | 73 | _handleTap(int index) { 74 | switch (index) { 75 | case 0: 76 | layoutService.changeGlobalPage(index); 77 | _setBarIndex(index); 78 | break; 79 | case 1: 80 | layoutService.changeGlobalPage(index); 81 | _setBarIndex(index); 82 | break; 83 | case 2: 84 | _navigate(); 85 | break; 86 | case 3: 87 | _navigate(); 88 | break; 89 | case 4: 90 | layoutService.changeGlobalPage(index); 91 | _setBarIndex(index); 92 | break; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/components/cards/AnimatedDialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class AnimatedDialog extends StatefulWidget { 6 | Widget? dialogContent; 7 | Widget? traversingWidget; 8 | GlobalKey? traversingWidgetGlobalKey; 9 | double? maxHeight; 10 | double? maxWidth; 11 | Animation? inputAnimation; 12 | AnimatedDialog( 13 | {this.dialogContent, 14 | this.traversingWidget, 15 | this.maxHeight, 16 | this.maxWidth, 17 | this.inputAnimation}); 18 | 19 | @override 20 | _AnimatedDialogState createState() => _AnimatedDialogState(); 21 | } 22 | 23 | class _AnimatedDialogState extends State 24 | with SingleTickerProviderStateMixin { 25 | /// The [AnimationController] is a Flutter Animation object that generates a new value 26 | /// whenever the hardware is ready to draw a new frame. 27 | AnimationController? _controller; 28 | 29 | ///DEPRECATED 30 | ///NO INTERNAL TWEEN IS BEING USED HERE, phased for generalDialog transitionBuilder 31 | /// Since the above object interpolates only between 0 and 1, but we'd rather apply a curve to the current 32 | /// animation, we're providing a custom [Tween] that allows to build more advanced animations, as seen in [initState()]. 33 | Animatable _sizeTween = Tween( 34 | begin: 0.0, 35 | end: 1.0, 36 | ); 37 | 38 | /// The [Animation] object itself, which is required by the [SizeTransition] widget in the [build()] method. 39 | Animation? _sizeAnimation; 40 | 41 | /// Detects which state the widget is currently in, and triggers the animation upon change. 42 | bool _isExpanded = false; 43 | 44 | /// Here we initialize the fields described above, and set up the widget to its initial state. 45 | @override 46 | initState() { 47 | _sizeAnimation = widget.inputAnimation ?? _sizeAnimation; 48 | super.initState(); 49 | 50 | _controller = AnimationController( 51 | vsync: this, 52 | duration: const Duration(milliseconds: 10), 53 | ); 54 | 55 | /// This curve is controlled by [_controller]. 56 | final CurvedAnimation curve = 57 | CurvedAnimation(parent: _controller!, curve: Curves.fastOutSlowIn); 58 | 59 | /// [_sizeAnimation] will interpolate using this curve - [Curves.fastOutSlowIn]. 60 | /* _sizeAnimation = _sizeTween.animate(curve); 61 | _controller.addListener(() { 62 | setState(() {}); 63 | });*/ 64 | 65 | //_controller.forward(); 66 | } 67 | 68 | @override 69 | void didUpdateWidget(AnimatedDialog oldWidget) { 70 | // TODO: implement didUpdateWidget 71 | super.didUpdateWidget(oldWidget); 72 | _sizeAnimation = oldWidget.inputAnimation; 73 | } 74 | 75 | @override 76 | dispose() { 77 | ///DEPRECATED 78 | ///NO INTERNAL TWEEN IS BEING USED HERE, phased for generalDialog transitionBuilder 79 | //_controller.reverse(); 80 | _controller!.dispose(); 81 | super.dispose(); 82 | } 83 | 84 | /// Whenever a tap is detected, toggle a change in the state and move the animation forward 85 | /// or backwards depending on the initial status. 86 | _toggleExpand() { 87 | setState(() { 88 | _isExpanded = !_isExpanded; 89 | }); 90 | print("isExpandable? ${_isExpanded}"); 91 | switch (_sizeAnimation!.status) { 92 | case AnimationStatus.completed: 93 | _controller!.reverse(); 94 | break; 95 | case AnimationStatus.dismissed: 96 | _controller!.forward(); 97 | break; 98 | case AnimationStatus.reverse: 99 | case AnimationStatus.forward: 100 | break; 101 | } 102 | } 103 | 104 | @override 105 | Widget build(BuildContext context) { 106 | return ScaleTransition( 107 | scale: _sizeAnimation!, 108 | child: widget.dialogContent ?? 109 | Container( 110 | height: 100, 111 | width: 100, 112 | color: MyTheme.darkRed, 113 | )); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/components/cards/PreferedPicks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:Tunein/globals.dart'; 5 | import 'package:Tunein/services/locator.dart'; 6 | import 'package:Tunein/services/themeService.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | final themeService = locator(); 10 | 11 | class PreferredPicks extends StatelessWidget { 12 | final String? bottomTitle; 13 | final String? imageUri; 14 | final Widget? backgroundWidget; 15 | final List? colors; 16 | final MapEntry? blurPower; 17 | final Color? blurColor; 18 | final MapEntry? textPosition; 19 | final Key? key; 20 | final Radius? borderRadius; 21 | final bool? allImageBlur; 22 | PreferredPicks( 23 | {this.bottomTitle, 24 | this.imageUri, 25 | this.colors, 26 | this.backgroundWidget, 27 | this.blurPower, 28 | this.blurColor, 29 | this.textPosition, 30 | this.key, 31 | this.borderRadius, 32 | this.allImageBlur = true}) 33 | : super(key: key); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | Color shadowColor = ((colors != null && colors!.length != 0) 38 | ? new Color(colors![0]) 39 | : Color(themeService.defaultColors[0])) 40 | .withOpacity(.7); 41 | return Container( 42 | margin: EdgeInsets.symmetric(horizontal: 5), 43 | decoration: BoxDecoration( 44 | color: Colors.transparent, 45 | borderRadius: BorderRadius.all(borderRadius ?? Radius.circular(10)), 46 | border: Border.all(width: .3, color: MyTheme.bgBottomBar), 47 | ), 48 | child: Material( 49 | color: Colors.transparent, 50 | child: Container( 51 | decoration: BoxDecoration( 52 | color: Colors.transparent, 53 | borderRadius: BorderRadius.all(borderRadius ?? Radius.circular(10)), 54 | border: Border.all(width: .3, color: MyTheme.bgBottomBar), 55 | ), 56 | child: Stack( 57 | clipBehavior: Clip.hardEdge, 58 | children: [ 59 | ImageFiltered( 60 | imageFilter: ImageFilter.blur( 61 | sigmaX: blurPower != null ? blurPower!.key : 2.0 as dynamic, 62 | sigmaY: 63 | blurPower != null ? blurPower!.value : 3.0 as dynamic), 64 | child: Container( 65 | clipBehavior: Clip.antiAlias, 66 | decoration: BoxDecoration( 67 | color: Colors.transparent, 68 | borderRadius: 69 | BorderRadius.all(borderRadius ?? Radius.circular(10)), 70 | border: Border.all(width: .3, color: MyTheme.bgBottomBar), 71 | ), 72 | child: backgroundWidget ?? 73 | ConstrainedBox( 74 | child: imageUri == null 75 | ? Image.asset("images/artist.jpg", 76 | fit: BoxFit.cover) 77 | : Image( 78 | image: FileImage(File(imageUri!)), 79 | fit: BoxFit.cover, 80 | colorBlendMode: BlendMode.clear, 81 | ), 82 | constraints: BoxConstraints.expand(), 83 | ), 84 | ), 85 | ), 86 | if (allImageBlur!) 87 | Container( 88 | child: ClipRect( 89 | child: BackdropFilter( 90 | filter: ImageFilter.blur( 91 | // sigmaX: blurPower != null ? blurPower.key : 2, 92 | sigmaX: 93 | blurPower != null ? blurPower!.key : 2.0 as dynamic, 94 | sigmaY: blurPower != null 95 | ? blurPower!.value 96 | : 3.0 as dynamic), 97 | child: Container( 98 | decoration: BoxDecoration( 99 | color: blurColor ?? Colors.grey.shade100.withOpacity(0.2), 100 | borderRadius: 101 | BorderRadius.all(borderRadius ?? Radius.circular(10)), 102 | border: Border.all(width: .3, color: MyTheme.bgBottomBar), 103 | )), 104 | ), 105 | )), 106 | if (bottomTitle != null) 107 | Positioned( 108 | child: Text( 109 | bottomTitle ?? "Choice card", 110 | style: TextStyle( 111 | color: ((colors != null && colors!.length != 0) 112 | ? new Color(colors![1]) 113 | : Color(0xffffffff)) 114 | .withOpacity(.8), 115 | fontSize: 22, 116 | fontWeight: FontWeight.w800, 117 | letterSpacing: 2, 118 | shadows: [ 119 | Shadow( 120 | // bottomLeft 121 | offset: Offset(-1.2, -1.2), 122 | color: shadowColor, 123 | blurRadius: 2), 124 | Shadow( 125 | // bottomRight 126 | offset: Offset(1.2, -1.2), 127 | color: shadowColor, 128 | blurRadius: 2), 129 | Shadow( 130 | // topRight 131 | offset: Offset(1.2, 1.2), 132 | color: shadowColor, 133 | blurRadius: 2), 134 | Shadow( 135 | // topLeft 136 | offset: Offset(-1.2, 1.2), 137 | color: shadowColor, 138 | blurRadius: 2, 139 | ), 140 | ]), 141 | ), 142 | bottom: textPosition != null ? textPosition!.key : 5, 143 | left: textPosition != null ? textPosition!.value : 5, 144 | ), 145 | ], 146 | ), 147 | ), 148 | ), 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/components/cards/expandableItems.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ExpandableItem extends StatefulWidget { 5 | Color? backgroundColor; 6 | Widget? backgroundWidget; 7 | String? title; 8 | Color? titleColor; 9 | Widget? expandedPortion; 10 | ExpandableItem( 11 | {this.backgroundColor, 12 | this.backgroundWidget, 13 | this.title, 14 | this.titleColor, 15 | this.expandedPortion}); 16 | 17 | @override 18 | _ExpandableItemState createState() => _ExpandableItemState(); 19 | } 20 | 21 | class _ExpandableItemState extends State 22 | with SingleTickerProviderStateMixin { 23 | /// The [AnimationController] is a Flutter Animation object that generates a new value 24 | /// whenever the hardware is ready to draw a new frame. 25 | AnimationController? _controller; 26 | 27 | /// Since the above object interpolates only between 0 and 1, but we'd rather apply a curve to the current 28 | /// animation, we're providing a custom [Tween] that allows to build more advanced animations, as seen in [initState()]. 29 | static final Animatable _sizeTween = Tween( 30 | begin: 0.0, 31 | end: 1.0, 32 | ); 33 | 34 | /// The [Animation] object itself, which is required by the [SizeTransition] widget in the [build()] method. 35 | Animation? _sizeAnimation; 36 | 37 | /// Detects which state the widget is currently in, and triggers the animation upon change. 38 | bool _isExpanded = false; 39 | 40 | /// Here we initialize the fields described above, and set up the widget to its initial state. 41 | @override 42 | initState() { 43 | super.initState(); 44 | 45 | _controller = AnimationController( 46 | vsync: this, 47 | duration: const Duration(milliseconds: 200), 48 | ); 49 | 50 | /// This curve is controlled by [_controller]. 51 | final CurvedAnimation curve = 52 | CurvedAnimation(parent: _controller!, curve: Curves.fastOutSlowIn); 53 | 54 | /// [_sizeAnimation] will interpolate using this curve - [Curves.fastOutSlowIn]. 55 | _sizeAnimation = _sizeTween.animate(curve); 56 | _controller!.addListener(() { 57 | setState(() {}); 58 | }); 59 | } 60 | 61 | @override 62 | dispose() { 63 | _controller!.dispose(); 64 | super.dispose(); 65 | } 66 | 67 | /// Whenever a tap is detected, toggle a change in the state and move the animation forward 68 | /// or backwards depending on the initial status. 69 | _toggleExpand() { 70 | setState(() { 71 | _isExpanded = !_isExpanded; 72 | }); 73 | print("isExpandable? ${_isExpanded}"); 74 | switch (_sizeAnimation!.status) { 75 | case AnimationStatus.completed: 76 | _controller!.reverse(); 77 | break; 78 | case AnimationStatus.dismissed: 79 | _controller!.forward(); 80 | break; 81 | case AnimationStatus.reverse: 82 | case AnimationStatus.forward: 83 | break; 84 | } 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | return GestureDetector( 90 | onTap: _toggleExpand, 91 | child: Container( 92 | height: 150.0, 93 | decoration: BoxDecoration( 94 | borderRadius: BorderRadius.circular(10.0), 95 | color: widget.backgroundColor ?? MyTheme.darkBlack), 96 | child: ClipRRect( 97 | borderRadius: BorderRadius.circular(10.0), 98 | child: Stack( 99 | children: [ 100 | Positioned.fill( 101 | left: 0, 102 | top: 0, 103 | child: widget.backgroundWidget ?? 104 | Container( 105 | color: 106 | widget.backgroundColor ?? MyTheme.darkBlack, 107 | )), 108 | Column(children: [ 109 | Flexible( 110 | child: SizeTransition( 111 | axisAlignment: 1.0, 112 | axis: Axis.vertical, 113 | sizeFactor: _sizeAnimation!, 114 | child: Container(child: widget.expandedPortion), 115 | )) 116 | ]), 117 | ], 118 | )))); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/components/cards/genericItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/threeDotPopupMenu.dart'; 2 | import 'package:Tunein/globals.dart'; 3 | import 'package:Tunein/models/ContextMenuOption.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class GenericItem extends StatefulWidget { 7 | List? choices; 8 | final Size? ScreenSize; 9 | final double? StaticContextMenuFromBottom; 10 | final void Function(ContextMenuOptions)? onContextSelect; 11 | final void Function(ContextMenuOptions)? onContextCancel; 12 | final void Function()? onTap; 13 | final Widget? leading; 14 | final String? title; 15 | final String? subTitle; 16 | List? colors; 17 | double? height; 18 | GenericItem( 19 | {this.choices, 20 | this.ScreenSize, 21 | this.StaticContextMenuFromBottom, 22 | this.onContextCancel, 23 | this.onContextSelect, 24 | this.leading, 25 | this.subTitle, 26 | this.title, 27 | this.colors, 28 | this.onTap, 29 | this.height}); 30 | 31 | @override 32 | _GenericItemState createState() => _GenericItemState(); 33 | } 34 | 35 | class _GenericItemState extends State { 36 | List? choices; 37 | Size? ScreenSize; 38 | double? StaticContextMenuFromBottom; 39 | void Function(ContextMenuOptions)? onContextSelect; 40 | void Function(ContextMenuOptions)? onContextCancel; 41 | void Function()? onTap; 42 | Widget? leading; 43 | String? title; 44 | String? subTitle; 45 | List? colors; 46 | double? height; 47 | @override 48 | void initState() { 49 | // TODO: implement initState 50 | this.choices = widget.choices; 51 | this.StaticContextMenuFromBottom = widget.StaticContextMenuFromBottom; 52 | this.ScreenSize = widget.ScreenSize; 53 | this.onContextCancel = widget.onContextCancel; 54 | this.onContextSelect = widget.onContextSelect; 55 | this.leading = widget.leading; 56 | this.title = widget.title; 57 | this.subTitle = widget.subTitle; 58 | this.colors = widget.colors; 59 | this.onTap = widget.onTap; 60 | this.height = widget.height; 61 | super.initState(); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return Container( 67 | color: Colors.transparent, 68 | padding: EdgeInsets.symmetric(vertical: 5), 69 | child: Row( 70 | mainAxisSize: MainAxisSize.max, 71 | children: [ 72 | Expanded( 73 | child: Container( 74 | height: height ?? 62, 75 | child: Material( 76 | color: Colors.transparent, 77 | child: Container( 78 | child: Row( 79 | mainAxisSize: MainAxisSize.min, 80 | crossAxisAlignment: CrossAxisAlignment.center, 81 | children: [ 82 | leading != null 83 | ? Padding( 84 | padding: EdgeInsets.only(right: 15), 85 | child: leading, 86 | ) 87 | : Container(), 88 | Expanded( 89 | flex: 8, 90 | child: Column( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | mainAxisAlignment: MainAxisAlignment.center, 93 | children: [ 94 | Padding( 95 | padding: const EdgeInsets.only(bottom: 8), 96 | child: Text( 97 | title ?? "", 98 | overflow: TextOverflow.fade, 99 | maxLines: 1, 100 | textWidthBasis: TextWidthBasis.parent, 101 | softWrap: false, 102 | style: TextStyle( 103 | fontSize: 13.5, 104 | fontWeight: FontWeight.w800, 105 | color: colors != null 106 | ? colors![0].withAlpha(200) 107 | : Colors.white, 108 | ), 109 | ), 110 | ), 111 | Text( 112 | subTitle ?? "", 113 | overflow: TextOverflow.ellipsis, 114 | style: TextStyle( 115 | fontSize: 12.5, 116 | fontWeight: FontWeight.w500, 117 | color: colors != null 118 | ? colors![1].withAlpha(200) 119 | : MyTheme.grey300, 120 | ), 121 | ), 122 | ], 123 | ), 124 | ), 125 | ], 126 | ), 127 | ), 128 | ), 129 | ), 130 | flex: 12, 131 | ), 132 | choices != null 133 | ? ThreeDotPopupMenu( 134 | choices: choices, 135 | onContextSelect: onContextSelect!, 136 | screenSize: ScreenSize, 137 | staticOffsetFromBottom: StaticContextMenuFromBottom, 138 | ) 139 | : Container() 140 | ], 141 | ), 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/components/common/DefaultArtistWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DefaultArtistWidget extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/components/common/ShowWithFadeComponent.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ShowWithFade extends StatelessWidget { 5 | Widget? child; 6 | final Duration? fadeDuration; 7 | final Duration? durationUntilFadeStarts; 8 | final Widget? shallowWidget; 9 | final Curve? inCurve; 10 | Stream? inStream; 11 | ShowWithFade( 12 | {@required this.child, 13 | this.fadeDuration, 14 | this.durationUntilFadeStarts, 15 | this.shallowWidget, 16 | this.inCurve}); 17 | 18 | ShowWithFade.fromStream( 19 | {this.inStream, 20 | this.fadeDuration, 21 | this.durationUntilFadeStarts, 22 | this.shallowWidget, 23 | this.inCurve}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | if (this.inStream != null) { 28 | return buildWithStream(this.inStream!); 29 | } 30 | Widget fadedWidget = Stack( 31 | children: [ 32 | shallowWidget ?? 33 | Container( 34 | color: MyTheme.bgBottomBar, 35 | constraints: BoxConstraints.expand(), 36 | ) 37 | ], 38 | ); 39 | return StreamBuilder( 40 | stream: Future.delayed( 41 | durationUntilFadeStarts ?? Duration(milliseconds: 200), 42 | () => true).asStream(), 43 | builder: (context, AsyncSnapshot snapshot) { 44 | return AnimatedSwitcher( 45 | switchInCurve: inCurve ?? Curves.linear, 46 | duration: fadeDuration ?? Duration(milliseconds: 200), 47 | child: !snapshot.hasData 48 | ? fadedWidget 49 | : Container( 50 | child: child, 51 | ), 52 | ); 53 | }, 54 | ); 55 | } 56 | 57 | Widget buildWithStream(Stream inStream) { 58 | Widget fadedWidget = Stack( 59 | children: [ 60 | shallowWidget ?? 61 | Container( 62 | color: MyTheme.bgBottomBar, 63 | constraints: BoxConstraints.expand(), 64 | ) 65 | ], 66 | ); 67 | return StreamBuilder( 68 | stream: Future.delayed( 69 | durationUntilFadeStarts ?? Duration(milliseconds: 0), 70 | () => inStream.first).asStream(), 71 | builder: (context, AsyncSnapshot snapshot) { 72 | return AnimatedSwitcher( 73 | switchInCurve: inCurve ?? Curves.linear, 74 | duration: fadeDuration ?? Duration(milliseconds: 200), 75 | child: !snapshot.hasData 76 | ? fadedWidget 77 | : Container( 78 | child: snapshot.data, 79 | ), 80 | ); 81 | }, 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/components/customPageView.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/common/ShowWithFadeComponent.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CustomPageView extends StatefulWidget { 5 | List pages; 6 | PageController? controller; 7 | bool preload; 8 | Widget? shallowWidget; 9 | ScrollPhysics? physics; 10 | CustomPageView({Key? key, pages, controller, shallowWidget, physics, preload}) 11 | : this.pages = pages ?? [], 12 | this.controller = controller ?? new PageController(keepPage: true), 13 | this.shallowWidget = shallowWidget, 14 | this.physics = physics, 15 | this.preload = preload ?? true, 16 | super(key: key); 17 | 18 | @override 19 | _CustomPageViewState createState() => _CustomPageViewState(); 20 | } 21 | 22 | class _CustomPageViewState extends State { 23 | List savedPages = []; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | savedPages = widget.pages 29 | .map((elem) => ShowWithFade( 30 | child: elem, 31 | shallowWidget: widget.shallowWidget ?? Container(color: Colors.red), 32 | durationUntilFadeStarts: durationUntilPageShow)) 33 | .toList(); 34 | } 35 | 36 | Duration durationUntilPageShow = Duration(milliseconds: 200); 37 | @override 38 | Widget build(BuildContext context) { 39 | return PageView( 40 | controller: widget.controller, 41 | physics: widget.physics, 42 | children: savedPages as List, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/components/drawer/DrawerControls.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:Tunein/models/playback.dart'; 3 | import 'package:Tunein/models/playerstate.dart'; 4 | import 'package:Tunein/plugins/nano.dart'; 5 | import 'package:Tunein/services/locator.dart'; 6 | import 'package:Tunein/services/musicService.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | final musicService = locator(); 10 | 11 | class DrawerMusicControls extends StatelessWidget { 12 | PlayerState? entryState; 13 | Tune? entrySong; 14 | 15 | DrawerMusicControls({this.entryState, this.entrySong}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | if (entrySong != null && entryState != null) { 20 | return createDrawerControls(entrySong!, entryState!); 21 | } 22 | 23 | return StreamBuilder( 24 | stream: musicService.playerState$, 25 | builder: (context, AsyncSnapshot> snapshot) { 26 | if (!snapshot.hasData) { 27 | return Container(); 28 | } 29 | Tune? song = snapshot.data!.value; 30 | List? songColor = song?.colors ?? []; 31 | PlayerState? state = snapshot.data!.key; 32 | 33 | return createDrawerControls(song, state); 34 | }, 35 | ); 36 | } 37 | 38 | Widget createDrawerControls(Tune? song, PlayerState state) { 39 | List? songColor = song?.colors ?? []; 40 | return Material( 41 | color: Colors.transparent, 42 | child: Container( 43 | child: Column( 44 | mainAxisSize: MainAxisSize.min, 45 | children: [ 46 | Row( 47 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 48 | children: [ 49 | Container( 50 | margin: EdgeInsets.all(5), 51 | height: 45, 52 | width: 45, 53 | child: InkWell( 54 | child: Icon( 55 | state == PlayerState.paused 56 | ? Icons.play_arrow 57 | : Icons.pause, 58 | size: 32, 59 | color: songColor!.length != 0 60 | ? Color(songColor[1]!) 61 | : MyTheme.grey300, 62 | ), 63 | onTap: () { 64 | Future.delayed(Duration(milliseconds: 200), () { 65 | musicService.playOrPause(song); 66 | }); 67 | }, 68 | ), 69 | ), 70 | Container( 71 | margin: EdgeInsets.all(5), 72 | height: 45, 73 | width: 45, 74 | child: InkWell( 75 | child: Icon( 76 | Icons.skip_next, 77 | size: 32, 78 | color: songColor.length != 0 79 | ? Color(songColor[1]!) 80 | : MyTheme.grey300, 81 | ), 82 | onTap: () { 83 | Future.delayed(Duration(milliseconds: 200), () { 84 | musicService.playNextSong(); 85 | }); 86 | }, 87 | ), 88 | ) 89 | ], 90 | ), 91 | Row( 92 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 93 | children: [ 94 | Container( 95 | margin: EdgeInsets.all(5), 96 | height: 45, 97 | width: 45, 98 | child: InkWell( 99 | child: Icon( 100 | Icons.skip_previous, 101 | size: 32, 102 | color: songColor.length != 0 103 | ? Color(songColor[1]!) 104 | : MyTheme.grey300, 105 | ), 106 | onTap: () { 107 | Future.delayed(Duration(milliseconds: 200), () { 108 | musicService.playPreviousSong(); 109 | }); 110 | }, 111 | ), 112 | ), 113 | Container( 114 | margin: EdgeInsets.all(5), 115 | height: 45, 116 | width: 45, 117 | child: InkWell( 118 | child: StreamBuilder( 119 | stream: musicService.playback$, 120 | builder: 121 | (context, AsyncSnapshot> snapshot) { 122 | Color iconColor = (song?.colors?.isNotEmpty ?? false) ? new Color(song?.colors?.elementAt(1)?.toInt() ?? MyTheme.grey300.value) 123 | .withOpacity(.5) : MyTheme.darkRed; 124 | if (snapshot.hasData && 125 | snapshot.data!.contains(Playback.shuffle)) { 126 | iconColor = MyTheme.darkRed; 127 | } 128 | return Icon( 129 | Icons.shuffle, 130 | color: iconColor, 131 | size: 32, 132 | ); 133 | }, 134 | ), 135 | onTap: () { 136 | musicService.updatePlayback(Playback.shuffle, 137 | removeIfExistent: true); 138 | }, 139 | ), 140 | ) 141 | ], 142 | ) 143 | ], 144 | ), 145 | ), 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/components/gridcell.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:Tunein/plugins/nano.dart'; 4 | import 'package:Tunein/services/themeService.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:Tunein/services/locator.dart'; 7 | import 'package:Tunein/services/musicService.dart'; 8 | import 'package:Tunein/globals.dart'; 9 | 10 | class GridCell extends StatelessWidget { 11 | GridCell(this.song); 12 | final musicService = locator(); 13 | final themeService = locator(); 14 | @required 15 | final Tune song; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return StreamBuilder>( 20 | stream: themeService.getThemeColors(song).asStream(), 21 | builder: (BuildContext context, AsyncSnapshot> snapshot) { 22 | List? songColors; 23 | if (snapshot.hasData) { 24 | songColors = snapshot.data!; 25 | } 26 | return AnimatedContainer( 27 | duration: Duration(milliseconds: 180), 28 | color: MyTheme.darkgrey, 29 | curve: Curves.fastOutSlowIn, 30 | child: Column( 31 | mainAxisSize: MainAxisSize.max, 32 | children: [ 33 | song.albumArt == null 34 | ? Image.asset("images/cover.png") 35 | : Image.file(File(song.albumArt!)), 36 | Expanded( 37 | child: Container( 38 | padding: EdgeInsets.symmetric(horizontal: 20), 39 | width: double.infinity, 40 | color: songColors != null 41 | ? new Color(songColors[0]).withAlpha(225) 42 | : MyTheme.darkgrey, 43 | child: Column( 44 | mainAxisSize: MainAxisSize.max, 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: [ 47 | Padding( 48 | padding: const EdgeInsets.only(bottom: 8), 49 | child: Text( 50 | song.title!, 51 | overflow: TextOverflow.ellipsis, 52 | style: TextStyle( 53 | fontSize: 13.5, 54 | color: (songColors != null 55 | ? new Color(songColors[1]) 56 | : Colors.white70) 57 | .withOpacity(.7), 58 | ), 59 | ), 60 | ), 61 | Text( 62 | song.artist!, 63 | overflow: TextOverflow.ellipsis, 64 | style: TextStyle( 65 | fontSize: 12.5, 66 | color: (songColors != null 67 | ? new Color(songColors[1]) 68 | : Colors.white70) 69 | .withOpacity(.7)), 70 | ), 71 | ], 72 | ), 73 | ), 74 | ) 75 | ], 76 | )); 77 | }, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/components/itemListDevider.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ItemListDevider extends StatelessWidget { 5 | final TextStyle? textStyle; 6 | final double? height; 7 | final String? DeviderTitle; 8 | final Color? backgroundColor; 9 | final String? secondaryTitle; 10 | 11 | const ItemListDevider( 12 | {this.textStyle, 13 | this.height, 14 | this.DeviderTitle, 15 | this.backgroundColor, 16 | this.secondaryTitle}); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Container( 21 | child: Padding( 22 | child: Row( 23 | children: [ 24 | Text( 25 | DeviderTitle ?? "Albums", 26 | style: textStyle ?? 27 | TextStyle( 28 | fontSize: 15.5, 29 | color: MyTheme.grey300, 30 | fontWeight: FontWeight.w800, 31 | letterSpacing: 1.5, 32 | ), 33 | textAlign: TextAlign.left, 34 | ), 35 | if (secondaryTitle != null) 36 | Padding( 37 | child: Text( 38 | secondaryTitle ?? "Albums", 39 | style: textStyle ?? 40 | TextStyle( 41 | fontSize: 10, 42 | color: MyTheme.grey300, 43 | fontWeight: FontWeight.w400, 44 | fontStyle: FontStyle.italic, 45 | letterSpacing: 1.25, 46 | ), 47 | strutStyle: StrutStyle( 48 | forceStrutHeight: true, 49 | height: 1.3, 50 | fontStyle: FontStyle.italic), 51 | textAlign: TextAlign.left, 52 | ), 53 | padding: EdgeInsets.only(left: 5), 54 | ) 55 | ], 56 | ), 57 | padding: EdgeInsets.all(8).add(EdgeInsets.only(top: 2, left: 4)), 58 | ), 59 | color: backgroundColor ?? MyTheme.bgBottomBar, 60 | constraints: BoxConstraints.expand(height: height ?? 35), 61 | ); 62 | } 63 | } 64 | 65 | class DynamicSliverHeaderDelegate extends SliverPersistentHeaderDelegate { 66 | final Widget child; 67 | final double maxHeight; 68 | final double minHeight; 69 | 70 | const DynamicSliverHeaderDelegate({ 71 | required this.child, 72 | this.maxHeight = 250, 73 | this.minHeight = 80, 74 | }); 75 | 76 | @override 77 | Widget build( 78 | BuildContext context, double shrinkOffset, bool overlapsContent) { 79 | return child; 80 | } 81 | 82 | // @override 83 | // bool shouldRebuild(DynamicSliverHeaderDelegate oldDelegate) => true; 84 | 85 | @override 86 | bool shouldRebuild(DynamicSliverHeaderDelegate oldDelegate) { 87 | return maxHeight != oldDelegate.maxHeight || 88 | minHeight != oldDelegate.minHeight || 89 | child != oldDelegate.child; 90 | } 91 | 92 | @override 93 | double get maxExtent => maxHeight; 94 | 95 | @override 96 | double get minExtent => minHeight; 97 | } 98 | -------------------------------------------------------------------------------- /lib/components/pageheader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../globals.dart'; 4 | 5 | class PageHeader extends StatelessWidget { 6 | final String title; 7 | final String subTitle; 8 | final MapEntry icon; 9 | final bool hideText; 10 | PageHeader(this.title, this.subTitle, this.icon, {this.hideText=false}); 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | padding: const EdgeInsets.only(left: 5), 15 | child: Container( 16 | height: 60, 17 | child: Row( 18 | mainAxisSize: MainAxisSize.max, 19 | children: [ 20 | Padding( 21 | padding: const EdgeInsets.only(right: 5), 22 | child: Container( 23 | // color: Colors.red, 24 | alignment: Alignment.center, 25 | width: 50, 26 | child: Icon( 27 | this.icon.key, 28 | color: this.icon.value, 29 | size: 30, 30 | ), 31 | ), 32 | ), 33 | !this.hideText?Flexible( 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | mainAxisAlignment: MainAxisAlignment.center, 37 | children: [ 38 | Padding( 39 | padding: const EdgeInsets.only(bottom: 8), 40 | child: Text( 41 | title, 42 | overflow: TextOverflow.ellipsis, 43 | style: TextStyle( 44 | fontSize: 14, 45 | fontWeight: FontWeight.w600, 46 | color: MyTheme.grey700, 47 | ), 48 | ), 49 | ), 50 | Text( 51 | subTitle, 52 | overflow: TextOverflow.ellipsis, 53 | style: TextStyle( 54 | fontSize: 12, 55 | fontWeight: FontWeight.w400, 56 | color: Colors.white54, 57 | ), 58 | ), 59 | ], 60 | ), 61 | ):Container(), 62 | ], 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/components/pagenavheader.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/pagenavheaderitem.dart'; 2 | import 'package:Tunein/globals.dart'; 3 | import 'package:Tunein/services/layout.dart'; 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:Tunein/values/lists.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class PageNavHeader extends StatefulWidget { 9 | final int pageIndex; 10 | 11 | PageNavHeader({super.key, required this.pageIndex}); 12 | 13 | _PageNavHeaderState createState() => _PageNavHeaderState(); 14 | } 15 | 16 | class _PageNavHeaderState extends State { 17 | final layoutService = locator(); 18 | 19 | @override 20 | PageNavHeader get widget => super.widget; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Container( 25 | color: MyTheme.darkBlack, 26 | height: 50, 27 | child: Row( 28 | children: [ 29 | Padding( 30 | padding: const EdgeInsets.only(left: 53), 31 | ), 32 | Expanded( 33 | child: ListView.builder( 34 | padding: 35 | EdgeInsets.only(right: MediaQuery.of(context).size.width), 36 | physics: NeverScrollableScrollPhysics(), 37 | controller: 38 | layoutService.pageServices[widget.pageIndex].headerController, 39 | scrollDirection: Axis.horizontal, 40 | itemCount: headerItems[widget.pageIndex]!.length, 41 | itemBuilder: (context, int index) { 42 | var items = headerItems[widget.pageIndex]; 43 | // if (index == items.length) { 44 | // return Container( 45 | // width: 2000, 46 | // ); 47 | // } 48 | return PageTitle( 49 | pageIndex: widget.pageIndex, 50 | index: index, 51 | key: items![index].value, 52 | title: items[index].key.toUpperCase(), 53 | ); 54 | }, 55 | ), 56 | ) 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/components/pagenavheaderitem.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/services/layout.dart'; 2 | import 'package:Tunein/services/locator.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'dart:math' as math; 5 | 6 | class PageTitle extends StatelessWidget { 7 | final layoutService = locator(); 8 | final String? title; 9 | final int? index; 10 | final int? pageIndex; 11 | PageTitle({ 12 | Key? key, 13 | this.pageIndex, 14 | this.title, 15 | this.index, 16 | }) : super(key: key); 17 | 18 | onAfterBuild(context) { 19 | layoutService.pageServices[pageIndex!].setSize(index!); 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return StreamBuilder( 25 | stream: layoutService.pageServices[pageIndex!].pageIndex$, 26 | builder: (context, AsyncSnapshot snapshot) { 27 | if (!snapshot.hasData) { 28 | return Container(); 29 | } 30 | final double pageValue = snapshot.data!; 31 | 32 | double opacity = 0.24; 33 | int floor = pageValue.floor(); 34 | int ceil = pageValue.ceil(); 35 | 36 | if (index == ceil && index == floor) { 37 | opacity = 1; 38 | } else { 39 | double dx = (ceil - pageValue); 40 | 41 | if (index == floor) { 42 | opacity = math.max(dx, 0.24); 43 | } 44 | if (index == ceil) { 45 | opacity = math.max(1 - dx, 0.24); 46 | } 47 | } 48 | 49 | WidgetsBinding.instance 50 | .addPostFrameCallback((_) => onAfterBuild(context)); 51 | return Container( 52 | // width: 116, 53 | alignment: Alignment.centerLeft, 54 | child: Text( 55 | title!, 56 | style: TextStyle( 57 | color: Colors.white.withOpacity(opacity), 58 | fontSize: 22, 59 | fontWeight: FontWeight.bold, 60 | ), 61 | ), 62 | ); 63 | }, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/components/playlistCell.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:Tunein/plugins/nano.dart'; 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:Tunein/services/musicService.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:Tunein/models/playerstate.dart'; 8 | import 'package:Tunein/globals.dart'; 9 | import 'package:Tunein/models/ContextMenuOption.dart'; 10 | 11 | class PlaylistCell extends StatelessWidget { 12 | final Playlist? playlistItem; 13 | VoidCallback? onTap; 14 | List? choices; 15 | final void Function(ContextMenuOptions)? onContextSelect; 16 | final void Function()? onContextCancel; 17 | PlaylistCell( 18 | {this.playlistItem, 19 | this.onTap, 20 | this.choices, 21 | this.onContextSelect, 22 | this.onContextCancel}); 23 | 24 | final _textColor = Colors.white54; 25 | final _fontWeight = FontWeight.w400; 26 | @override 27 | Widget build(BuildContext context) { 28 | return Container( 29 | color: Colors.transparent, 30 | padding: EdgeInsets.symmetric(vertical: 5), 31 | child: Row( 32 | mainAxisSize: MainAxisSize.max, 33 | children: [ 34 | Expanded( 35 | child: Container( 36 | child: Material( 37 | color: Colors.transparent, 38 | child: InkWell( 39 | splashColor: MyTheme.darkgrey, 40 | child: Container( 41 | constraints: BoxConstraints.expand(), 42 | child: Row( 43 | children: [ 44 | Padding( 45 | padding: EdgeInsets.only(right: 15), 46 | child: SizedBox( 47 | height: 62, 48 | width: 62, 49 | child: FadeInImage( 50 | placeholder: AssetImage('images/track.png'), 51 | fadeInDuration: Duration(milliseconds: 200), 52 | fadeOutDuration: Duration(milliseconds: 100), 53 | image: playlistItem!.covertArt != null 54 | ? FileImage( 55 | new File(playlistItem!.covertArt!), 56 | ) 57 | : AssetImage('images/track.png') 58 | as ImageProvider, 59 | ), 60 | ), 61 | ), 62 | Flexible( 63 | child: Column( 64 | crossAxisAlignment: CrossAxisAlignment.start, 65 | mainAxisAlignment: MainAxisAlignment.center, 66 | children: [ 67 | Padding( 68 | padding: const EdgeInsets.only(bottom: 8), 69 | child: Text( 70 | (playlistItem!.name == null) 71 | ? "Unknon Title" 72 | : playlistItem!.name as String, 73 | overflow: TextOverflow.ellipsis, 74 | style: TextStyle( 75 | fontSize: 13.5, 76 | fontWeight: _fontWeight, 77 | color: Colors.white, 78 | ), 79 | ), 80 | ), 81 | Text( 82 | (playlistItem!.songs == null) 83 | ? "No songs" 84 | : "${playlistItem!.songs!.length} song(s)", 85 | overflow: TextOverflow.ellipsis, 86 | style: TextStyle( 87 | fontSize: 12.5, 88 | fontWeight: _fontWeight, 89 | color: _textColor, 90 | ), 91 | ), 92 | ], 93 | ), 94 | ), 95 | ], 96 | ), 97 | ), 98 | onTap: () { 99 | onTap!(); 100 | }, 101 | ), 102 | ), 103 | ), 104 | flex: 12, 105 | ), 106 | Expanded( 107 | flex: 2, 108 | child: Material( 109 | child: PopupMenuButton( 110 | child: Material( 111 | color: Colors.transparent, 112 | child: InkWell( 113 | splashColor: MyTheme.darkgrey, 114 | radius: 30.0, 115 | child: Padding( 116 | padding: const EdgeInsets.only(right: 10.0), 117 | child: Icon( 118 | IconData(0xea7c, fontFamily: 'boxicons'), 119 | size: 22, 120 | color: Colors.white70, 121 | )), 122 | ), 123 | ), 124 | elevation: 3.2, 125 | onCanceled: () { 126 | print('You have not chosen anything'); 127 | onContextCancel!(); 128 | }, 129 | tooltip: 'Playing options', 130 | onSelected: (ContextMenuOptions choice) { 131 | onContextSelect!(choice); 132 | }, 133 | itemBuilder: (BuildContext context) { 134 | return choices!.map((ContextMenuOptions choice) { 135 | return PopupMenuItem( 136 | value: choice, 137 | child: Text(choice.title), 138 | ); 139 | }).toList(); 140 | }, 141 | ), 142 | color: Colors.transparent, 143 | ), 144 | ) 145 | ], 146 | ), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/components/scrollbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/globals.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MyScrollbar extends StatefulWidget { 5 | final ScrollController? controller; 6 | final Color? color; 7 | bool showFromTheStart; 8 | bool neverHide; 9 | MyScrollbar( 10 | {Key? key, 11 | this.controller, 12 | this.color, 13 | this.showFromTheStart = false, 14 | this.neverHide = false}) 15 | : super(key: key); 16 | _MyScrollbarState createState() => _MyScrollbarState(); 17 | } 18 | 19 | class _MyScrollbarState extends State { 20 | double scrollableAreaWidth = 10; 21 | double thumbWidth = 6; 22 | double thumbHeight = 50; 23 | RenderBox? boxAfterRender; 24 | double thumbPos = 0; 25 | bool showFromTheStart = false; 26 | bool neverHide = false; 27 | bool _offstage = true; 28 | bool _closing = false; 29 | 30 | _onAfterBuild(BuildContext context) { 31 | boxAfterRender = context.findRenderObject() as RenderBox?; 32 | } 33 | 34 | _updateThumbPos(double newPos) { 35 | mounted 36 | ? setState(() { 37 | thumbPos = newPos; 38 | _offstage = false; 39 | }) 40 | : null; 41 | 42 | if (_closing) return; 43 | 44 | _closing = true; 45 | 46 | Future.delayed(Duration(milliseconds: 1500), () { 47 | mounted 48 | ? setState(() { 49 | (neverHide == null || !neverHide) 50 | ? _offstage = true 51 | : _offstage = false; 52 | _closing = false; 53 | }) 54 | : null; 55 | }); 56 | } 57 | 58 | @override 59 | initState() { 60 | widget.controller!.addListener(() { 61 | double offset = widget.controller!.offset; 62 | double p = offset / widget.controller!.position.maxScrollExtent; 63 | double height = boxAfterRender!.size.height; 64 | _updateThumbPos((height - thumbHeight) * p); 65 | }); 66 | this.showFromTheStart = widget.showFromTheStart; 67 | this.neverHide = widget.neverHide; 68 | if (showFromTheStart) { 69 | _offstage = false; 70 | } 71 | super.initState(); 72 | } 73 | 74 | _doMagic(Offset offset, RenderBox box) { 75 | double verticalOffset = offset.dy; 76 | if (verticalOffset < 0) verticalOffset = 0; 77 | if (verticalOffset > box.size.height) verticalOffset = box.size.height; 78 | double p = verticalOffset / box.size.height; 79 | double newSrollPos = widget.controller!.position.maxScrollExtent * p; 80 | widget.controller!.jumpTo(newSrollPos); 81 | double p2 = 82 | widget.controller!.offset / widget.controller!.position.maxScrollExtent; 83 | _updateThumbPos((box.size.height - thumbHeight) * p2); 84 | } 85 | 86 | _onDragUpdate(BuildContext context, DragUpdateDetails updateDetails) { 87 | RenderBox? box = context.findRenderObject() as RenderBox?; 88 | Offset offset = box!.globalToLocal(updateDetails.globalPosition); 89 | _doMagic(offset, box); 90 | } 91 | 92 | _onDragStart(BuildContext context, DragStartDetails startDetails) { 93 | RenderBox? box = context.findRenderObject() as RenderBox?; 94 | Offset offset = box!.globalToLocal(startDetails.globalPosition); 95 | _doMagic(offset, box); 96 | } 97 | 98 | _onDragEnd(BuildContext context, DragEndDetails startDetails) { 99 | // setState(() { 100 | // _offstage = true; 101 | // }); 102 | } 103 | 104 | @override 105 | MyScrollbar get widget => super.widget; 106 | 107 | @override 108 | Widget build(BuildContext context) { 109 | WidgetsBinding.instance.addPostFrameCallback((_) => _onAfterBuild(context)); 110 | 111 | return GestureDetector( 112 | onVerticalDragUpdate: (DragUpdateDetails details) => 113 | _onDragUpdate(context, details), 114 | onVerticalDragStart: (DragStartDetails details) => 115 | _onDragStart(context, details), 116 | onVerticalDragEnd: (DragEndDetails details) => 117 | _onDragEnd(context, details), 118 | child: Container( 119 | color: widget.color != null ? widget.color : MyTheme.darkBlack, 120 | width: scrollableAreaWidth, 121 | child: Align( 122 | alignment: Alignment.centerRight, 123 | child: Column( 124 | mainAxisSize: MainAxisSize.max, 125 | children: [ 126 | Expanded( 127 | child: AnimatedOpacity( 128 | opacity: !_offstage ? 1.0 : 0.0, 129 | duration: Duration(milliseconds: 150), 130 | child: Container( 131 | color: Colors.grey[800], 132 | width: thumbWidth, 133 | child: Stack( 134 | children: [ 135 | Positioned( 136 | top: thumbPos, 137 | left: 0, 138 | right: 0, 139 | child: Container( 140 | alignment: Alignment.center, 141 | color: Colors.white70, 142 | width: thumbWidth, 143 | height: thumbHeight, 144 | ), 145 | ) 146 | ], 147 | ), 148 | ), 149 | ), 150 | ), 151 | ], 152 | ), 153 | ), 154 | ), 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/components/slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/plugins/nano.dart'; 2 | import 'package:Tunein/services/locator.dart'; 3 | import 'package:Tunein/services/musicService.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:Tunein/globals.dart'; 6 | import 'package:Tunein/models/playerstate.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | 9 | class NowPlayingSlider extends StatelessWidget { 10 | final musicService = locator(); 11 | Tune? currentSong; 12 | final List? colors; 13 | 14 | NowPlayingSlider(this.colors, {this.currentSong}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return StreamBuilder>>( 19 | stream: Rx.combineLatest2(musicService.position$, musicService.playerState$, (a, b) => MapEntry(a, b)), 20 | builder: (BuildContext context, AsyncSnapshot>> snapshot) { 21 | if (!snapshot.hasData) { 22 | return Slider( 23 | value: 0, 24 | onChanged: (double value) => null, 25 | activeColor: MyTheme.darkRed, 26 | inactiveColor: Colors.white24, 27 | ); 28 | } 29 | if (snapshot.data!.value.key == PlayerState.stopped) { 30 | return Slider( 31 | value: 0, 32 | onChanged: (double value) => null, 33 | activeColor: MyTheme.darkRed, 34 | inactiveColor: Colors.white24, 35 | ); 36 | } 37 | final Duration _currentDuration = snapshot.data!.key; 38 | final Tune? _currentSong = snapshot.data!.value.value; 39 | final int _millseconds = _currentDuration.inMilliseconds; 40 | final int? _songDurationInMilliseconds = _currentSong != null ? _currentSong.duration : 0; 41 | return Padding( 42 | padding: const EdgeInsets.symmetric(horizontal: 20), 43 | child: Row( 44 | mainAxisSize: MainAxisSize.max, 45 | crossAxisAlignment: CrossAxisAlignment.center, 46 | children: [ 47 | Text( 48 | parseDuration(_currentDuration.inMilliseconds), 49 | style: TextStyle(color: Color(colors![1]!).withOpacity(.7), fontSize: 12, fontWeight: FontWeight.w600), 50 | ), 51 | Expanded( 52 | child: Slider( 53 | min: 0, 54 | max: _songDurationInMilliseconds!.toDouble(), 55 | value: _songDurationInMilliseconds > _millseconds ? _millseconds.toDouble() : _songDurationInMilliseconds.toDouble(), 56 | onChangeStart: (double value) => musicService.invertSeekingState(), 57 | onChanged: (double value) { 58 | final Duration _duration = Duration( 59 | milliseconds: value.toInt(), 60 | ); 61 | musicService.updatePosition(_duration); 62 | }, 63 | onChangeEnd: (double value) { 64 | musicService.invertSeekingState(); 65 | musicService.audioSeek(value / 1000); 66 | }, 67 | activeColor: Color(colors![1]!).withOpacity(.7), 68 | inactiveColor: Color(colors![1]!).withOpacity(.2), 69 | ), 70 | ), 71 | Text( 72 | parseDuration(_songDurationInMilliseconds), 73 | style: TextStyle(color: Color(colors![1]!).withOpacity(.7), fontSize: 12, fontWeight: FontWeight.w600), 74 | ), 75 | ], 76 | ), 77 | ); 78 | }, 79 | ); 80 | } 81 | 82 | String parseDuration(x) { 83 | final double _temp = x / 1000; 84 | final int _minutes = (_temp / 60).floor(); 85 | final int _seconds = (((_temp / 60) - _minutes) * 60).round(); 86 | if (_seconds.toString().length != 1) { 87 | return _minutes.toString() + ":" + _seconds.toString(); 88 | } else { 89 | return _minutes.toString() + ":0" + _seconds.toString(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/components/stageScrollingPhysics.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'dart:math' as math; 4 | 5 | class StageScrollingPhysics extends ScrollPhysics { 6 | /// Creates scroll physics that prevent the scroll offset from exceeding the 7 | /// bounds of the content.. 8 | /// 9 | int currentStage; 10 | List? stages; 11 | StageScrollingPhysics( 12 | {ScrollPhysics? parent, this.stages, this.currentStage = 0}) 13 | : super(parent: parent); 14 | 15 | @override 16 | StageScrollingPhysics applyTo(ScrollPhysics? ancestor) { 17 | return StageScrollingPhysics( 18 | parent: buildParent(ancestor), 19 | stages: this.stages, 20 | currentStage: this.currentStage); 21 | } 22 | 23 | double _getPixels() { 24 | return stages![currentStage]; 25 | } 26 | 27 | double _getTargetPixels( 28 | ScrollPosition position, Tolerance tolerance, double velocity) { 29 | if (velocity < -tolerance.velocity) { 30 | if (currentStage - 1 >= 0) currentStage--; 31 | } else if (velocity > tolerance.velocity) { 32 | if (currentStage + 1 < this.stages!.length) currentStage++; 33 | } 34 | return _getPixels(); 35 | } 36 | 37 | @override 38 | Simulation? createBallisticSimulation( 39 | ScrollMetrics position, double velocity) { 40 | final Tolerance tolerance = this.tolerance; 41 | if (position.outOfRange) { 42 | double? end; 43 | if (position.pixels > position.maxScrollExtent) 44 | end = position.maxScrollExtent; 45 | if (position.pixels < position.minScrollExtent) 46 | end = position.minScrollExtent; 47 | assert(end != null); 48 | return ScrollSpringSimulation( 49 | spring, 50 | position.pixels, 51 | end!, 52 | math.min(0.0, velocity), 53 | tolerance: tolerance, 54 | ); 55 | } 56 | final double target = 57 | _getTargetPixels(position as ScrollPosition, tolerance, velocity); 58 | if (target != position.pixels) 59 | return ScrollSpringSimulation(spring, position.pixels, target, 60 | position.pixels > target ? -200 : 200, 61 | tolerance: tolerance); 62 | return null; 63 | } 64 | 65 | @override 66 | bool get allowImplicitScrolling => false; 67 | } 68 | -------------------------------------------------------------------------------- /lib/components/threeDotPopupMenu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:Tunein/globals.dart'; 4 | import 'package:Tunein/models/ContextMenuOption.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class ThreeDotPopupMenu extends StatelessWidget { 8 | final List? choices; 9 | final Size? screenSize; 10 | final Color? IconColor; 11 | final double? staticOffsetFromBottom; 12 | final void Function(ContextMenuOptions) onContextSelect; 13 | 14 | ThreeDotPopupMenu( 15 | {required this.choices, 16 | required this.screenSize, 17 | this.IconColor, 18 | required this.onContextSelect, 19 | this.staticOffsetFromBottom}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Container( 24 | child: GestureDetector( 25 | child: Material( 26 | color: Colors.transparent, 27 | child: InkWell( 28 | splashColor: MyTheme.darkgrey, 29 | radius: 30.0, 30 | child: Padding( 31 | padding: const EdgeInsets.only(right: 10.0), 32 | child: Icon( 33 | IconData(0xea7c, fontFamily: 'boxicons'), 34 | size: 22, 35 | color: IconColor != null ? IconColor : Colors.white70, 36 | )), 37 | ), 38 | ), 39 | onTapDown: (details) { 40 | List> itemList = 41 | choices!.map((ContextMenuOptions choice) { 42 | return PopupMenuItem( 43 | value: choice, 44 | child: Text(choice.title), 45 | ); 46 | }).toList(); 47 | double YToSubstract = 0.0; 48 | if (details.globalPosition.dy > 49 | screenSize!.height - 210 - 10 * choices!.length) { 50 | YToSubstract = max( 51 | 0.0, 52 | details.globalPosition.dy - 53 | (screenSize!.height - 54 | choices!.length * 30 - 55 | (staticOffsetFromBottom ?? 160))); 56 | } 57 | showPopMenu(context, itemList, 58 | Buttonoffset: details.globalPosition, 59 | ExtraOffset: Offset(-0, -YToSubstract)); 60 | }, 61 | ), 62 | ); 63 | } 64 | 65 | void showPopMenu(context, List> items, 66 | {@required Offset? Buttonoffset, Offset? ExtraOffset}) async { 67 | final RenderBox? overlay = 68 | Overlay.of(context)!.context.findRenderObject() as RenderBox?; 69 | RenderBox box = context.findRenderObject(); 70 | Buttonoffset = box.localToGlobal(box.size.topRight(Offset.zero)); 71 | RelativeRect position = RelativeRect.fromSize( 72 | Rect.fromPoints( 73 | Offset(Buttonoffset.dx + (ExtraOffset != null ? ExtraOffset.dx : 0.0), 74 | Buttonoffset.dy + (ExtraOffset != null ? ExtraOffset.dy : 0.0)), 75 | Offset(Buttonoffset.dx, Buttonoffset.dy), 76 | ), 77 | overlay!.size, 78 | ); 79 | ContextMenuOptions? Choice = await showMenu( 80 | context: context, 81 | position: position, 82 | items: items, 83 | useRootNavigator: true); 84 | if (Choice != null) { 85 | onContextSelect(Choice); 86 | } else { 87 | print("you selected nothing"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/components/trackListDeck.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/pageheader.dart'; 2 | import 'package:Tunein/components/trackListDeckItem.dart'; 3 | import 'package:Tunein/globals.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class TrackListDeck extends StatelessWidget { 7 | List? items; 8 | bool? hideText; 9 | 10 | TrackListDeck({this.items, this.hideText = true}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Material( 15 | color: Colors.transparent, 16 | child: Container( 17 | margin: EdgeInsets.only(left: 10), 18 | decoration: BoxDecoration( 19 | border: Border( 20 | bottom: BorderSide( 21 | width: 0.3, color: MyTheme.darkgrey.withOpacity(.4))), 22 | ), 23 | child: ListView( 24 | itemExtent: (hideText ?? false) ? 65 : 120, 25 | children: items ?? 26 | [ 27 | TrackListDeckItem( 28 | title: "Shuffle", 29 | subtitle: "All Tracks", 30 | icon: Icon(Icons.shuffle), 31 | ), 32 | TrackListDeckItem( 33 | title: "Sort", 34 | subtitle: "All Tracks", 35 | icon: Icon(Icons.sort), 36 | ), 37 | TrackListDeckItem( 38 | title: "Filter", 39 | subtitle: "All Tracks", 40 | icon: Icon(Icons.filter_list), 41 | ) 42 | ], 43 | scrollDirection: Axis.horizontal, 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/globals.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:settings_ui/settings_ui.dart'; 4 | 5 | class MyTheme { 6 | static final darkRed = Color(0xffff245a); 7 | static final darkBlack = Color(0xff111111); 8 | static final grey300 = Color(0xfff6f8fb); 9 | static final grey500 = Color(0xffdee5ec); 10 | static final grey700 = Color(0xfff5f5f5); 11 | static final darkgrey = Color(0xffa3acbd); 12 | static final bgBottomBar = Color(0xff1e1e1e); 13 | static final bgdivider = Color(0xff2c2c2c); 14 | 15 | static final darkTheme = ThemeData( 16 | backgroundColor: darkBlack, 17 | primaryColor: grey700, 18 | colorScheme: const ColorScheme.dark() 19 | ); 20 | static final settingsDarkTheme = SettingsThemeData( 21 | settingsListBackground: MyTheme.darkBlack, 22 | tileDescriptionTextColor: MyTheme.grey300, 23 | titleTextColor: MyTheme.darkRed, 24 | tileHighlightColor: MyTheme.grey300, 25 | settingsTileTextColor: MyTheme.grey300, 26 | leadingIconsColor: MyTheme.darkRed 27 | ); 28 | } 29 | 30 | class MyUtils { 31 | static String getArtists(artists) { 32 | if(artists == null) return "Unknow Artist"; 33 | return artists.split(";").reduce((String a, String b) { 34 | return a + " & " + b; 35 | }); 36 | } 37 | 38 | static dynamic createDelayedPageroute(context, Widget page, Widget exitPage) async { 39 | 40 | Future buildPageAsync() async { 41 | return Future.microtask(() { 42 | return page; 43 | }); 44 | } 45 | 46 | var newPage = await buildPageAsync(); 47 | //var route = MaterialPageRoute(builder: (_) => newPage,); 48 | Navigator.push(context, SlideRightRoute(page: newPage,exitPage: exitPage)); 49 | } 50 | } 51 | 52 | 53 | class SlideRightRoute extends PageRouteBuilder { 54 | final Widget? page; 55 | final Widget? exitPage; 56 | SlideRightRoute({this.page, this.exitPage}) 57 | : super( 58 | pageBuilder: ( 59 | BuildContext context, 60 | Animation animation, 61 | Animation secondaryAnimation, 62 | ) => 63 | page!, 64 | transitionsBuilder: ( 65 | BuildContext context, 66 | Animation animation, 67 | Animation secondaryAnimation, 68 | Widget child, 69 | ) => 70 | Stack( 71 | children: [ 72 | SlideTransition( 73 | position: new Tween( 74 | begin: const Offset(0.0, 0.0), 75 | end: const Offset(-1.0, 0.0), 76 | ).animate(animation), 77 | child: exitPage, 78 | ), 79 | SlideTransition( 80 | position: new Tween( 81 | begin: const Offset(1.0, 0.0), 82 | end: Offset.zero, 83 | ).animate(animation), 84 | child: page, 85 | ) 86 | ], 87 | ), 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/bottomPanel.dart'; 2 | import 'package:Tunein/components/playing.dart'; 3 | import 'package:Tunein/globals.dart'; 4 | import 'package:Tunein/plugins/nano.dart'; 5 | import 'package:Tunein/root.dart'; 6 | import 'package:Tunein/services/layout.dart'; 7 | import 'package:Tunein/services/locator.dart'; 8 | import 'package:flutter/cupertino.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:permission_handler/permission_handler.dart'; 11 | import 'package:sliding_up_panel/sliding_up_panel.dart'; 12 | import 'services/locator.dart'; 13 | import 'services/languageService.dart'; 14 | 15 | Nano nano = Nano(); 16 | 17 | void main() async { 18 | WidgetsFlutterBinding.ensureInitialized(); 19 | await Permission.storage.request(); 20 | PermissionStatus permission = await Permission.storage.request(); 21 | print(permission); 22 | setupLocator(); 23 | runApp(new MyApp()); 24 | } 25 | 26 | class MyApp extends StatelessWidget { 27 | //final LanguageService = locator(); 28 | @override 29 | Widget build(BuildContext context) { 30 | //LanguageService.flutterI18nDelegate.load(null); 31 | return MaterialApp( 32 | debugShowCheckedModeBanner: false, 33 | title: "Tune In Music Player", 34 | localizationsDelegates: [ 35 | //LanguageService.flutterI18nDelegate, 36 | ], 37 | darkTheme: MyTheme.darkTheme, 38 | themeMode: ThemeMode.dark, 39 | home: Wrapper( 40 | child: Column( 41 | mainAxisSize: MainAxisSize.max, 42 | children: [ 43 | Expanded( 44 | child: Root(), 45 | ), 46 | Container( 47 | height: 60, 48 | color: Colors.blue, 49 | ) 50 | ], 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | class Wrapper extends StatelessWidget { 58 | final Widget child; 59 | 60 | final layoutService = locator(); 61 | 62 | Wrapper({super.key, required this.child}); 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return SlidingUpPanel( 67 | panel: 68 | NowPlayingScreen(controller: layoutService.albumPlayerPageController), 69 | controller: layoutService.globalPanelController, 70 | minHeight: 60, 71 | maxHeight: MediaQuery.of(context).size.height, 72 | backdropEnabled: true, 73 | backdropOpacity: 0.5, 74 | parallaxEnabled: true, 75 | onPanelClosed: () { 76 | layoutService.albumPlayerPageController.jumpToPage(1); 77 | }, 78 | onPanelSlide: (value) { 79 | if (value >= 0.3) { 80 | layoutService.onPanelOpen(value); 81 | } 82 | }, 83 | collapsed: Material( 84 | child: BottomPanel(), 85 | ), 86 | body: MaterialApp( 87 | debugShowCheckedModeBanner: false, 88 | title: "Tune In Music Player", 89 | color: MyTheme.darkRed, 90 | home: child, 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/models/ContextMenuOption.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContextMenuOptions { 4 | ContextMenuOptions( 5 | {required this.title, 6 | required this.icon, 7 | required this.function, 8 | required this.id}); 9 | String title; 10 | IconData icon; 11 | VoidCallback function; 12 | int id; 13 | @override 14 | String toString() { 15 | return "ContextMenuOptions{title:${this.title},icon:${this.icon},id:${this.id}"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/models/playback.dart: -------------------------------------------------------------------------------- 1 | enum Playback { 2 | repeatSong, 3 | repeatQueue, 4 | shuffle, 5 | } 6 | -------------------------------------------------------------------------------- /lib/models/playerstate.dart: -------------------------------------------------------------------------------- 1 | enum PlayerState { 2 | playing, 3 | paused, 4 | stopped, 5 | } 6 | -------------------------------------------------------------------------------- /lib/pages/collection/collection.page.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/pagenavheader.dart'; 2 | import 'package:Tunein/pages/collection/favorites.page.dart'; 3 | import 'package:Tunein/pages/collection/playlists.page.dart'; 4 | import 'package:Tunein/services/layout.dart'; 5 | import 'package:Tunein/services/locator.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class CollectionPage extends StatelessWidget { 9 | CollectionPage({Key? key}) : super(key: key); 10 | final layoutService = locator(); 11 | @override 12 | Widget build(BuildContext context) { 13 | return Column( 14 | children: [ 15 | PageNavHeader( 16 | pageIndex: 1, 17 | ), 18 | Flexible( 19 | child: PageView( 20 | physics: AlwaysScrollableScrollPhysics(), 21 | controller: layoutService.pageServices[1].pageViewController, 22 | children: [ 23 | PlaylistsPage(), 24 | FavoritesPage(), 25 | ], 26 | ), 27 | ) 28 | ], 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/pages/collection/favorites.page.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/gridcell.dart'; 2 | import 'package:Tunein/components/pageheader.dart'; 3 | import 'package:Tunein/globals.dart'; 4 | import 'package:Tunein/models/playerstate.dart'; 5 | import 'package:Tunein/plugins/nano.dart'; 6 | import 'package:Tunein/services/locator.dart'; 7 | import 'package:Tunein/services/musicService.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class FavoritesPage extends StatefulWidget { 11 | _FavoritesPageState createState() => _FavoritesPageState(); 12 | } 13 | 14 | class _FavoritesPageState extends State 15 | with AutomaticKeepAliveClientMixin { 16 | final musicService = locator(); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | super.build(context); 21 | var size = MediaQuery.of(context).size; 22 | final double itemWidth = size.width / 2; 23 | return Container( 24 | padding: EdgeInsets.only(bottom: 85), 25 | height: double.infinity, 26 | width: double.infinity, 27 | color: MyTheme.darkBlack, 28 | child: Column( 29 | children: [ 30 | PageHeader( 31 | "Favorites", 32 | "All Tracks", 33 | MapEntry(IconData(0xeaaf, fontFamily: 'boxicons'), Colors.white), 34 | ), 35 | Expanded( 36 | child: StreamBuilder>( 37 | stream: musicService.favorites$, 38 | builder: (context, AsyncSnapshot> snapshot) { 39 | if (!snapshot.hasData) { 40 | return Container(); 41 | } 42 | 43 | final _songs = snapshot.data; 44 | return GridView.builder( 45 | padding: EdgeInsets.all(0), 46 | itemCount: _songs!.length, 47 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 48 | crossAxisCount: 2, 49 | mainAxisSpacing: 3, 50 | crossAxisSpacing: 3, 51 | childAspectRatio: (itemWidth / (itemWidth + 50)), 52 | ), 53 | itemBuilder: (BuildContext context, int index) { 54 | return GestureDetector( 55 | onTap: () { 56 | musicService.updatePlaylist(_songs); 57 | musicService.playOrPause(_songs[index]); 58 | }, 59 | child: GridCell(_songs[index]), 60 | ); 61 | }, 62 | ); 63 | }, 64 | ), 65 | ), 66 | ], 67 | ), 68 | ); 69 | } 70 | 71 | @override 72 | bool get wantKeepAlive => true; 73 | } 74 | -------------------------------------------------------------------------------- /lib/pages/library/albums.page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:Tunein/components/AlbumSongCell.dart'; 4 | import 'package:Tunein/components/gridcell.dart'; 5 | import 'package:Tunein/models/playerstate.dart'; 6 | import 'package:Tunein/services/settingService.dart'; 7 | import 'package:Tunein/services/uiScaleService.dart'; 8 | import 'package:Tunein/values/contextMenus.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:Tunein/components/albumCard.dart'; 11 | import 'package:Tunein/services/locator.dart'; 12 | import 'package:Tunein/services/musicService.dart'; 13 | import 'package:Tunein/plugins/nano.dart'; 14 | import 'package:Tunein/pages/single/singleAlbum.page.dart'; 15 | import 'package:rxdart/rxdart.dart'; 16 | 17 | class AlbumsPage extends StatefulWidget { 18 | AlbumsPage({Key? key, controller}) : super(key: key); 19 | 20 | _AlbumsPageState createState() => _AlbumsPageState(); 21 | } 22 | 23 | class _AlbumsPageState extends State 24 | with AutomaticKeepAliveClientMixin { 25 | final musicService = locator(); 26 | final SettingService = locator(); 27 | BehaviorSubject currentAlbum = new BehaviorSubject(); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | super.build(context); 32 | var size = MediaQuery.of(context).size; 33 | double albumGridCellHeight = uiScaleService.AlbumsGridCellHeight(size); 34 | 35 | return Container( 36 | child: StreamBuilder( 37 | stream: Rx.combineLatest2( 38 | musicService.albums$, 39 | SettingService.getOrCreateSingleSettingStream( 40 | SettingsIds.SET_ALBUM_LIST_PAGE), 41 | (a, b) => MapEntry, String>(a, b!)), 42 | builder: (BuildContext context, 43 | AsyncSnapshot, String>> snapshot) { 44 | if (!snapshot.hasData) { 45 | return Container(); 46 | } 47 | 48 | if (snapshot.data!.key.length == 0) { 49 | return Container(); 50 | } 51 | final _albums = snapshot.data!.key; 52 | Map? UISettings = 53 | SettingService.DeserializeUISettings(snapshot.data!.value); 54 | int itemsPerRow = int.tryParse(UISettings![LIST_PAGE_SettingsIds 55 | .ALBUMS_PAGE_GRID_ROW_ITEM_COUNT] as String) ?? 56 | 3; 57 | int animationDelay = int.tryParse(UISettings[LIST_PAGE_SettingsIds 58 | .ALBUMS_PAGE_BOX_FADE_IN_DURATION] as String) ?? 59 | 150; 60 | final double itemWidth = size.width / itemsPerRow; 61 | return GridView.builder( 62 | padding: EdgeInsets.all(0), 63 | itemCount: _albums.length, 64 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 65 | crossAxisCount: itemsPerRow, 66 | mainAxisSpacing: itemsPerRow.toDouble(), 67 | crossAxisSpacing: itemsPerRow.toDouble(), 68 | childAspectRatio: (itemWidth / (itemWidth + 50)), 69 | ), 70 | itemBuilder: (BuildContext context, int index) { 71 | int newIndex = (index % itemsPerRow) + 2; 72 | return GestureDetector( 73 | onTap: () { 74 | goToAlbumSongsList(_albums[index], context); 75 | }, 76 | child: AlbumGridCell( 77 | _albums[index], 78 | ((albumGridCellHeight * 0.8) / itemsPerRow) * 3, 79 | albumGridCellHeight * 0.20, 80 | animationDelay: (animationDelay * newIndex) - 81 | (index < 6 ? ((6 - index) * 150) : 0), 82 | useAnimation: !(animationDelay == 0), 83 | choices: albumCardContextMenulist, 84 | onContextSelect: (choice) { 85 | switch (choice.id) { 86 | case 1: 87 | { 88 | musicService.playEntireAlbum(_albums[index]); 89 | break; 90 | } 91 | case 2: 92 | { 93 | musicService.shuffleEntireAlbum(_albums[index]); 94 | break; 95 | } 96 | } 97 | }, 98 | Screensize: size, 99 | onContextCancel: (option) { 100 | print("canceled"); 101 | }, 102 | ), 103 | ); 104 | }, 105 | ); 106 | }, 107 | ), 108 | ); 109 | } 110 | 111 | void goToAlbumSongsList(album, context) async { 112 | Size screenSize = MediaQuery.of(context).size; 113 | List returnedSongs = await Navigator.of(context).push( 114 | MaterialPageRoute( 115 | builder: (context) => SingleAlbumPage( 116 | null, 117 | album: album, 118 | heightToSubstract: 60, 119 | ), 120 | ), 121 | ); 122 | } 123 | 124 | @override 125 | bool get wantKeepAlive { 126 | return true; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/pages/library/artists.page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:Tunein/components/AlbumSongCell.dart'; 4 | import 'package:Tunein/components/ArtistCell.dart'; 5 | import 'package:Tunein/components/gridcell.dart'; 6 | import 'package:Tunein/models/playerstate.dart'; 7 | import 'package:Tunein/pages/single/singleArtistPage.dart'; 8 | import 'package:Tunein/services/settingService.dart'; 9 | import 'package:Tunein/services/uiScaleService.dart'; 10 | import 'package:Tunein/values/contextMenus.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:Tunein/components/albumCard.dart'; 13 | import 'package:Tunein/services/locator.dart'; 14 | import 'package:Tunein/services/musicService.dart'; 15 | import 'package:Tunein/plugins/nano.dart'; 16 | import 'package:Tunein/pages/single/singleAlbum.page.dart'; 17 | import 'package:rxdart/rxdart.dart'; 18 | import 'package:animations/animations.dart'; 19 | 20 | class ArtistsPage extends StatefulWidget { 21 | PageController controller; 22 | ArtistsPage({Key? key, controller}) 23 | : this.controller = 24 | controller != null ? controller : new PageController(), 25 | super(key: key); 26 | 27 | _ArtistsPageState createState() => _ArtistsPageState(); 28 | } 29 | 30 | class _ArtistsPageState extends State 31 | with AutomaticKeepAliveClientMixin { 32 | final musicService = locator(); 33 | final SettingService = locator(); 34 | 35 | BehaviorSubject currentAlbum = new BehaviorSubject(); 36 | ContainerTransitionType transitionType = ContainerTransitionType.fadeThrough; 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | super.build(context); 41 | var size = MediaQuery.of(context).size; 42 | double artistGridCellHeight = uiScaleService.ArtistGridCellHeight(size); 43 | BehaviorSubject settingStream = SettingService.getOrCreateSingleSettingStream( 44 | SettingsIds.SET_ALBUM_LIST_PAGE); 45 | return Container( 46 | child: StreamBuilder( 47 | stream: Rx.combineLatest2( 48 | musicService.artists$, 49 | settingStream, 50 | (a, b) => MapEntry, String>(a, b!)), 51 | builder: (BuildContext context, 52 | AsyncSnapshot, String>> snapshot) { 53 | if (!snapshot.hasData) { 54 | return Container(); 55 | } 56 | if (snapshot.data!.key.length == 0) { 57 | return Container(); 58 | } 59 | final _artists = snapshot.data!.key; 60 | Map? UISettings = 61 | SettingService.DeserializeUISettings(snapshot.data!.value); 62 | int itemsPerRow = int.tryParse(UISettings![LIST_PAGE_SettingsIds 63 | .ARTISTS_PAGE_GRID_ROW_ITEM_COUNT] as String) ?? 64 | 3; 65 | int animationDelay = int.tryParse(UISettings[LIST_PAGE_SettingsIds 66 | .ARTISTS_PAGE_BOX_FADE_IN_DURATION] as String) ?? 67 | 150; 68 | final double itemWidth = size.width / itemsPerRow; 69 | return GridView.builder( 70 | padding: EdgeInsets.all(0), 71 | itemCount: _artists.length, 72 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 73 | crossAxisCount: itemsPerRow, 74 | mainAxisSpacing: itemsPerRow.toDouble(), 75 | crossAxisSpacing: itemsPerRow.toDouble(), 76 | childAspectRatio: (itemWidth / (itemWidth + 50)), 77 | ), 78 | itemBuilder: (BuildContext context, int index) { 79 | int newIndex = (index % itemsPerRow) + (itemsPerRow - 1); 80 | return GestureDetector( 81 | onTap: () { 82 | goToSingleArtistPage(_artists[index]); 83 | }, 84 | child: ArtistGridCell( 85 | _artists[index], 86 | ((artistGridCellHeight * 0.75) / itemsPerRow) * 3, 87 | artistGridCellHeight * 0.25, 88 | choices: artistCardContextMenulist, 89 | animationDelay: (animationDelay * newIndex) - 90 | (index < 6 ? ((6 - index) * 150) : 0), 91 | useAnimation: animationDelay != 0, 92 | onContextSelect: (choice) { 93 | switch (choice.id) { 94 | case 1: 95 | { 96 | musicService.playAllArtistAlbums(_artists[index]); 97 | break; 98 | } 99 | case 2: 100 | { 101 | musicService.suffleAllArtistAlbums(_artists[index]); 102 | break; 103 | } 104 | } 105 | }, 106 | onContextCancel: (choice) { 107 | print("Cancelled"); 108 | }, 109 | Screensize: size, 110 | ), 111 | ); 112 | }, 113 | ); 114 | }, 115 | ), 116 | ); 117 | } 118 | 119 | void goToSingleArtistPage(Artist artist) { 120 | Navigator.of(context).push( 121 | MaterialPageRoute( 122 | builder: (context) => SingleArtistPage( 123 | artist, 124 | heightToSubstract: 60, 125 | ), 126 | ), 127 | ); 128 | } 129 | 130 | @override 131 | bool get wantKeepAlive { 132 | return true; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/pages/library/library.page.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/customPageView.dart'; 2 | import 'package:Tunein/components/pagenavheader.dart'; 3 | import 'package:Tunein/globals.dart'; 4 | import 'package:Tunein/pages/library/albums.page.dart'; 5 | import 'package:Tunein/pages/library/artists.page.dart'; 6 | import 'package:Tunein/pages/library/tracks.page.dart'; 7 | import 'package:Tunein/pages/single/LandingPage.dart'; 8 | import 'package:Tunein/services/layout.dart'; 9 | import 'package:Tunein/services/locator.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class LibraryPage extends StatelessWidget { 13 | LibraryPage({Key? key}) : super(key: key); 14 | final layoutService = locator(); 15 | @override 16 | Widget build(BuildContext context) { 17 | var children = [ 18 | LandingPage(), 19 | TracksPage(), 20 | ArtistsPage(), 21 | AlbumsPage(controller: layoutService.albumListPageController), 22 | ]; 23 | Widget shallowWidget; 24 | shallowWidget = Container( 25 | height: 200, 26 | color: MyTheme.darkgrey.withOpacity(.01), 27 | ); 28 | return Column( 29 | children: [ 30 | PageNavHeader( 31 | pageIndex: 0, 32 | ), 33 | Flexible( 34 | child: StreamBuilder( 35 | stream: Future.delayed(Duration(milliseconds: 100), () => true) 36 | .asStream(), 37 | builder: (context, AsyncSnapshot snapshot) { 38 | return AnimatedSwitcher( 39 | duration: Duration(milliseconds: 300), 40 | child: snapshot.hasData 41 | ? CustomPageView( 42 | shallowWidget: Container(color: MyTheme.bgBottomBar), 43 | pages: children, 44 | controller: 45 | layoutService.pageServices[0].pageViewController, 46 | ) 47 | : shallowWidget, 48 | ); 49 | }, 50 | ), 51 | ) 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/plugins/AudioPluginService.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:isolate'; 3 | 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:Tunein/services/isolates/musicServiceIsolate.dart'; 6 | import 'package:audioplayer/audioplayer.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:rxdart/rxdart.dart'; 9 | 10 | class AudioPluginService { 11 | final MusicServiceIsolate = locator(); 12 | 13 | Future sendNewIsolateCommand({required String command, String message = ""}) { 14 | ReceivePort tempPort = ReceivePort(); 15 | MusicServiceIsolate.sendCrossPluginIsolatesMessage( 16 | CrossIsolatesMessage( 17 | sender: tempPort.sendPort, command: command, message: message)); 18 | return tempPort.forEach((data) { 19 | if (data != "OK") { 20 | tempPort.close(); 21 | return data; 22 | } 23 | }); 24 | } 25 | 26 | Future playSong(String uri, 27 | {required String? album, 28 | required String? title, 29 | required String? artist, 30 | required String? albumArt}) { 31 | return sendNewIsolateCommand( 32 | command: "playMusic", 33 | message: json.encode({ 34 | 'uri': uri, 35 | 'album': album, 36 | 'title': title, 37 | 'artist': artist, 38 | 'albumArt': albumArt, 39 | })); 40 | } 41 | 42 | Future pauseSong() { 43 | return sendNewIsolateCommand(command: "pauseMusic"); 44 | } 45 | 46 | Future stopSong() { 47 | return sendNewIsolateCommand(command: "stopMusic"); 48 | } 49 | 50 | Future seek(double seconds) { 51 | return sendNewIsolateCommand( 52 | command: "seekMusic", message: seconds.toString()); 53 | } 54 | 55 | Future useNotification({bool? useNotification, bool? cancelWhenNotPlaying}) { 56 | return sendNewIsolateCommand( 57 | command: "useAndroidNotification", 58 | message: json.encode({ 59 | 'useNotification': useNotification, 60 | 'cancelWhenNotPlaying': cancelWhenNotPlaying 61 | })); 62 | } 63 | 64 | Future showNotification() { 65 | return sendNewIsolateCommand( 66 | command: "showAndroidNotification", message: ""); 67 | } 68 | 69 | Future hideNotification() { 70 | return sendNewIsolateCommand( 71 | command: "hideAndroidNotification", message: ""); 72 | } 73 | 74 | Future setItem( 75 | {required String album, 76 | required String title, 77 | required String artist, 78 | required String albumArt, 79 | required String uri}) { 80 | return sendNewIsolateCommand( 81 | command: "setItem", 82 | message: json.encode({ 83 | 'uri': uri, 84 | 'album': album, 85 | 'title': title, 86 | 'artist': artist, 87 | 'albumArt': albumArt, 88 | })); 89 | } 90 | 91 | BehaviorSubject subscribeToPositionChanges() { 92 | ReceivePort tempPort = ReceivePort(); 93 | MusicServiceIsolate.sendCrossPluginIsolatesMessage( 94 | CrossIsolatesMessage( 95 | sender: tempPort.sendPort, 96 | command: "subscribeToPosition", 97 | message: "")); 98 | 99 | BehaviorSubject returnedSubject = 100 | new BehaviorSubject.seeded(Duration(milliseconds: 0)); 101 | tempPort.forEach((data) { 102 | if (data != null && data != "OK") { 103 | returnedSubject.add(data); 104 | } 105 | }); 106 | 107 | return returnedSubject; 108 | } 109 | 110 | BehaviorSubject subscribeToStateChanges() { 111 | ReceivePort tempPort = ReceivePort(); 112 | MusicServiceIsolate.sendCrossPluginIsolatesMessage( 113 | CrossIsolatesMessage( 114 | sender: tempPort.sendPort, 115 | command: "subscribeToState", 116 | message: "")); 117 | 118 | BehaviorSubject returnedSubject = 119 | new BehaviorSubject.seeded(null); 120 | tempPort.forEach((data) { 121 | if (data != null && data != "OK") { 122 | returnedSubject.add(_deserializeAudioPlayerStateEnum(data)); 123 | } 124 | }); 125 | 126 | return returnedSubject; 127 | } 128 | 129 | BehaviorSubject subscribeToPlaybackKeys() { 130 | ReceivePort tempPort = ReceivePort(); 131 | MusicServiceIsolate.sendCrossPluginIsolatesMessage( 132 | CrossIsolatesMessage( 133 | sender: tempPort.sendPort, 134 | command: "subscribeToplaybackKeys", 135 | message: "")); 136 | 137 | BehaviorSubject returnedSubject = 138 | new BehaviorSubject.seeded(null); 139 | tempPort.forEach((data) { 140 | if (data != null && data != "OK") { 141 | returnedSubject.add(_deserializePlaybackKeysEnum(data)); 142 | } 143 | }); 144 | 145 | return returnedSubject; 146 | } 147 | 148 | AudioPlayerState _deserializeAudioPlayerStateEnum(String entry) { 149 | List enumStringList = entry.split("."); 150 | switch (enumStringList[1]) { 151 | case "COMPLETED": 152 | { 153 | return AudioPlayerState.COMPLETED; 154 | } 155 | case "PLAYING": 156 | { 157 | return AudioPlayerState.PLAYING; 158 | } 159 | case "PAUSED": 160 | { 161 | return AudioPlayerState.PAUSED; 162 | } 163 | case "STOPPED": 164 | { 165 | return AudioPlayerState.STOPPED; 166 | } 167 | } 168 | throw Exception; 169 | } 170 | 171 | PlayBackKeys _deserializePlaybackKeysEnum(String entry) { 172 | List enumStringList = entry.split("."); 173 | switch (enumStringList[1]) { 174 | case "PAUSE_KEY": 175 | { 176 | return PlayBackKeys.PAUSE_KEY; 177 | } 178 | case "PLAY_KEY": 179 | { 180 | return PlayBackKeys.PLAY_KEY; 181 | } 182 | case "NEXT_KEY": 183 | { 184 | return PlayBackKeys.NEXT_KEY; 185 | } 186 | case "REWIND_KEY": 187 | { 188 | return PlayBackKeys.REWIND_KEY; 189 | } 190 | case "STOP_KEY": 191 | { 192 | return PlayBackKeys.STOP_KEY; 193 | } 194 | case "SEEK_KEY": 195 | { 196 | return PlayBackKeys.SEEK_KEY; 197 | } 198 | case "FAST_FORWARD_KEY": 199 | { 200 | return PlayBackKeys.FAST_FORWARD_KEY; 201 | } 202 | } 203 | throw Exception; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/plugins/AudioReceiverService.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:Tunein/utils/MathUtils.dart'; 4 | import 'package:audioplayer/audioplayer.dart'; 5 | 6 | class AudioReceiverService { 7 | AudioPlayer _audioPlayer = AudioPlayer(); 8 | Map _audioPositionSub = 9 | new Map(); 10 | Map _audioStateChangeSub = 11 | new Map(); 12 | Map _audioPlaybkacKeysSub = 13 | new Map(); 14 | 15 | AudioReceiverService(); 16 | 17 | Future playSong(String uri, 18 | {required String? album, 19 | required String? title, 20 | required String? artist, 21 | required String? albumArt}) { 22 | return _audioPlayer.play(uri, 23 | title: title, album: album, albumArt: albumArt, author: artist); 24 | } 25 | 26 | Future setItem( 27 | {required String uri, 28 | required String album, 29 | required String title, 30 | required String artist, 31 | required String albumArt}) { 32 | return _audioPlayer.setItem( 33 | uri: uri, 34 | title: title, 35 | album: album, 36 | albumArt: albumArt, 37 | author: artist); 38 | } 39 | 40 | Future pauseSong() { 41 | return _audioPlayer.pause(); 42 | } 43 | 44 | Future stopSong() { 45 | return _audioPlayer.stop(); 46 | } 47 | 48 | Future seek(double seconds) { 49 | return _audioPlayer.seek(seconds); 50 | } 51 | 52 | StreamSubscription? onPositionChanges(Function(Duration) callback) { 53 | String uID = MathUtils.getUniqueId(); 54 | _audioPositionSub[uID] = 55 | _audioPlayer.onAudioPositionChanged.listen((Duration duration) { 56 | callback(duration); 57 | }); 58 | return _audioPositionSub[uID]; 59 | } 60 | 61 | StreamSubscription? onStateChanges(Function(String) callback) { 62 | String uID = MathUtils.getUniqueId(); 63 | _audioStateChangeSub[uID] = 64 | _audioPlayer.onPlayerStateChanged.listen((AudioPlayerState state) { 65 | callback(serializeEnums(state)); 66 | }); 67 | return _audioStateChangeSub[uID]; 68 | } 69 | 70 | StreamSubscription? onPlaybackKeys(Function(String) callback) { 71 | String uID = MathUtils.getUniqueId(); 72 | _audioPlaybkacKeysSub[uID] = 73 | _audioPlayer.onPlaybackKeyEvent.listen((PlayBackKeys data) { 74 | callback(serializeEnums(data)); 75 | }); 76 | return _audioPlaybkacKeysSub[uID]; 77 | } 78 | 79 | closeAllSubs() { 80 | if (_audioPlaybkacKeysSub != null) 81 | _audioPlaybkacKeysSub.forEach((key, element) { 82 | element.cancel(); 83 | _audioPlaybkacKeysSub.remove(key); 84 | }); 85 | if (_audioPositionSub != null) 86 | _audioPositionSub.forEach((key, element) { 87 | element.cancel(); 88 | _audioPositionSub.remove(key); 89 | }); 90 | if (_audioStateChangeSub != null) 91 | _audioStateChangeSub.forEach((key, element) { 92 | element.cancel(); 93 | _audioStateChangeSub.remove(key); 94 | }); 95 | } 96 | 97 | serializeEnums(entry) { 98 | return entry.toString(); 99 | } 100 | 101 | useNotification({bool? useNotification, bool? cancelWhenPlayingStops}) { 102 | return _audioPlayer.useNotificationMediaControls( 103 | useNotification!, cancelWhenPlayingStops!); 104 | } 105 | 106 | showNotification() { 107 | return _audioPlayer.showNotificationMediaControls(); 108 | } 109 | 110 | hideNotification() { 111 | return _audioPlayer.hideNotificationMediaControls(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/plugins/ThemeReceiverService.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:Tunein/plugins/nano.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | import 'package:tunein_image_utils_plugin/tunein_image_utils_plugin.dart'; 7 | 8 | class ThemeReceiverService { 9 | late BehaviorSubject> _color$; 10 | BehaviorSubject> get color$ => _color$; 11 | 12 | late Map> _savedColors; 13 | late Map> _artistSavedColors; 14 | late List defaultColors = [0xff111111, 0xffffffff, 0xffffffff]; 15 | 16 | ThemeReceiverService() { 17 | _initStreams(); 18 | _savedColors = Map>(); 19 | _artistSavedColors = Map>(); 20 | } 21 | 22 | Future updateTheme(String? songId, String? songArt) async { 23 | if (_savedColors.containsKey(songId)) { 24 | _color$.add(_savedColors[songId]!); 25 | return; 26 | } 27 | 28 | String? path = songArt; 29 | if (path == null) { 30 | _color$.add([0xff111111, 0xffffffff]); 31 | return; 32 | } 33 | 34 | final colors = await TuneinImageUtilsPlugin.getColor(path); 35 | List _colors = []; 36 | for (var color in colors) { 37 | _colors.add(color); 38 | } 39 | _color$.add(_colors); 40 | _savedColors[songId] = _colors; 41 | 42 | return; 43 | } 44 | 45 | Future> getThemeColors(String? songId, String? songArt) async { 46 | List color = []; 47 | if (_savedColors.containsKey(songId)) { 48 | color.addAll(_savedColors[songId]!); 49 | 50 | return color; 51 | } 52 | 53 | String? path = songArt; 54 | 55 | if (path == null) { 56 | color.addAll(defaultColors); 57 | return color; 58 | } 59 | print(path); 60 | final colors = await TuneinImageUtilsPlugin.getColor(path); 61 | 62 | List _colors = []; 63 | for (var color in colors) { 64 | _colors.add(color); 65 | } 66 | if (_colors.length < 3) { 67 | do { 68 | _colors.add(_colors[1]); 69 | } while (_colors.length < 3); 70 | } 71 | color.addAll(_colors); 72 | _savedColors[songId] = _colors; 73 | 74 | return color; 75 | } 76 | 77 | Future> getArtistColors( 78 | int artistID, String? artistCoverArtPath) async { 79 | List color = []; 80 | if (_artistSavedColors.containsKey(artistID)) { 81 | color.addAll(_artistSavedColors[artistID]!); 82 | 83 | return color; 84 | } 85 | 86 | String? path = artistCoverArtPath; 87 | 88 | if (path == null) { 89 | color.addAll(defaultColors); 90 | return color; 91 | } 92 | 93 | final colors = await TuneinImageUtilsPlugin.getColor(path); 94 | 95 | List _colors = []; 96 | for (var color in colors) { 97 | _colors.add(color); 98 | } 99 | if (_colors.length < 3) { 100 | do { 101 | _colors.add(_colors[1]); 102 | } while (_colors.length < 3); 103 | } 104 | color.addAll(_colors); 105 | _artistSavedColors[artistID.toString()] = _colors; 106 | 107 | return color; 108 | } 109 | 110 | Future execute(String caller, dynamic arguments) { 111 | switch (caller) { 112 | case "getArtistColors": 113 | { 114 | return this 115 | .getArtistColors(arguments["artistId"], arguments["coverArt"]); 116 | } 117 | case "getThemeColors": 118 | { 119 | return this 120 | .getThemeColors(arguments["songId"], arguments["coverArt"]); 121 | } 122 | case "updateTheme": 123 | { 124 | return this.updateTheme(arguments["songId"], arguments["coverArt"]); 125 | } 126 | } 127 | throw Exception; 128 | } 129 | 130 | void _initStreams() { 131 | _color$ = BehaviorSubject>.seeded([0xff111111, 0xffffffff]); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/services/http/httpRequests.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:Tunein/services/locator.dart'; 3 | import 'package:Tunein/services/settingService.dart'; 4 | import 'package:dio_flutter_transformer2/dio_flutter_transformer2.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class httpRequests { 8 | Dio instance = new Dio(); 9 | 10 | httpRequests() { 11 | instance.transformer = FlutterTransformer(); 12 | } 13 | 14 | Future get( 15 | {required String? url, 16 | Map? data, 17 | Map? headers, 18 | int? timeout}) async { 19 | try { 20 | Response response = await Dio().get(url!, 21 | options: Options(headers: headers, sendTimeout: timeout), 22 | queryParameters: data); 23 | print(response); 24 | return response; 25 | } catch (e) { 26 | print(e); 27 | throw e; 28 | } 29 | } 30 | 31 | Future post( 32 | {@required String? url, 33 | Map? data, 34 | Map? headers, 35 | int? timeout}) async { 36 | try { 37 | Response response = await Dio().post( 38 | url!, 39 | options: Options(headers: headers, sendTimeout: timeout), 40 | data: data, 41 | ); 42 | print(response); 43 | return response; 44 | } catch (e) { 45 | print(e); 46 | throw e; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/services/http/requests.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:Tunein/plugins/nano.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:Tunein/services/http/httpRequests.dart'; 6 | import 'package:Tunein/services/locator.dart'; 7 | import 'package:Tunein/services/settingService.dart'; 8 | 9 | class Requests { 10 | final requestService = locator(); 11 | final SettingsService = locator(); 12 | 13 | Map? Settings = new Map(); 14 | StreamSubscription? settingStreamSubscription; 15 | 16 | Requests() { 17 | settingStreamSubscription = SettingsService.settings$.listen((data) { 18 | Settings = data; 19 | }); 20 | } 21 | 22 | static String SPOTIFY_SEARCH_URL = "https://api.spotify.com/v1/search"; 23 | static String SPOTIFY_API_KEY = "https://api.spotify.com/v1/search"; 24 | static String ARTIST_DATA_URL = 25 | "https://api.spotify.com/v1/artists/0OdUWJ0sBjDrqHygGUXeCF"; 26 | 27 | static String LAST_FOM_API_KEY = "abeaa955cfeb92a1c9e3ca52bebb120f"; 28 | 29 | static String DISCOGS_SEARCH_URL = "https://api.discogs.com/database/search"; 30 | static String DISCOGS_ARTIST_URL = "https://api.discogs.com/artists/"; 31 | static String DISCOGS_API_TOKEN = "xhvYJGwbYCsfKYrbGisBLoNlowOsnZSRUrBAStCR"; 32 | 33 | Future spotifySearch(String searchTerm, {String type = "artist"}) async { 34 | Response requestResqponse = 35 | await requestService.get(url: SPOTIFY_SEARCH_URL, data: { 36 | "q": searchTerm, 37 | "type": type, 38 | }, headers: { 39 | "Authorization": "Bearer " + SPOTIFY_API_KEY 40 | }); 41 | 42 | if (requestResqponse.data != null) { 43 | return requestResqponse.data; 44 | } 45 | } 46 | 47 | Future discogsSearch(String searchTerm, {String type = "artist"}) async { 48 | Response requestResqponse = await requestService.get( 49 | url: DISCOGS_SEARCH_URL, 50 | data: { 51 | "q": searchTerm, 52 | "type": type, 53 | "token": Settings![SettingsIds.SET_DISCOG_API_KEY] 54 | }, 55 | ); 56 | 57 | if (requestResqponse.data != null) { 58 | print(requestResqponse.data); 59 | return requestResqponse.data; 60 | } 61 | } 62 | 63 | Future? getArtistDataFromDiscogs(Artist artist) async { 64 | if (artist.name == null) return null; 65 | 66 | if (artist.apiData["discogID"] == null) { 67 | print("not gone use discogID"); 68 | dynamic result = await discogsSearch(artist.name!); 69 | //discogs specific response schema 70 | if (result["results"].length == 0) { 71 | return null; 72 | } 73 | //by default the most accurate result from the search is the first one 74 | //This could be added as a configuration option in the future 75 | return result["results"][0]; 76 | } else { 77 | print("gone use discogID"); 78 | Response requestResqponse = await requestService.get( 79 | url: DISCOGS_ARTIST_URL + artist.apiData["discogID"]!, 80 | data: {"token": DISCOGS_API_TOKEN}); 81 | if (requestResqponse.data != null) { 82 | return requestResqponse.data; 83 | } else { 84 | return Future.value(null); 85 | } 86 | } 87 | } 88 | 89 | Future getDiscogArtistData(Artist artist) async { 90 | if (artist.name == null) return null; 91 | if (artist.apiData["discogID"] == null) { 92 | dynamic result = await discogsSearch(artist.name!); 93 | //discogs specific response schema 94 | if (result["results"].length == 0) { 95 | return null; 96 | } 97 | //by default the most accurate result from the search is the first one 98 | //This could be added as a configuration option in the future 99 | int id = result["results"][0]["id"]; 100 | Response requestResqponse = await requestService.get( 101 | url: DISCOGS_ARTIST_URL + id.toString(), 102 | data: {"token": DISCOGS_API_TOKEN}); 103 | if (requestResqponse.data != null) { 104 | return requestResqponse.data; 105 | } else { 106 | return null; 107 | } 108 | } else { 109 | Response requestResqponse = await requestService.get( 110 | url: DISCOGS_ARTIST_URL + artist.apiData["discogID"]!, 111 | data: {"token": DISCOGS_API_TOKEN}); 112 | if (requestResqponse.data != null) { 113 | return requestResqponse.data; 114 | } else { 115 | return null; 116 | } 117 | } 118 | } 119 | 120 | Future pingURL(String? url, {Duration? timeout}) async { 121 | if (url != null) { 122 | Response requestResponse = await requestService.get( 123 | url: url, 124 | timeout: timeout!.inSeconds, 125 | ); 126 | if (requestResponse != null) { 127 | return requestResponse.data; 128 | } else { 129 | return null; 130 | } 131 | } 132 | } 133 | 134 | void dispose() { 135 | settingStreamSubscription?.cancel(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/services/http/server/httpOutgoingServer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class HttpOutgoingServer { 4 | HttpServer? currentServer; 5 | Map? callbacks = Map(); 6 | 7 | HttpOutgoingServer( 8 | {bool doCreateServer = true, 9 | String? ip, 10 | String? port, 11 | bool newSharedServer = true, 12 | bool initiateListeningImmediately = true}) { 13 | if (doCreateServer) { 14 | createServer(ip ?? "0.0.0.0", port ?? "8090", 15 | shared: newSharedServer, 16 | initiateImmediately: initiateListeningImmediately); 17 | } 18 | } 19 | 20 | Future createServer(String ip, String port, 21 | {bool shared = true, bool initiateImmediately = true}) async { 22 | int intPort = int.parse(port, 23 | radix: 10, onError: (err) => throw "Server port is incorrect"); 24 | try { 25 | currentServer = await HttpServer.bind(ip, intPort); 26 | if (initiateImmediately) { 27 | startListening(); 28 | } 29 | } catch (e) { 30 | print("Http Server Not initiated"); 31 | print("currentServer is : $currentServer"); 32 | print(e); 33 | // print(e.stack); 34 | } 35 | } 36 | 37 | void startListening() { 38 | if (currentServer == null) throw "No Server to listen on"; 39 | currentServer!.listen((request) { 40 | if (callbacks!.containsKey(request.uri.path)) { 41 | SimpleRequest req = callbacks![request.uri.path]!; 42 | if (req.method!.contains(request.method)) { 43 | if (req.callback != null) { 44 | req.callback!(request).then((value) { 45 | if (value != null) { 46 | try { 47 | request.response.write(value); 48 | request.response.close(); 49 | } catch (e) { 50 | print("error when writing to the response"); 51 | print(e); 52 | // print(e.stack); 53 | } 54 | } 55 | }); 56 | } 57 | } else { 58 | _sendWrongMethodFound(request.response); 59 | } 60 | } else { 61 | _sendNotFound(request.response); 62 | } 63 | }); 64 | } 65 | 66 | Future stopCurrentHTTPServer({bool forceStop = false}) { 67 | if (currentServer != null) { 68 | return currentServer!.close(force: forceStop); 69 | } 70 | return Future.value(true); 71 | } 72 | 73 | void addListenerCallback(SimpleRequest request) { 74 | callbacks![request.URL!] = request; 75 | } 76 | 77 | void removeListenerCallback(String URL) { 78 | callbacks!.remove(URL); 79 | } 80 | 81 | _sendNotFound(HttpResponse response) { 82 | response.write('Not found'); 83 | response.statusCode = HttpStatus.notFound; 84 | response.close(); 85 | } 86 | 87 | _sendWrongMethodFound(HttpResponse response) { 88 | response.write('Bad Method'); 89 | response.statusCode = HttpStatus.notFound; 90 | response.close(); 91 | } 92 | } 93 | 94 | class SimpleRequest { 95 | List? method; 96 | String? URL; 97 | Future Function(HttpRequest)? callback; 98 | SimpleRequest({this.method, this.URL, this.callback}); 99 | } 100 | -------------------------------------------------------------------------------- /lib/services/http/utilsRequests.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/services/locator.dart'; 2 | import 'package:Tunein/services/http/httpRequests.dart'; 3 | import 'package:dio/dio.dart'; 4 | 5 | final HttpRequests = locator(); 6 | 7 | class UtilsRequests { 8 | Future> getNetworkImage(String url) async { 9 | if (url != null) { 10 | Response respone = await HttpRequests.instance.get>(url, 11 | options: Options(responseType: ResponseType.bytes)); 12 | return respone.data; 13 | } else { 14 | return []; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/services/languageService.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/services/themeService.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | ///import 'package:flutter_i18n/flutter_i18n.dart'; 5 | import 'locator.dart'; 6 | 7 | final themeService = locator(); 8 | 9 | ///NOT USED YET, WILL PROBABLY BE DEPRECATED 10 | class languageService { 11 | String _flutterI18nDelegate = ""; 12 | 13 | languageService() { 14 | /* _flutterI18nDelegate = FlutterI18nDelegate( 15 | translationLoader: FileTranslationLoader( 16 | useCountryCode: false, 17 | fallbackFile: 'en', 18 | basePath: 'locale', 19 | forcedLocale: Locale('en')), 20 | );*/ 21 | } 22 | 23 | //FlutterI18nDelegate get flutterI18nDelegate => _flutterI18nDelegate; 24 | 25 | settingService() { 26 | _initStreams(); 27 | } 28 | 29 | _initStreams() {} 30 | 31 | void dispose() {} 32 | } 33 | -------------------------------------------------------------------------------- /lib/services/layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_inner_drawer/inner_drawer.dart'; 3 | //import 'package:preload_page_view/preload_page_view.dart'; 4 | import 'package:sliding_up_panel/sliding_up_panel.dart'; 5 | import 'package:Tunein/services/pageService.dart'; 6 | 7 | class LayoutService { 8 | // Main PageView 9 | late PageController _globalPageController; 10 | PageController get globalPageController => _globalPageController; 11 | 12 | // Sub PageViews 13 | late List _pageServices; 14 | List get pageServices => _pageServices; 15 | 16 | // Main Panel 17 | late PanelController _globalPanelController; 18 | PanelController get globalPanelController => _globalPanelController; 19 | late PageController _albumPlayerPageController; 20 | PageController get albumPlayerPageController => _albumPlayerPageController; 21 | late PageController _albumListPageController; 22 | PageController get albumListPageController => _albumListPageController; 23 | late VoidCallback _onPanelOpenCallback; 24 | VoidCallback get onPanelOpenCallback => _onPanelOpenCallback; 25 | 26 | set onPanelOpenCallback(VoidCallback value) { 27 | _onPanelOpenCallback = value; 28 | } // global keys 29 | 30 | final GlobalKey _scaffoldKey = GlobalKey(); 31 | 32 | GlobalKey get scaffoldKey => _scaffoldKey; 33 | 34 | final GlobalKey _sideDrawerKey = 35 | GlobalKey(); 36 | 37 | GlobalKey get sideDrawerKey => _sideDrawerKey; 38 | 39 | LayoutService() { 40 | _initGlobalPageView(); 41 | _initSubPageViews(); 42 | _initGlobalPanel(); 43 | _initPlayingPageView(); 44 | _initAlbumListPageView(); 45 | } 46 | 47 | void _initSubPageViews() { 48 | // _pageServices = List(4); 49 | _pageServices = List.filled(4, PageService(0)); 50 | for (var i = 0; i < _pageServices.length; i++) { 51 | _pageServices[i] = PageService(i, 52 | Controller: i == 0 ? PageController(keepPage: true) : null); 53 | } 54 | } 55 | 56 | void _initGlobalPanel() { 57 | _globalPanelController = PanelController(); 58 | } 59 | 60 | void _initGlobalPageView() { 61 | _globalPageController = PageController(); 62 | } 63 | 64 | void _initPlayingPageView() { 65 | _albumPlayerPageController = PageController(initialPage: 1, keepPage: true); 66 | } 67 | 68 | void _initAlbumListPageView() { 69 | _albumListPageController = PageController(); 70 | } 71 | 72 | void changeGlobalPage(int pageIndex) { 73 | Curve curve = Curves.fastOutSlowIn; 74 | _globalPageController.animateToPage( 75 | pageIndex, 76 | duration: Duration(milliseconds: 200), 77 | curve: curve, 78 | ); 79 | } 80 | 81 | //mainpanel functions 82 | 83 | onPanelOpen(dynamic data) { 84 | if (onPanelOpenCallback != null) { 85 | onPanelOpenCallback(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/services/locator.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/plugins/NotificationControlService.dart'; 2 | import 'package:Tunein/services/castService.dart'; 3 | import 'package:Tunein/services/fileService.dart'; 4 | import 'package:Tunein/services/http/httpRequests.dart'; 5 | import 'package:Tunein/services/http/requests.dart'; 6 | import 'package:Tunein/services/http/utilsRequests.dart'; 7 | import 'package:Tunein/services/languageService.dart'; 8 | import 'package:Tunein/services/layout.dart'; 9 | import 'package:Tunein/services/memoryCacheService.dart'; 10 | import 'package:Tunein/services/musicMetricsService.dart'; 11 | import 'package:Tunein/services/musicService.dart'; 12 | import 'package:Tunein/services/isolates/musicServiceIsolate.dart'; 13 | import 'package:Tunein/services/platformService.dart'; 14 | import 'package:Tunein/services/queueService.dart'; 15 | import 'package:Tunein/services/settingService.dart'; 16 | import 'package:Tunein/services/sideDrawerService.dart'; 17 | import 'package:Tunein/services/themeService.dart'; 18 | import 'package:get_it/get_it.dart'; 19 | 20 | GetIt locator = GetIt.instance; 21 | 22 | void setupLocator() { 23 | locator.registerSingleton(MemoryCacheService()); 24 | locator.registerSingleton(fileService()); 25 | locator.registerSingleton(PlatformService()); 26 | locator.registerSingleton(musicServiceIsolate()); 27 | locator.registerSingleton(notificationControlService()); 28 | locator.registerSingleton(settingService()); 29 | locator.registerSingleton(CastService()); 30 | locator.registerSingleton(MusicMetricsService()); 31 | locator.registerSingleton(ThemeService()); 32 | locator.registerSingleton(QueueService()); 33 | locator.registerSingleton(MusicService()); 34 | 35 | locator.registerSingleton(LayoutService()); 36 | locator.registerSingleton(SideDrawerService()); 37 | 38 | locator.registerSingleton(languageService()); 39 | locator.registerSingleton(httpRequests()); 40 | locator.registerSingleton(Requests()); 41 | locator.registerSingleton(UtilsRequests()); 42 | } 43 | -------------------------------------------------------------------------------- /lib/services/memoryCacheService.dart: -------------------------------------------------------------------------------- 1 | enum CachedItems { SDCARD_NAME } 2 | 3 | class MemoryCacheService { 4 | late Map primaryCache; 5 | 6 | MemoryCacheService() { 7 | init(); 8 | } 9 | 10 | dynamic setCacheItem(dynamic id, dynamic value) { 11 | if (id is CachedItems) { 12 | primaryCache[id.toString()] = value; 13 | return; 14 | } 15 | primaryCache[id] = value; 16 | return; 17 | } 18 | 19 | dynamic getCacheItem(dynamic id) { 20 | if (id is CachedItems) { 21 | return primaryCache[id.toString()]; 22 | } 23 | return primaryCache[id]; 24 | } 25 | 26 | bool isItemCached(String id) { 27 | if (id is CachedItems) { 28 | return primaryCache.containsKey(id.toString()); 29 | } 30 | return primaryCache.containsKey(id); 31 | } 32 | 33 | init() { 34 | primaryCache = new Map(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/services/pageService.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/values/lists.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:rxdart/rxdart.dart'; 4 | 5 | class PageService { 6 | // Sub PAGEVIEW 7 | late BehaviorSubject _pageIndex$; 8 | BehaviorSubject get pageIndex$ => _pageIndex$; 9 | late var _pageController; 10 | get pageViewController => _pageController; 11 | 12 | // HEADER NAVIGATION BAR 13 | late ScrollController _headerController; 14 | ScrollController get headerController => _headerController; 15 | late double _offset; 16 | late double _width; 17 | late List _navSizes; 18 | late List _cumulativeNavSizes; 19 | late bool _isSet; 20 | late int _setCount; 21 | List get navSizes => _navSizes; 22 | List get cumulativeNavSizes => _cumulativeNavSizes; 23 | 24 | final int id; 25 | final double viewPort; 26 | final Controller; 27 | PageService(this.id, {double this.viewPort = 1, this.Controller}) { 28 | _initPageView(); 29 | _initHeaderNavBar(); 30 | _registerListeners(); 31 | } 32 | 33 | void updatePageIndex(double value) { 34 | _pageIndex$.add(value); 35 | } 36 | 37 | void setSize(int index) { 38 | if (_isSet) { 39 | return; 40 | } 41 | GlobalKey key = headerItems[id]![index].value; 42 | 43 | RenderBox renderBoxRed = 44 | key.currentContext!.findRenderObject() as RenderBox; 45 | double width = renderBoxRed.size.width; 46 | _navSizes[index] = width; 47 | _setCount = _setCount + 1; 48 | _checkSet(); 49 | } 50 | 51 | void _checkSet() { 52 | if (_setCount == headerItems[id]!.length) { 53 | _isSet = true; 54 | _constructCumulative(); 55 | } 56 | } 57 | 58 | _constructCumulative() { 59 | _cumulativeNavSizes.add(0); 60 | _cumulativeNavSizes.add(_navSizes[0]); 61 | _navSizes.reduce((a, b) { 62 | _cumulativeNavSizes.add(a + b); 63 | return a + b; 64 | }); 65 | } 66 | 67 | _initPageView() { 68 | _pageIndex$ = BehaviorSubject.seeded(0); 69 | _pageController = 70 | this.Controller ?? PageController(viewportFraction: viewPort); 71 | } 72 | 73 | _initHeaderNavBar() { 74 | _headerController = ScrollController(); 75 | _width = _offset = 0; 76 | _setCount = 0; 77 | _isSet = false; 78 | // _navSizes = List(headerItems[id]!.length); 79 | _navSizes = List.filled(headerItems[id]!.length, 0); 80 | 81 | _cumulativeNavSizes = []; 82 | } 83 | 84 | void _registerListeners() { 85 | _pageController.addListener(() { 86 | int floor = _pageController.page.floor(); 87 | _pageIndex$.add(_pageController.page); 88 | _offset = _cumulativeNavSizes[floor]; 89 | _width = _navSizes[floor]; 90 | _headerController 91 | .jumpTo((_pageController.page - floor).abs() * _width + _offset); 92 | }); 93 | } 94 | 95 | void dispose() { 96 | _pageController.dispose(); 97 | _headerController.dispose(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/services/platformService.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | import 'package:dart_ping/dart_ping.dart'; 5 | import 'package:network_info_plus/network_info_plus.dart'; 6 | 7 | class PlatformService { 8 | Future isOnWifi() async { 9 | ConnectivityResult connectivityResult = 10 | await (Connectivity().checkConnectivity()); 11 | if (connectivityResult == ConnectivityResult.mobile) { 12 | return false; 13 | } else if (connectivityResult == ConnectivityResult.wifi) { 14 | return true; 15 | } 16 | return true; 17 | } 18 | 19 | Future getCurrentIP() async { 20 | final info = NetworkInfo(); 21 | String? ipAddress = await info.getWifiIP(); 22 | return ipAddress; 23 | } 24 | 25 | Future pingIp(String ip, 26 | {Duration interval = const Duration(seconds: 1), 27 | int pingNumber = 2}) async { 28 | final ping = Ping(ip, count: pingNumber, interval: interval.inSeconds); 29 | StreamSubscription subscription = ping.stream.listen((event) { 30 | print(event); 31 | }); 32 | await subscription.asFuture(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/services/routes/pageRoutes.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/Tune/songTags.dart'; 2 | import 'package:Tunein/pages/single/singleAlbum.page.dart'; 3 | import 'package:Tunein/pages/single/singleArtistPage.dart'; 4 | import 'package:Tunein/plugins/nano.dart'; 5 | import 'package:Tunein/services/locator.dart'; 6 | import 'package:Tunein/services/musicService.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | final musicService = locator(); 11 | 12 | class PageRoutes { 13 | static void goToAlbumSongsList(Tune? song, context, 14 | {Album? album, 15 | bool subtract60ForBottomBar = false, 16 | bool rootRouter = false}) { 17 | Album targetAlbum = album ?? musicService.getAlbumFromSong(song!); 18 | if (targetAlbum != null) { 19 | Navigator.of(context, rootNavigator: rootRouter).push( 20 | MaterialPageRoute( 21 | builder: (context) => SingleAlbumPage( 22 | null, 23 | album: targetAlbum, 24 | heightToSubstract: subtract60ForBottomBar ? 60 : 0, 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | 31 | static void goToSingleArtistPage(Tune song, context, 32 | {Artist? artist, 33 | bool subtract60ForBottomBar = false, 34 | bool rootRouter = false}) { 35 | Artist targetArtist = artist ?? musicService.getArtistTitle(song.artist!); 36 | if (targetArtist != null) { 37 | Navigator.of(context, rootNavigator: rootRouter).push( 38 | MaterialPageRoute( 39 | builder: (context) => SingleArtistPage(targetArtist, 40 | heightToSubstract: subtract60ForBottomBar ? 60 : 0), 41 | ), 42 | ); 43 | } 44 | } 45 | 46 | static void goToEditTagsPage(Tune song, context, 47 | {bool subtract60ForBottomBar = false, bool rootRouter = false}) { 48 | if (song != null) { 49 | Navigator.of(context, rootNavigator: rootRouter).push( 50 | MaterialPageRoute( 51 | builder: (context) => 52 | SongTags(song, heightToSubtract: subtract60ForBottomBar ? 60 : 0), 53 | ), 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/services/sideDrawerService.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/components/drawer/sideDrawer.dart'; 2 | import 'package:Tunein/services/layout.dart'; 3 | import 'package:Tunein/services/locator.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_inner_drawer/inner_drawer.dart'; 6 | //import 'package:preload_page_view/preload_page_view.dart'; 7 | 8 | class SideDrawerService { 9 | final layoutService = locator(); 10 | 11 | // Current State of InnerDrawerState 12 | late GlobalKey _innerDrawerKey; 13 | 14 | SideDrawerService() { 15 | _innerDrawerKey = layoutService.sideDrawerKey; 16 | } 17 | 18 | //making the over scroll from the library pages : scrolling pst the first page , open the drawer 19 | 20 | void toggle() { 21 | _innerDrawerKey.currentState!.toggle( 22 | // direction is optional 23 | // if not set, the last direction will be used 24 | //InnerDrawerDirection.start OR InnerDrawerDirection.end 25 | direction: InnerDrawerDirection.start); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/services/themeService.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/plugins/nano.dart'; 2 | import 'package:Tunein/utils/messaginUtils.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:rxdart/rxdart.dart'; 5 | 6 | final _androidAppRetain = MethodChannel("android_app_retain"); 7 | 8 | class ThemeService { 9 | late BehaviorSubject> _color$; 10 | BehaviorSubject> get color$ => _color$; 11 | 12 | late Map> _savedColors; 13 | late Map> _artistSavedColors; 14 | List defaultColors = [0xff111111, 0xffffffff, 0xffffffff]; 15 | ThemeService() { 16 | _initStreams(); 17 | _savedColors = Map>(); 18 | _artistSavedColors = Map>(); 19 | } 20 | 21 | void updateTheme(Tune song) async { 22 | final color = await MessagingUtils.sendNewIsolateCommand( 23 | command: "updateTheme", 24 | message: {"songId": song.id, "coverArt": song.albumArt}); 25 | 26 | return; 27 | } 28 | 29 | Future> getThemeColors(Tune song) async { 30 | final color = await MessagingUtils.sendNewIsolateCommand( 31 | command: "getThemeColors", 32 | message: {"songId": song.id, "coverArt": song.albumArt}); 33 | 34 | return color; 35 | } 36 | 37 | Future> getArtistColors(Artist artist) async { 38 | final color = await MessagingUtils.sendNewIsolateCommand( 39 | command: "getArtistColors", 40 | message: {"artistId": artist.id, "coverArt": artist.coverArt}); 41 | 42 | return color; 43 | } 44 | 45 | void _initStreams() { 46 | _color$ = BehaviorSubject>.seeded([0xff111111, 0xffffffff]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/services/uiScaleService.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | import 'dart:math'; 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | class uiScaleService{ 10 | static ArtistGridCellHeight(Size screenSize){ 11 | return min(screenSize.height/4,155); 12 | } 13 | 14 | static AlbumsGridCellHeight(Size screenSize){ 15 | return min(screenSize.height/4,155); 16 | } 17 | 18 | static AlbumArtistInfoPage(Size screenSize){ 19 | return min(screenSize.height/3.5,200); 20 | } 21 | } -------------------------------------------------------------------------------- /lib/utils/ConversionUtils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | import 'dart:ui' as ui; 4 | 5 | import 'package:Tunein/plugins/nano.dart'; 6 | import 'package:Tunein/services/fileService.dart'; 7 | import 'package:Tunein/services/locator.dart'; 8 | import 'package:flutter/cupertino.dart'; 9 | import 'package:flutter/rendering.dart'; 10 | 11 | class ConversionUtils { 12 | static final FileService = locator(); 13 | 14 | static String DurationToFancyText(Duration duration, 15 | {showHours = true, showMinutes = true, showSeconds = true, String? orElse}) { 16 | assert(duration != null, "duration argument can't be null"); 17 | String finalText = ""; 18 | if (showHours && duration.inHours != 0) { 19 | finalText += "${duration.inHours} hours "; 20 | } 21 | if (showMinutes && duration.inMinutes.remainder(60) != 0) { 22 | finalText += "${duration.inMinutes.remainder(60)} min "; 23 | } 24 | if (showSeconds && duration.inSeconds.remainder(60) != 0) { 25 | finalText += "${duration.inSeconds.remainder(60)} sec"; 26 | } 27 | 28 | return orElse != null ? finalText == "" ? orElse : finalText : finalText; 29 | } 30 | 31 | static String DurationToStandardTimeDisplay( 32 | {required Duration inputDuration, 33 | showHours = false, 34 | showMinutes = true, 35 | showSeconds = true}) { 36 | String finalDuration = ""; 37 | 38 | if (showHours) { 39 | int hours = inputDuration.inHours; 40 | finalDuration += "${hours < 10 ? "0" : ""}${inputDuration.inHours}:"; 41 | } 42 | if (showMinutes) { 43 | if (showHours) { 44 | int minutes = inputDuration.inMinutes.remainder(60); 45 | finalDuration += "${minutes < 10 ? "0" : ""}${minutes}:"; 46 | } else { 47 | int minutes = inputDuration.inMinutes; 48 | finalDuration += "${minutes < 10 ? "0" : ""}${minutes}:"; 49 | } 50 | } 51 | if (showSeconds) { 52 | int seconds = inputDuration.inSeconds.remainder(60); 53 | finalDuration += "${seconds < 10 ? "0" : ""}${seconds}"; 54 | } 55 | 56 | return finalDuration; 57 | } 58 | 59 | static Future> FileUriTo8Bit(String uri, 60 | {File? fileInstead}) async { 61 | assert(uri != null || fileInstead != null, 62 | "one of uri and file needs to be supplied"); 63 | if (fileInstead != null) { 64 | assert(fileInstead.existsSync(), "File Not Found"); 65 | return fileInstead.readAsBytesSync(); 66 | } else { 67 | return await FileService.readFile(uri, readAsBytes: true); 68 | } 69 | } 70 | 71 | static Future fromWidgetGlobalKeyToImageByteList( 72 | GlobalKey widgetlobalKey) async { 73 | assert(widgetlobalKey != null, "You can't pass a null global key"); 74 | RenderRepaintBoundary? boundary = widgetlobalKey.currentContext! 75 | .findRenderObject() as RenderRepaintBoundary?; 76 | ui.Image image = await boundary!.toImage(pixelRatio: 3.0); 77 | ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); 78 | Uint8List pngBytes = byteData!.buffer.asUint8List(); 79 | return pngBytes; 80 | } 81 | 82 | static double songListToDuration(List songs) { 83 | int finalDuration = 0; 84 | 85 | songs.forEach((elem) { 86 | finalDuration += elem.duration ?? 0; 87 | }); 88 | 89 | return finalDuration.toDouble(); 90 | } 91 | 92 | /// Creates an image from the given widget by first spinning up a element and render tree, 93 | /// then waiting for the given [wait] amount of time and then creating an image via a [RepaintBoundary]. 94 | /// 95 | /// The final image will be of size [imageSize] and the the widget will be layout, ... with the given [logicalSize]. 96 | static Future createImageFromWidget(Widget widget, 97 | {required Duration wait, 98 | required Size logicalSize, 99 | required Size imageSize}) async { 100 | final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary(); 101 | 102 | logicalSize ??= ui.window.physicalSize / ui.window.devicePixelRatio; 103 | imageSize ??= ui.window.physicalSize; 104 | 105 | assert(logicalSize.aspectRatio == imageSize.aspectRatio); 106 | 107 | final RenderView renderView = RenderView( 108 | window: WidgetsBinding.instance.window, 109 | child: RenderPositionedBox( 110 | alignment: Alignment.center, child: repaintBoundary), 111 | configuration: ViewConfiguration( 112 | size: logicalSize, 113 | devicePixelRatio: 1.0, 114 | ), 115 | ); 116 | 117 | final PipelineOwner pipelineOwner = PipelineOwner(); 118 | final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager()); 119 | 120 | pipelineOwner.rootNode = renderView; 121 | renderView.prepareInitialFrame(); 122 | 123 | final RenderObjectToWidgetElement rootElement = 124 | RenderObjectToWidgetAdapter( 125 | container: repaintBoundary, 126 | child: widget, 127 | ).attachToRenderTree(buildOwner); 128 | 129 | buildOwner.buildScope(rootElement); 130 | 131 | if (wait != null) { 132 | await Future.delayed(wait); 133 | } 134 | 135 | buildOwner.buildScope(rootElement); 136 | buildOwner.finalizeTree(); 137 | 138 | pipelineOwner.flushLayout(); 139 | pipelineOwner.flushCompositingBits(); 140 | pipelineOwner.flushPaint(); 141 | 142 | final ui.Image image = await repaintBoundary.toImage( 143 | pixelRatio: imageSize.width / logicalSize.width); 144 | final ByteData? byteData = 145 | await image.toByteData(format: ui.ImageByteFormat.png); 146 | 147 | return byteData!.buffer.asUint8List(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/utils/MathUtils.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | import 'dart:math'; 6 | import 'package:uuid/uuid.dart' as uuid; 7 | class MathUtils{ 8 | 9 | static int getRandomFromRange(int min, int max){ 10 | Random rnd; 11 | rnd = new Random(); 12 | return min + rnd.nextInt(max - min); 13 | } 14 | 15 | static String getUniqueId(){ 16 | return uuid.Uuid().v4(); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /lib/utils/messaginUtils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:Tunein/services/isolates/musicServiceIsolate.dart'; 4 | import 'package:Tunein/services/locator.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | class MessagingUtils { 8 | static Future sendNewIsolateCommand({required String command, message = ""}) { 9 | musicServiceIsolate MusicServiceIsolate = locator(); 10 | ReceivePort tempPort = ReceivePort(); 11 | MusicServiceIsolate.sendCrossPluginIsolatesMessage( 12 | CrossIsolatesMessage( 13 | sender: tempPort.sendPort, command: command, message: message)); 14 | return tempPort.singleWhere((data) { 15 | if (data != "OK") { 16 | tempPort.close(); 17 | return true; 18 | } else { 19 | return false; 20 | } 21 | }); 22 | } 23 | 24 | static Future sendNewStandardIsolateCommand( 25 | {required String command, message}) { 26 | musicServiceIsolate MusicServiceIsolate = locator(); 27 | ReceivePort tempPort = ReceivePort(); 28 | MusicServiceIsolate.sendCrossIsolateMessage(CrossIsolatesMessage( 29 | sender: tempPort.sendPort, command: command, message: message ?? "")); 30 | return tempPort.singleWhere((data) { 31 | if (data != "OK") { 32 | tempPort.close(); 33 | return true; 34 | } else { 35 | return false; 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/values/contextMenus.dart: -------------------------------------------------------------------------------- 1 | import 'package:Tunein/models/ContextMenuOption.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | ///The context menu for the regular song cards found on tracks page, album page and queue 5 | final songCardContextMenulist = [ 6 | ContextMenuOptions( 7 | id: 8, 8 | title: "Album", 9 | icon: Icons.play_circle_outline, 10 | function: () {}, 11 | ), 12 | ContextMenuOptions( 13 | id: 9, title: "Artist", icon: Icons.play_circle_outline, function: () {}), 14 | ContextMenuOptions( 15 | id: 1, 16 | title: "Play one", 17 | icon: Icons.play_circle_outline, 18 | function: () {}), 19 | ContextMenuOptions( 20 | id: 4, title: "Play Album", icon: Icons.album, function: () {}), 21 | ContextMenuOptions( 22 | id: 2, title: "Shuffle next queue", icon: Icons.shuffle, function: () {}), 23 | ContextMenuOptions( 24 | id: 3, title: "Shuffle next album", icon: Icons.shuffle, function: () {}), 25 | ContextMenuOptions( 26 | id: 5, title: "Cast", icon: Icons.cast_connected, function: () {}), 27 | ContextMenuOptions( 28 | id: 6, title: "Cast to new Device", icon: Icons.cast, function: () {}), 29 | ContextMenuOptions( 30 | id: 7, 31 | title: "Song information", 32 | icon: Icons.info_outline, 33 | function: () {}), 34 | ContextMenuOptions( 35 | id: 10, title: "Edit Tags", icon: Icons.edit, function: () {}), 36 | ]; 37 | 38 | ///The context menu for the regular artist card found in the artists page 39 | final artistCardContextMenulist = [ 40 | ContextMenuOptions( 41 | id: 1, 42 | title: "Play All", 43 | icon: Icons.play_circle_outline, 44 | function: () {}), 45 | ContextMenuOptions( 46 | id: 2, title: "Shuffle", icon: Icons.shuffle, function: () {}), 47 | ]; 48 | 49 | ///The context menu for the regular Album card found in the albums page 50 | final albumCardContextMenulist = [ 51 | ContextMenuOptions( 52 | id: 1, 53 | title: "Play Album", 54 | icon: Icons.play_circle_outline, 55 | function: () {}), 56 | ContextMenuOptions( 57 | id: 2, title: "Shuffle", icon: Icons.shuffle, function: () {}), 58 | ]; 59 | 60 | ///The context menu for the regular playlist card foudn in the playlists page 61 | final playlistCardContextMenulist = [ 62 | ContextMenuOptions( 63 | id: 1, title: "Add new Songs", icon: Icons.add, function: () {}), 64 | ContextMenuOptions( 65 | id: 2, 66 | title: "Play All", 67 | icon: Icons.play_circle_outline, 68 | function: () {}), 69 | ContextMenuOptions( 70 | id: 3, title: "Shuffle", icon: Icons.shuffle, function: () {}), 71 | ContextMenuOptions( 72 | id: 4, 73 | title: "Edit playlist", 74 | icon: Icons.edit_attributes, 75 | function: () {}), 76 | ContextMenuOptions( 77 | id: 5, title: "Delete playlist", icon: Icons.delete, function: () {}), 78 | ]; 79 | 80 | ///The context menu for the song cards found on a single playlist page 81 | final playlistSongCardContextMenulist = [ 82 | ContextMenuOptions( 83 | id: 1, 84 | title: "Play one", 85 | icon: Icons.play_circle_outline, 86 | function: () {}), 87 | ContextMenuOptions( 88 | id: 2, 89 | title: "Shuffle next playlist", 90 | icon: Icons.shuffle, 91 | function: () {}), 92 | ContextMenuOptions( 93 | id: 3, title: "Shuffle next album", icon: Icons.shuffle, function: () {}), 94 | ]; 95 | 96 | ///The context menu for the song cards found on the search page for playlists 97 | final playlistSearchSongCardContextMenulist = [ 98 | ContextMenuOptions( 99 | id: 1, 100 | title: "Add one", 101 | icon: Icons.play_circle_outline, 102 | function: () {}), 103 | ContextMenuOptions( 104 | id: 2, title: "Add entire album", icon: Icons.shuffle, function: () {}), 105 | ]; 106 | 107 | ///The context menu for the song cards found on the Edit Playlist page 108 | final editPlaylistSongCardContextMenulist = [ 109 | ContextMenuOptions( 110 | id: 1, title: "delete song", icon: Icons.delete, function: () {}), 111 | ]; 112 | -------------------------------------------------------------------------------- /lib/values/lists.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final Map>> headerItems = { 4 | 0: [ 5 | MapEntry( 6 | "Home", 7 | GlobalKey(), 8 | ), 9 | MapEntry( 10 | "Tracks", 11 | GlobalKey(), 12 | ), 13 | MapEntry( 14 | "Artists", 15 | GlobalKey(), 16 | ), 17 | MapEntry( 18 | "Albums", 19 | GlobalKey(), 20 | ) 21 | ], 22 | 1: [ 23 | MapEntry("Playlists", GlobalKey()), 24 | MapEntry("Favorites", GlobalKey()), 25 | ], 26 | 2:[ 27 | MapEntry("General", GlobalKey()), 28 | MapEntry("Interface", GlobalKey()), 29 | MapEntry("Metrics", GlobalKey()), 30 | MapEntry("Servers", GlobalKey()) 31 | ], 32 | 3:[ 33 | MapEntry("About", GlobalKey()), 34 | ] 35 | }; 36 | 37 | final List> bottomNavBarItems = [ 38 | MapEntry("Library", Icon(IconData(0xec2f, fontFamily: 'boxicons'))), 39 | MapEntry("Playlists", Icon(IconData(0xeccd, fontFamily: 'boxicons'))), 40 | MapEntry("Search", Icon(IconData(0xeb2e, fontFamily: 'boxicons'))), 41 | MapEntry("Equalizer", Icon(IconData(0xea86, fontFamily: 'boxicons'))), 42 | MapEntry("Settings", Icon(IconData(0xec2e, fontFamily: 'boxicons'))), 43 | ]; 44 | -------------------------------------------------------------------------------- /locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /locale/es.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: Tunein 2 | description: A dynamically themed music player with built in casting support made with Flutter 3 | version: 0.1.2 4 | 5 | 6 | publish_to: "none" 7 | 8 | 9 | environment: 10 | sdk: ">=2.18.4 <3.0.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | # The following adds the Cupertino Icons font to your application. 17 | # Use with the CupertinoIcons class for iOS style icons. 18 | cupertino_icons: ^1.0.2 19 | audioplayer: 20 | git: 21 | ref: master 22 | url: https://github.com/moda20/audioplayer 23 | path_provider: ^2.0.11 24 | flutter_reorderable_list: ^1.2.0 25 | rxdart: ^0.27.7 26 | sliding_up_panel: ^2.0.0+1 27 | shared_preferences: ^2.0.15 28 | media_notification: 29 | git: 30 | url: https://github.com/moda20/flutter_media_notification 31 | ref: master 32 | permission_handler: ^10.2.0 33 | get_it: ^7.2.0 34 | crypto: ^3.0.2 35 | uuid: ^3.0.7 36 | page_transition: "^1.1.5" 37 | flutter_custom_dialog: ^1.2.0 38 | fading_edge_scrollview: ^3.0.0 39 | string_similarity: ^2.0.0 40 | another_flushbar: ^1.12.29 41 | settings_ui: ^2.0.2 42 | #flutter_i18n: ^0.20.1 43 | marquee: ^2.2.3 44 | flutter_isolate: ^2.0.4 45 | dio: ^4.0.6 46 | dio_flutter_transformer2: ^4.0.1 47 | morpheus: ^1.2.2+1 48 | animations: ^2.0.7 49 | badges: ^2.0.3 50 | popup_menu: # this package is only updated on github but the update is not pushed to pub.dev 51 | git: 52 | url: https://github.com/chinabrant/popup_menu 53 | ref: master 54 | upnp: 55 | git: 56 | url: https://github.com/moda20/upnp.dart 57 | ref: master 58 | connectivity_plus: ^3.0.2 59 | flutter_file_meta_data: 60 | git: 61 | url: https://github.com/moda20/flutter_file_meta_data 62 | ref: master 63 | dart_ping: ^7.0.2 64 | preload_page_view: 65 | git: 66 | url: https://github.com/moda20/preload_page_view 67 | ref: 90bf545d49c79cb4980db3cf396e87a7e4b26f58 68 | publish_to: none 69 | toast: ^0.3.0 70 | expandable: ^5.0.1 71 | flutter_inner_drawer: 72 | git: 73 | url: https://github.com/bikcrum/flutter_inner_drawer 74 | ref: bugfix/type-casting 75 | url_launcher: ^6.1.7 76 | dart_tags: ^0.4.0 77 | autocomplete_textfield: ^1.7.3 78 | tunein_image_utils_plugin: 79 | git: 80 | url: https://github.com/moda20/tunein_image_utils_plugin 81 | media_gallery: ^0.1.5 82 | image_list: ^3.0.0 83 | extended_tabs: ^4.0.2 84 | external_path: ^1.0.3 85 | photo_gallery: ^1.1.1 86 | network_info_plus: ^3.0.1 87 | collection: ^1.16.0 88 | # ext_storage: ^1.0.3 89 | 90 | dev_dependencies: 91 | flutter_test: 92 | sdk: flutter 93 | 94 | 95 | flutter: 96 | assets: 97 | - images/ 98 | - locale/ 99 | fonts: 100 | - family: Boxicons 101 | fonts: 102 | - asset: fonts/boxicons.ttf 103 | weight: 400 104 | - family: Fontawesome 105 | fonts: 106 | - asset: fonts/fa.ttf 107 | weight: 400 108 | - family: LigatureSymbols 109 | fonts: 110 | - asset: fonts/LigatureSymbols-2.11.ttf 111 | weight: 400 112 | 113 | 114 | 115 | 116 | uses-material-design: true 117 | -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/10.png -------------------------------------------------------------------------------- /screenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/11.png -------------------------------------------------------------------------------- /screenshots/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/12.png -------------------------------------------------------------------------------- /screenshots/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/13.png -------------------------------------------------------------------------------- /screenshots/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/14.png -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/8.png -------------------------------------------------------------------------------- /screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moda20/flutter-tunein/a6f6f608a6693083f2b26d69965a116ba9528965/screenshots/9.png --------------------------------------------------------------------------------