├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj └── .gitignore ├── fonts ├── Jura-Bold.ttf ├── Jura-Light.ttf ├── Jura-Medium.ttf ├── Jura-Regular.ttf └── Jura-SemiBold.ttf ├── graphics ├── default.png ├── icon │ ├── icon.png │ └── mstream.png └── mstream-logo.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── mstream_music │ │ │ │ │ └── MainActivity.kt │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── addfluttertoandroid │ │ │ │ │ └── MyApplication.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── .metadata ├── .vscode └── launch.json ├── lib ├── objects │ ├── download_tracker.dart │ ├── metadata.dart │ ├── server.dart │ └── display_item.dart ├── singletons │ ├── media.dart │ ├── transcode.dart │ ├── file_explorer.dart │ ├── downloads.dart │ ├── browser_list.dart │ ├── server_list.dart │ └── api.dart ├── screens │ ├── about_screen.dart │ ├── metadata_screen.dart │ ├── downloads.dart │ ├── manage_server.dart │ ├── auto_dj.dart │ ├── add_server.dart │ └── browser.dart └── media │ ├── common.dart │ └── audio_stuff.dart ├── README.md ├── test └── widget_test.dart ├── pubspec.yaml ├── .gitignore └── pubspec.lock /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /fonts/Jura-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/fonts/Jura-Bold.ttf -------------------------------------------------------------------------------- /fonts/Jura-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/fonts/Jura-Light.ttf -------------------------------------------------------------------------------- /graphics/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/graphics/default.png -------------------------------------------------------------------------------- /fonts/Jura-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/fonts/Jura-Medium.ttf -------------------------------------------------------------------------------- /fonts/Jura-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/fonts/Jura-Regular.ttf -------------------------------------------------------------------------------- /graphics/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/graphics/icon/icon.png -------------------------------------------------------------------------------- /fonts/Jura-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/fonts/Jura-SemiBold.ttf -------------------------------------------------------------------------------- /graphics/icon/mstream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/graphics/icon/mstream.png -------------------------------------------------------------------------------- /graphics/mstream-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/graphics/mstream-logo.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/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/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IrosTheBeggar/mstream_music/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/mstream_music/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.mstream_music 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "mstream_music", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /lib/objects/download_tracker.dart: -------------------------------------------------------------------------------- 1 | import 'display_item.dart'; 2 | 3 | class DownloadTracker { 4 | String serverUrl; 5 | String filePath; 6 | 7 | int progress = 0; 8 | 9 | // These can be set to update downlaod progress for a particular item 10 | // you should always check if these exist before using them 11 | late DisplayItem? referenceDisplayItem; 12 | 13 | DownloadTracker(this.serverUrl, this.filePath); 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mstream_music 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /lib/singletons/media.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | import '../media/audio_stuff.dart'; 3 | 4 | class MediaManager { 5 | MediaManager._privateConstructor(); 6 | static final MediaManager _instance = MediaManager._privateConstructor(); 7 | factory MediaManager() { 8 | return _instance; 9 | } 10 | 11 | late AudioHandler audioHandler; 12 | 13 | start() async { 14 | audioHandler = await AudioService.init( 15 | builder: () => AudioPlayerHandler(), 16 | config: AudioServiceConfig( 17 | androidNotificationChannelName: 'mStream Music', 18 | androidNotificationOngoing: true, 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/singletons/transcode.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:rxdart/rxdart.dart'; 3 | 4 | class TranscodeManager { 5 | bool transcodeOn = false; 6 | 7 | late final BehaviorSubject _transcodeOnStream = 8 | BehaviorSubject.seeded(transcodeOn); 9 | 10 | TranscodeManager._privateConstructor(); 11 | static final TranscodeManager _instance = 12 | TranscodeManager._privateConstructor(); 13 | 14 | factory TranscodeManager() { 15 | return _instance; 16 | } 17 | 18 | void dispose() { 19 | _transcodeOnStream.close(); 20 | } //initializes the subject with element already; 21 | 22 | Stream get curentServerStream => _transcodeOnStream.stream; 23 | } 24 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.2.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.20" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/objects/metadata.dart: -------------------------------------------------------------------------------- 1 | class MusicMetadata { 2 | String hash; 3 | 4 | String? artist; 5 | String? album; 6 | String? title; 7 | int? track; 8 | int? disc; 9 | int? year; 10 | int? rating; 11 | String? albumArt; 12 | 13 | MusicMetadata(this.artist, this.album, this.title, this.track, this.disc, 14 | this.year, this.hash, this.rating, this.albumArt); 15 | 16 | MusicMetadata.fromJson(Map json) 17 | : artist = json['artist'], 18 | album = json['album'], 19 | title = json['title'], 20 | track = json['track'], 21 | disc = json['disc'], 22 | year = json['year'], 23 | hash = json['hash'], 24 | rating = json['rating'], 25 | albumArt = json['albumArt']; 26 | 27 | Map toJson() => { 28 | 'artist': artist, 29 | 'album': album, 30 | 'title': title, 31 | 'track': track, 32 | 'disc': disc, 33 | 'year': year, 34 | 'hash': hash, 35 | 'rating': rating, 36 | 'albumArt': albumArt, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:mstream_music/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MStreamApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/objects/server.dart: -------------------------------------------------------------------------------- 1 | class Server { 2 | String url; 3 | String localname; // name we use when mappings files to the fs 4 | bool saveToSdCard = false; 5 | 6 | // authentication is optional (mstream servers can be public OR private) 7 | String? username; 8 | String? password; 9 | String? jwt; 10 | 11 | // Auto DJ 12 | int? autoDJminRating; 13 | Map autoDJPaths = {}; 14 | List playlists = []; 15 | 16 | Server(this.url, this.username, this.password, this.jwt, this.localname); 17 | 18 | Server.fromJson(Map json) 19 | : url = json['url'], 20 | jwt = json['jwt'], 21 | username = json['username'], 22 | password = json['password'], 23 | localname = json['localname'], 24 | autoDJPaths = json['autoDJPaths']?.cast() ?? {}, 25 | autoDJminRating = json['autoDJminRating'], 26 | playlists = List.from(json['playlists']) ?? [], 27 | saveToSdCard = json['saveToSdCard'] ?? false; 28 | 29 | Map toJson() => { 30 | 'url': url, 31 | 'jwt': jwt, 32 | 'username': username, 33 | 'password': password, 34 | 'localname': localname, 35 | 'autoDJPaths': autoDJPaths, 36 | 'autoDJminRating': autoDJminRating, 37 | 'playlists': playlists, 38 | 'saveToSdCard': saveToSdCard 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/screens/about_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AboutScreen extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text("About"), 9 | ), 10 | body: Container( 11 | padding: EdgeInsets.all(40.0), 12 | child: ListView(children: [ 13 | Image(image: AssetImage('graphics/mstream-logo.png')), 14 | Container( 15 | height: 15, 16 | ), 17 | Text('mStream Mobile v0.12', 18 | style: TextStyle( 19 | fontFamily: 'Jura', 20 | color: Color(0xFF000000), 21 | fontWeight: FontWeight.bold, 22 | fontSize: 20)), 23 | Container( 24 | height: 45, 25 | ), 26 | Text('Developed By:', 27 | style: TextStyle( 28 | fontFamily: 'Jura', 29 | color: Color(0xFF000000), 30 | fontWeight: FontWeight.bold, 31 | fontSize: 20)), 32 | Text('Paul Sori', 33 | style: TextStyle( 34 | fontFamily: 'Jura', 35 | color: Color(0xFF000000), 36 | fontWeight: FontWeight.bold, 37 | fontSize: 20)), 38 | Text('paul@mstream.io', 39 | style: TextStyle( 40 | fontFamily: 'Jura', 41 | color: Color(0xFF000000), 42 | fontWeight: FontWeight.bold, 43 | fontSize: 20)), 44 | ]))); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | mStream does not use the microphone 7 | UIBackgroundModes 8 | 9 | audio 10 | 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | mstream_music 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | $(FLUTTER_BUILD_NUMBER) 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/screens/metadata_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../objects/metadata.dart'; 3 | 4 | class MeteDataScreen extends StatelessWidget { 5 | // In the constructor, require a Todo. 6 | const MeteDataScreen({Key? key, required this.meta, required this.path}) 7 | : super(key: key); 8 | 9 | // Declare a field that holds the Todo. 10 | final MusicMetadata meta; 11 | 12 | final String? path; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | backgroundColor: Color(0xFF3f3f3f), 18 | appBar: AppBar( 19 | title: Text("Song Info"), 20 | ), 21 | body: Container( 22 | // padding: EdgeInsets.all(20.0), 23 | child: ListView(children: [ 24 | if (meta.albumArt != null) ...[Image.network(meta.albumArt!)], 25 | Container(height: 20), 26 | if (meta.title != null) ...[ 27 | Container( 28 | padding: EdgeInsets.symmetric(horizontal: 20.0), 29 | child: Text(meta.title!)), 30 | ], 31 | if (meta.artist != null) ...[ 32 | Container( 33 | padding: EdgeInsets.symmetric(horizontal: 20.0), 34 | child: Text(meta.artist!)) 35 | ], 36 | if (meta.album != null) ...[ 37 | Container( 38 | padding: EdgeInsets.symmetric(horizontal: 20.0), 39 | child: Text(meta.album!)), 40 | ], 41 | if (meta.year != null) ...[ 42 | Container( 43 | padding: EdgeInsets.symmetric(horizontal: 20.0), 44 | child: Text(meta.year!.toString())) 45 | ], 46 | Container(height: 20), 47 | if (path != null) ...[ 48 | Container( 49 | padding: EdgeInsets.symmetric(horizontal: 20.0), 50 | child: Text(path!)) 51 | ] 52 | ]))); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/addfluttertoandroid/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.mstream_music; 2 | 3 | import android.app.Application; 4 | import java.security.KeyStore; 5 | import javax.net.ssl.*; 6 | import java.security.cert.*; 7 | import java.io.IOException; 8 | import java.security.KeyManagementException; 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | public class MyApplication extends Application { 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | disableSSLCertificateChecking(); 16 | } 17 | 18 | private static void disableSSLCertificateChecking() { 19 | TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { 20 | public X509Certificate[] getAcceptedIssuers() { 21 | return null; 22 | } 23 | 24 | @Override 25 | public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 26 | // Not implemented 27 | } 28 | 29 | @Override 30 | public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 31 | // Not implemented 32 | } 33 | }}; 34 | 35 | try { 36 | SSLContext sc = SSLContext.getInstance("TLS"); 37 | 38 | sc.init(null, trustAllCerts, new java.security.SecureRandom()); 39 | 40 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 41 | 42 | HttpsURLConnection.setDefaultHostnameVerifier( 43 | new HostnameVerifier() { 44 | @Override 45 | public boolean verify(String s, SSLSession sslSession) { 46 | return true; 47 | } 48 | } 49 | ); 50 | } catch (KeyManagementException e) { 51 | e.printStackTrace(); 52 | } catch (NoSuchAlgorithmException e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | def keystoreProperties = new Properties() 26 | def keystorePropertiesFile = rootProject.file('key.properties') 27 | if (keystorePropertiesFile.exists()) { 28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 29 | } 30 | 31 | android { 32 | compileSdkVersion 34 33 | 34 | sourceSets { 35 | main.java.srcDirs += 'src/main/kotlin' 36 | } 37 | 38 | lintOptions { 39 | disable 'InvalidPackage' 40 | } 41 | 42 | defaultConfig { 43 | applicationId "mstream.music" 44 | minSdkVersion flutter.minSdkVersion 45 | targetSdkVersion 34 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | multiDexEnabled true 49 | } 50 | 51 | signingConfigs { 52 | release { 53 | keyAlias keystoreProperties['keyAlias'] 54 | keyPassword keystoreProperties['keyPassword'] 55 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 56 | storePassword keystoreProperties['storePassword'] 57 | } 58 | } 59 | buildTypes { 60 | release { 61 | signingConfig signingConfigs.release 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation 'com.android.support:multidex:1.0.3' 72 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20" 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mstream_music 2 | description: A music streaming app for mStream Server 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 0.14.0+25 19 | 20 | environment: 21 | sdk: ">=2.15.1 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | http: ^1.2.2 27 | uuid: ^4.5.0 28 | rxdart: ^0.28.0 29 | disk_space: ^0.2.1 30 | path_provider: ^2.1.4 31 | audio_session: ^0.1.21 32 | audio_service: ^0.18.15 33 | just_audio: ^0.9.40 34 | flutter_slidable: ^0.6.0 35 | flutter_downloader: ^1.11.8 36 | flutter_launcher_icons: ^0.13.1 37 | flutter_barcode_scanner: ^2.0.0 38 | cupertino_icons: ^1.0.8 39 | path: any 40 | dev_dependencies: 41 | flutter_test: 42 | sdk: flutter 43 | 44 | flutter_icons: 45 | android: true 46 | ios: true 47 | image_path_android: "graphics/icon/icon.png" 48 | image_path_ios: "graphics/icon/mstream.png" 49 | 50 | # For information on the generic Dart part of this file, see the 51 | # following page: https://dart.dev/tools/pub/pubspec 52 | 53 | # The following section is specific to Flutter. 54 | flutter: 55 | assets: 56 | - graphics/mstream-logo.png 57 | - graphics/default.png 58 | # The following line ensures that the Material Icons font is 59 | # included with your application, so that you can use the icons in 60 | # the material Icons class. 61 | uses-material-design: true 62 | 63 | # To add assets to your application, add an assets section, like this: 64 | # assets: 65 | # - images/a_dot_burr.jpeg 66 | # - images/a_dot_ham.jpeg 67 | 68 | # An image asset can refer to one or more resolution-specific "variants", see 69 | # https://flutter.dev/assets-and-images/#resolution-aware. 70 | 71 | # For details regarding adding assets from package dependencies, see 72 | # https://flutter.dev/assets-and-images/#from-packages 73 | 74 | fonts: 75 | - family: Jura 76 | fonts: 77 | - asset: fonts/Jura-Bold.ttf 78 | weight: 700 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/screens/downloads.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:disk_space/disk_space.dart'; 3 | 4 | import '../singletons/downloads.dart'; 5 | import '../objects/download_tracker.dart'; 6 | 7 | class DownloadScreen extends StatelessWidget { 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBar( 11 | title: Text("Downloads"), 12 | ), 13 | body: Column(children: [ 14 | Card( 15 | child: Column( 16 | mainAxisSize: MainAxisSize.min, 17 | children: [ 18 | Text( 19 | 'Total Storage Space (in MiB)', 20 | style: TextStyle( 21 | color: Colors.blue, 22 | ), 23 | ), 24 | FutureBuilder( 25 | future: DiskSpace.getTotalDiskSpace, 26 | builder: (BuildContext _, AsyncSnapshot snapshot) { 27 | print(snapshot.data.toString()); 28 | return Text( 29 | snapshot.data.toString(), 30 | style: TextStyle( 31 | color: Colors.blue, 32 | ), 33 | ); 34 | }, 35 | ) 36 | ], 37 | )), 38 | Expanded( 39 | child: SizedBox( 40 | child: StreamBuilder>( 41 | stream: DownloadManager().downloadSream, 42 | builder: (context, snapshot) { 43 | final List dList = snapshot 44 | .data?.entries 45 | .map((e) => e.value) 46 | .toList() ?? 47 | []; 48 | 49 | return ListView.separated( 50 | physics: const AlwaysScrollableScrollPhysics(), 51 | itemCount: dList.length, 52 | separatorBuilder: 53 | (BuildContext context, int index) => 54 | Divider(height: 3, color: Colors.black), 55 | itemBuilder: (BuildContext context, int index) { 56 | return ListTile( 57 | title: Text( 58 | dList[index].filePath, 59 | style: TextStyle( 60 | color: Colors.black, 61 | ), 62 | ), 63 | subtitle: Text( 64 | 'progress: ' + 65 | dList[index].progress.toString() + 66 | '%', 67 | style: TextStyle( 68 | color: Colors.black, 69 | ), 70 | ), 71 | ); 72 | }); 73 | }))) 74 | ])); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/singletons/file_explorer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:path/path.dart' as path; 6 | import 'package:path_provider/path_provider.dart'; 7 | 8 | import './browser_list.dart'; 9 | import '../objects/display_item.dart'; 10 | import '../objects/server.dart'; 11 | 12 | class FileExplorer { 13 | FileExplorer._privateConstructor(); 14 | static final FileExplorer _instance = FileExplorer._privateConstructor(); 15 | factory FileExplorer() { 16 | return _instance; 17 | } 18 | 19 | Future getServerDir(Server s) async { 20 | Directory? woo = await getDownloadDir(s.saveToSdCard); 21 | if (woo == null) { 22 | return 'NO SD CARD DETECTED'; 23 | } 24 | 25 | return new Directory(path.join(woo.path.toString(), 'media/${s.localname}')) 26 | .path 27 | .toString(); 28 | } 29 | 30 | Future getLocalFiles(String? directory, Server s) async { 31 | BrowserManager().setBrowserLabel('Local Files'); 32 | List newList = []; 33 | 34 | Directory file; 35 | if (directory == null) { 36 | BrowserManager().clear(); 37 | Directory? woo = await getDownloadDir(s.saveToSdCard); 38 | if (woo == null) { 39 | return; 40 | } 41 | file = new Directory(path.join(woo.path.toString(), 'media')); 42 | } else { 43 | file = new Directory(directory); 44 | } 45 | 46 | int stringLength = file.path.toString().length + 47 | 1; // The plug ones covers the extra `/` that will be on the results 48 | 49 | file 50 | .list(recursive: false, followLinks: false) 51 | .listen((FileSystemEntity entity) { 52 | print(entity.path); 53 | Icon useIcon; 54 | String type; 55 | if (entity is File) { 56 | useIcon = new Icon(Icons.music_note, color: Colors.black); 57 | type = 'localFile'; 58 | } else { 59 | useIcon = new Icon(Icons.folder_open_outlined, color: Colors.black); 60 | type = 'localDirectory'; 61 | } 62 | 63 | String thisName = entity.path.substring(stringLength, entity.path.length); 64 | DisplayItem newItem = 65 | new DisplayItem(s, thisName, type, entity.path, useIcon, null); 66 | newList.add(newItem); 67 | }).onDone(() { 68 | BrowserManager().addListToStack(newList); 69 | }); 70 | } 71 | 72 | Future getPathForServer(Server s) async { 73 | Directory? woo = await getDownloadDir(s.saveToSdCard); 74 | if (woo != null) { 75 | Directory file = 76 | new Directory(path.join(woo.path.toString(), 'media/${s.localname}')); 77 | getLocalFiles(file.path.toString(), s); 78 | } 79 | } 80 | 81 | Future deleteFile(String path, Server? server) async { 82 | File f = File(path); 83 | if (f.existsSync()) { 84 | await f.delete(); 85 | } 86 | 87 | BrowserManager().removeAll(path, server, 'localFile'); 88 | } 89 | 90 | Future deleteDirectory(String path, Server? server) async { 91 | Directory f = Directory(path); 92 | if (f.existsSync()) { 93 | await f.delete(recursive: true); 94 | } 95 | 96 | BrowserManager().removeAll(path, server, 'localDirectory'); 97 | } 98 | 99 | Future getDownloadDir(bool sd) { 100 | if (sd == false) { 101 | return getApplicationDocumentsDirectory(); 102 | } 103 | 104 | return getExternalStorageDirectory(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Do not remove or rename entries in this file, only add new ones 2 | # See https://github.com/flutter/flutter/issues/128635 for more context. 3 | 4 | # Miscellaneous 5 | *.class 6 | *.lock 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # Visual Studio Code related 23 | .classpath 24 | .project 25 | .settings/ 26 | .vscode/* 27 | 28 | # Flutter repo-specific 29 | /bin/cache/ 30 | /bin/internal/bootstrap.bat 31 | /bin/internal/bootstrap.sh 32 | /bin/mingit/ 33 | /dev/benchmarks/mega_gallery/ 34 | /dev/bots/.recipe_deps 35 | /dev/bots/android_tools/ 36 | /dev/devicelab/ABresults*.json 37 | /dev/docs/doc/ 38 | /dev/docs/api_docs.zip 39 | /dev/docs/flutter.docs.zip 40 | /dev/docs/lib/ 41 | /dev/docs/pubspec.yaml 42 | /dev/integration_tests/**/xcuserdata 43 | /dev/integration_tests/**/Pods 44 | /packages/flutter/coverage/ 45 | version 46 | analysis_benchmark.json 47 | 48 | # packages file containing multi-root paths 49 | .packages.generated 50 | 51 | # Flutter/Dart/Pub related 52 | **/doc/api/ 53 | .dart_tool/ 54 | .flutter-plugins 55 | .flutter-plugins-dependencies 56 | **/generated_plugin_registrant.dart 57 | .packages 58 | .pub-preload-cache/ 59 | .pub-cache/ 60 | .pub/ 61 | build/ 62 | flutter_*.png 63 | linked_*.ds 64 | unlinked.ds 65 | unlinked_spec.ds 66 | 67 | # Android related 68 | **/android/**/gradle-wrapper.jar 69 | .gradle/ 70 | **/android/captures/ 71 | **/android/gradlew 72 | **/android/gradlew.bat 73 | **/android/local.properties 74 | **/android/**/GeneratedPluginRegistrant.java 75 | **/android/key.properties 76 | *.jks 77 | 78 | # iOS/XCode related 79 | **/ios/**/*.mode1v3 80 | **/ios/**/*.mode2v3 81 | **/ios/**/*.moved-aside 82 | **/ios/**/*.pbxuser 83 | **/ios/**/*.perspectivev3 84 | **/ios/**/*sync/ 85 | **/ios/**/.sconsign.dblite 86 | **/ios/**/.tags* 87 | **/ios/**/.vagrant/ 88 | **/ios/**/DerivedData/ 89 | **/ios/**/Icon? 90 | **/ios/**/Pods/ 91 | **/ios/**/.symlinks/ 92 | **/ios/**/profile 93 | **/ios/**/xcuserdata 94 | **/ios/.generated/ 95 | **/ios/Flutter/.last_build_id 96 | **/ios/Flutter/App.framework 97 | **/ios/Flutter/Flutter.framework 98 | **/ios/Flutter/Flutter.podspec 99 | **/ios/Flutter/Generated.xcconfig 100 | **/ios/Flutter/ephemeral 101 | **/ios/Flutter/app.flx 102 | **/ios/Flutter/app.zip 103 | **/ios/Flutter/flutter_assets/ 104 | **/ios/Flutter/flutter_export_environment.sh 105 | **/ios/ServiceDefinitions.json 106 | **/ios/Runner/GeneratedPluginRegistrant.* 107 | 108 | # macOS 109 | **/Flutter/ephemeral/ 110 | **/Pods/ 111 | **/macos/Flutter/GeneratedPluginRegistrant.swift 112 | **/macos/Flutter/ephemeral 113 | **/xcuserdata/ 114 | 115 | # Windows 116 | **/windows/flutter/ephemeral/ 117 | **/windows/flutter/generated_plugin_registrant.cc 118 | **/windows/flutter/generated_plugin_registrant.h 119 | **/windows/flutter/generated_plugins.cmake 120 | 121 | # Linux 122 | **/linux/flutter/ephemeral/ 123 | **/linux/flutter/generated_plugin_registrant.cc 124 | **/linux/flutter/generated_plugin_registrant.h 125 | **/linux/flutter/generated_plugins.cmake 126 | 127 | # Coverage 128 | coverage/ 129 | 130 | # Symbols 131 | app.*.symbols 132 | 133 | # Exceptions to above rules. 134 | !**/ios/**/default.mode1v3 135 | !**/ios/**/default.mode2v3 136 | !**/ios/**/default.pbxuser 137 | !**/ios/**/default.perspectivev3 138 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 139 | !/dev/ci/**/Gemfile.lock 140 | !.vscode/settings.json 141 | -------------------------------------------------------------------------------- /lib/objects/display_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'server.dart'; 6 | import 'metadata.dart'; 7 | import '../singletons/browser_list.dart'; 8 | import '../singletons/file_explorer.dart'; 9 | 10 | class DisplayItem { 11 | final Server? server; 12 | final String type; 13 | final String? data; 14 | 15 | bool showRating = false; 16 | 17 | Icon? icon; 18 | String name; 19 | MusicMetadata? metadata; 20 | String? altAlbumArt; 21 | String? subtext; 22 | 23 | int downloadProgress = 0; 24 | 25 | Widget? getImage() { 26 | String? aaFile = altAlbumArt ?? this.metadata?.albumArt ?? null; 27 | 28 | if (this.server != null && aaFile != null) { 29 | String lolUrl = Uri.encodeFull(this.server!.url + 30 | '/album-art/' + 31 | aaFile + 32 | '?compress=s' + 33 | (this.server!.jwt == null ? '' : '&token=' + this.server!.jwt!)); 34 | 35 | return Image.network(lolUrl.toString()); 36 | } 37 | 38 | return icon; 39 | } 40 | 41 | Widget getText() { 42 | if (metadata?.title != null) { 43 | return Text( 44 | (showRating == true && metadata?.rating != null 45 | ? '[' + (metadata!.rating! / 2).toString() + '] ' 46 | : '') + 47 | metadata!.title!, 48 | style: TextStyle(color: Colors.black), 49 | ); 50 | } 51 | 52 | if (type == 'file' || type == 'localFile') { 53 | return new Text( 54 | (showRating == true && metadata?.rating != null 55 | ? '[' + (metadata!.rating! / 2).toString() + '] ' 56 | : '') + 57 | this.data!.split('/').last, 58 | style: TextStyle(fontSize: 18, color: Colors.black)); 59 | } 60 | 61 | return new Text(this.name, 62 | style: 63 | TextStyle(fontFamily: 'Jura', fontSize: 18, color: Colors.black)); 64 | } 65 | 66 | Widget? getSubText() { 67 | if (metadata?.artist != null) { 68 | return Text( 69 | metadata!.artist!, 70 | style: TextStyle(fontSize: 16, color: Colors.black), 71 | ); 72 | } 73 | 74 | if (subtext != null) { 75 | return new Text( 76 | subtext!, 77 | style: TextStyle(fontSize: 16, color: Colors.black), 78 | ); 79 | } 80 | 81 | return null; 82 | } 83 | 84 | DisplayItem( 85 | this.server, this.name, this.type, this.data, this.icon, this.subtext) { 86 | // Check if file is saved on device 87 | if (this.type == 'file') { 88 | String downloadDirectory = this.server!.localname + this.data!; 89 | FileExplorer().getDownloadDir(this.server!.saveToSdCard).then((dir) { 90 | if (dir == null) { 91 | return; 92 | } 93 | String finalString = '${dir.path}/media/$downloadDirectory'; 94 | 95 | new File(finalString).exists().then((ex) { 96 | if (ex == true) { 97 | this.downloadProgress = 100; 98 | BrowserManager().updateStream(); 99 | } 100 | }); 101 | }); 102 | } 103 | } 104 | 105 | // DisplayItem.fromJson(Map json) 106 | // : name = json['name'], 107 | // type = json['type'], 108 | // server = json['server'], 109 | // subtext = json['subtext'], 110 | // data = json['data']; 111 | 112 | // Map toJson() => { 113 | // 'name': name, 114 | // 'server': server, 115 | // 'type': type, 116 | // 'subtext': subtext, 117 | // 'data': data 118 | // }; 119 | } 120 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 33 | 37 | 41 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /lib/singletons/downloads.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | import 'dart:ui'; 3 | import 'dart:async'; 4 | import 'dart:io'; 5 | 6 | import 'package:mstream_music/singletons/file_explorer.dart'; 7 | import 'package:mstream_music/singletons/server_list.dart'; 8 | import 'package:rxdart/rxdart.dart'; 9 | import 'package:flutter_downloader/flutter_downloader.dart'; 10 | import 'package:path/path.dart' as path; 11 | 12 | import '../objects/download_tracker.dart'; 13 | 14 | class DownloadManager { 15 | DownloadManager._privateConstructor(); 16 | static final DownloadManager _instance = 17 | DownloadManager._privateConstructor(); 18 | factory DownloadManager() { 19 | return _instance; 20 | } 21 | 22 | // streams 23 | late final BehaviorSubject> _downloadStream = 24 | BehaviorSubject>.seeded(downloadMap); 25 | 26 | Map downloadMap = {}; 27 | ReceivePort _port = ReceivePort(); 28 | 29 | initDownloader() async { 30 | await FlutterDownloader.initialize(); 31 | IsolateNameServer.registerPortWithName( 32 | _port.sendPort, 'downloader_send_port'); 33 | // bindBackgroundIsolate(); 34 | 35 | _port.listen((dynamic data) { 36 | _syncItem(data[0], data[1], data[2]); 37 | }); 38 | 39 | FlutterDownloader.registerCallback(_callbackDownloader); 40 | } 41 | 42 | static void _callbackDownloader(id, status, progress) { 43 | print('Download task ($id) is in status ($status) and process ($progress)'); 44 | final SendPort send = 45 | IsolateNameServer.lookupPortByName('downloader_send_port')!; 46 | send.send([id, status, progress]); 47 | } 48 | 49 | disposeDownloader() {} 50 | 51 | void dispose() { 52 | _downloadStream.close(); 53 | } 54 | 55 | void unbindBackgroundIsolate() { 56 | IsolateNameServer.removePortNameMapping('downloader_send_port'); 57 | } 58 | 59 | void bindBackgroundIsolate() { 60 | bool isSuccess = IsolateNameServer.registerPortWithName( 61 | _port.sendPort, 'downloader_send_port'); 62 | if (!isSuccess) { 63 | unbindBackgroundIsolate(); 64 | bindBackgroundIsolate(); 65 | return; 66 | } 67 | _port.listen((dynamic data) { 68 | // if (debug) { 69 | // print('UI Isolate Callback: $data'); 70 | // } 71 | String id = data[0]; 72 | DownloadTaskStatus status = data[1]; 73 | int progress = data[2]; 74 | 75 | _syncItem(id, status, progress); 76 | }); 77 | } 78 | 79 | Future _syncItem( 80 | String id, DownloadTaskStatus status, int progress) async { 81 | try { 82 | DownloadTracker dt = downloadMap[id]!; 83 | if (status == DownloadTaskStatus.complete) { 84 | // TODO: update queue items 85 | } 86 | 87 | // dt.referenceDisplayItem?.downloadProgress = progress; 88 | dt.progress = progress; 89 | } catch (err) { 90 | print(err); 91 | } 92 | } 93 | 94 | Future downloadOneFile(String downloadUrl, String serverName, 95 | String filepath, bool? saveToSdCard) async { 96 | String downloadDirectory = serverName + filepath; 97 | 98 | bool sd = 99 | saveToSdCard ?? ServerManager().lookupServer(serverName).saveToSdCard; 100 | final dir = await FileExplorer().getDownloadDir(sd); 101 | if (dir == null) { 102 | return; 103 | } 104 | 105 | String downloadTo = '${dir.path}/media/$downloadDirectory'; 106 | 107 | if (new File(downloadTo).existsSync() == true) { 108 | print('exists!'); 109 | return; 110 | } 111 | 112 | String lol = path.dirname(downloadTo); 113 | String filename = path.basename(downloadTo); 114 | 115 | new Directory(lol).createSync(recursive: true); 116 | Uri url = Uri.parse(downloadUrl); 117 | 118 | String? taskId = await FlutterDownloader.enqueue( 119 | url: downloadUrl, 120 | fileName: filename, 121 | savedDir: lol, 122 | showNotification: 123 | false, // show download progress in status bar (for Android) 124 | openFileFromNotification: 125 | false, // click on notification to open downloaded file (for Android) 126 | ); 127 | 128 | downloadMap[taskId!] = new DownloadTracker(downloadUrl, downloadDirectory); 129 | } 130 | 131 | Stream> get downloadSream => 132 | _downloadStream.stream; 133 | } 134 | -------------------------------------------------------------------------------- /lib/singletons/browser_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rxdart/rxdart.dart'; 3 | 4 | import '../singletons/server_list.dart'; 5 | import '../objects/display_item.dart'; 6 | import '../objects/server.dart'; 7 | 8 | class BrowserManager { 9 | final List> browserCache = []; 10 | final List scrollCache = []; 11 | 12 | final List browserList = []; 13 | 14 | String listName = 'Welcome'; 15 | bool loading = false; 16 | 17 | late final BehaviorSubject> _browserStream = 18 | BehaviorSubject>.seeded(browserList); 19 | late final BehaviorSubject _browserLabel = 20 | BehaviorSubject.seeded(listName); 21 | 22 | BrowserManager._privateConstructor(); 23 | static final BrowserManager _instance = BrowserManager._privateConstructor(); 24 | 25 | // scroll controller in stream format 26 | ScrollController sc = ScrollController(); 27 | 28 | factory BrowserManager() { 29 | return _instance; 30 | } 31 | 32 | void setBrowserLabel(String label) { 33 | listName = label; 34 | _browserLabel.sink.add(label); 35 | } 36 | 37 | void clear() { 38 | List hold = browserCache[0]; 39 | 40 | browserCache.clear(); 41 | browserList.clear(); 42 | 43 | browserCache.add(hold); 44 | 45 | scrollCache.clear(); 46 | } 47 | 48 | void goToNavScreen() { 49 | browserCache.clear(); 50 | browserList.clear(); 51 | 52 | if (ServerManager().currentServer == null) { 53 | return; 54 | } 55 | 56 | DisplayItem newItem1 = new DisplayItem( 57 | ServerManager().currentServer!, 58 | 'File Explorer', 59 | 'execAction', 60 | 'fileExplorer', 61 | Icon(Icons.folder, color: Color(0xFFffab00)), 62 | null); 63 | 64 | DisplayItem newItem2 = new DisplayItem( 65 | ServerManager().currentServer!, 66 | 'Playlists', 67 | 'execAction', 68 | 'playlists', 69 | Icon(Icons.queue_music, color: Colors.black), 70 | null); 71 | 72 | DisplayItem newItem3 = new DisplayItem( 73 | ServerManager().currentServer!, 74 | 'Albums', 75 | 'execAction', 76 | 'albums', 77 | Icon(Icons.album, color: Colors.black), 78 | null); 79 | 80 | DisplayItem newItem4 = new DisplayItem( 81 | ServerManager().currentServer!, 82 | 'Artists', 83 | 'execAction', 84 | 'artists', 85 | Icon(Icons.library_music, color: Colors.black), 86 | null); 87 | 88 | DisplayItem newItem5 = new DisplayItem( 89 | ServerManager().currentServer!, 90 | 'Rated', 91 | 'execAction', 92 | 'rated', 93 | Icon(Icons.star, color: Colors.black), 94 | null); 95 | 96 | DisplayItem newItem6 = new DisplayItem( 97 | ServerManager().currentServer!, 98 | 'Recent', 99 | 'execAction', 100 | 'recent', 101 | Icon(Icons.query_builder, color: Colors.black), 102 | null); 103 | 104 | DisplayItem newItem7 = new DisplayItem( 105 | ServerManager().currentServer!, 106 | 'Local Files', 107 | 'execAction', 108 | 'localFiles', 109 | Icon(Icons.folder_open_outlined, color: Colors.black), 110 | null); 111 | 112 | browserCache.add( 113 | [newItem1, newItem2, newItem3, newItem4, newItem5, newItem6, newItem7]); 114 | browserList.add(newItem1); 115 | browserList.add(newItem2); 116 | browserList.add(newItem3); 117 | browserList.add(newItem4); 118 | browserList.add(newItem5); 119 | browserList.add(newItem6); 120 | browserList.add(newItem7); 121 | 122 | _browserLabel.sink.add('Browser'); 123 | _browserStream.sink.add(browserList); 124 | } 125 | 126 | void noServerScreen() { 127 | browserCache.clear(); 128 | browserList.clear(); 129 | scrollCache.clear(); 130 | 131 | browserList.add(new DisplayItem(null, 'Welcome To mStream', 'addServer', '', 132 | Icon(Icons.add, color: Colors.black), 'Click here to add server')); 133 | 134 | _browserStream.sink.add(browserList); 135 | } 136 | 137 | void addListToStack(List newList) { 138 | browserCache.add(newList); 139 | 140 | // TODO: throws an error when you click the file explorer tab from the queue 141 | // scrollCache.add(sc.offset); 142 | 143 | browserList.clear(); 144 | newList.forEach((element) { 145 | browserList.add(element); 146 | }); 147 | 148 | _browserStream.sink.add(browserList); 149 | } 150 | 151 | updateStream() { 152 | _browserStream.sink.add(browserList); 153 | } 154 | 155 | void popBrowser() { 156 | if (BrowserManager().browserCache.length < 2) { 157 | return; 158 | } 159 | 160 | browserCache.removeLast(); 161 | browserList.clear(); 162 | browserCache[browserCache.length - 1].forEach((el) { 163 | browserList.add(el); 164 | }); 165 | 166 | _browserStream.sink.add(browserList); 167 | 168 | if (BrowserManager().browserCache.length == 1) { 169 | _browserLabel.sink.add('Browser'); 170 | } 171 | 172 | // double scrollTo = scrollCache.removeLast(); 173 | // sc.jumpTo(scrollTo); 174 | } 175 | 176 | void removeAll(String data, Server? server, String type) { 177 | browserList.removeWhere( 178 | (e) => e.server == server && e.data == data && e.type == type); 179 | _browserStream.sink.add(browserList); 180 | 181 | browserCache.forEach((b) { 182 | b.removeWhere( 183 | (e) => e.server == server && e.data == data && e.type == type); 184 | }); 185 | } 186 | 187 | void dispose() { 188 | _browserStream.close(); 189 | _browserLabel.close(); 190 | } 191 | 192 | Stream> get browserListStream => _browserStream.stream; 193 | Stream get broswerLabelStream => _browserLabel.stream; 194 | } 195 | -------------------------------------------------------------------------------- /lib/singletons/server_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'package:mstream_music/singletons/file_explorer.dart'; 5 | 6 | import '../objects/server.dart'; 7 | import './browser_list.dart'; 8 | 9 | import 'package:path_provider/path_provider.dart'; 10 | import 'package:path/path.dart' as path; 11 | import 'package:rxdart/rxdart.dart'; 12 | import 'package:http/http.dart' as http; 13 | 14 | class ServerManager { 15 | final List serverList = []; 16 | Server? currentServer; 17 | 18 | // streams 19 | late final BehaviorSubject> _serverListStream = 20 | BehaviorSubject>.seeded(serverList); 21 | late final BehaviorSubject _currentServerStream = 22 | BehaviorSubject.seeded(currentServer); 23 | 24 | ServerManager._privateConstructor(); 25 | static final ServerManager _instance = ServerManager._privateConstructor(); 26 | 27 | factory ServerManager() { 28 | return _instance; 29 | } 30 | 31 | Future get _serverFile async { 32 | final directory = await getApplicationDocumentsDirectory(); 33 | final path = directory.path; 34 | return File('$path/servers.json'); 35 | } 36 | 37 | Future writeServerFile() async { 38 | final file = await _serverFile; 39 | 40 | // Write the file 41 | return file.writeAsString(jsonEncode(serverList)); 42 | } 43 | 44 | Future readServerManager() async { 45 | try { 46 | final file = await _serverFile; 47 | 48 | // Read the file 49 | String contents = await file.readAsString(); 50 | return jsonDecode(contents); 51 | } catch (e) { 52 | // If we encounter an error, return 0 53 | return []; 54 | } 55 | } 56 | 57 | Future loadServerList() async { 58 | List serversJson = await readServerManager(); 59 | 60 | serversJson.forEach((s) { 61 | Server newServer = Server.fromJson(s); 62 | serverList.add(newServer); 63 | }); 64 | 65 | _serverListStream.sink.add(serverList); 66 | 67 | if (serverList.length > 0) { 68 | currentServer = serverList[0]; 69 | BrowserManager().goToNavScreen(); 70 | _currentServerStream.sink.add(currentServer); 71 | serverList.forEach((Server s) { 72 | getServerPaths(s); 73 | }); 74 | } else { 75 | BrowserManager().noServerScreen(); 76 | } 77 | } 78 | 79 | Future addServer(Server newServer) async { 80 | serverList.add(newServer); 81 | 82 | if (currentServer == null) { 83 | currentServer = newServer; 84 | _currentServerStream.sink.add(currentServer); 85 | BrowserManager().goToNavScreen(); 86 | } 87 | 88 | // Create server directory (for downloads) 89 | Directory? file = 90 | await FileExplorer().getDownloadDir(newServer.saveToSdCard); 91 | if (file != null) { 92 | String dir = path.join(file.path, "media/${newServer.localname}"); 93 | await new Directory(dir).create(recursive: true); 94 | } 95 | 96 | await writeServerFile(); 97 | 98 | _serverListStream.sink.add(serverList); 99 | } 100 | 101 | Future editServer(int serverIndex, String url, String? username, 102 | String? password, bool saveToSd) async { 103 | serverList[serverIndex].url = url; 104 | ServerManager().serverList[serverIndex].password = password; 105 | ServerManager().serverList[serverIndex].username = username; 106 | ServerManager().serverList[serverIndex].saveToSdCard = saveToSd; 107 | 108 | await callAfterEditServer(); 109 | } 110 | 111 | void changeCurrentServer(int currentServerIndex) { 112 | currentServer = serverList[currentServerIndex]; 113 | _currentServerStream.sink.add(currentServer); 114 | BrowserManager().goToNavScreen(); 115 | } 116 | 117 | Future getServerPaths(Server server, {bool throwErr = false}) async { 118 | try { 119 | var response = await http 120 | .get(Uri.parse(server.url).resolve('/api/v1/ping'), headers: { 121 | 'Content-Type': 'application/json', 122 | 'x-access-token': server.jwt ?? '' 123 | }).timeout(Duration(seconds: 5)); 124 | 125 | if (response.statusCode != 200) { 126 | throw Exception('Failed to connect to server'); 127 | } 128 | 129 | var res = jsonDecode(response.body); 130 | 131 | Set pathCompare = new Set(); 132 | for (var i = 0; i < res['vpaths'].length; i++) { 133 | pathCompare.add(res['vpaths'][i]); 134 | // add new keys 135 | if (!server.autoDJPaths.containsKey(res['vpaths'][i])) { 136 | server.autoDJPaths[res['vpaths'][i]] = true; 137 | } 138 | } 139 | 140 | // Remove outdated entries 141 | server.autoDJPaths 142 | .removeWhere((key, value) => !pathCompare.contains(key)); 143 | 144 | // Make sure all entries are not false 145 | bool falseFlag = true; 146 | server.autoDJPaths.forEach((key, value) { 147 | if (value == true) { 148 | falseFlag = false; 149 | } 150 | }); 151 | if (falseFlag == true) { 152 | server.autoDJPaths.forEach((key, value) { 153 | server.autoDJPaths[key] = true; 154 | }); 155 | } 156 | 157 | // Update Playlists 158 | server.playlists.clear(); 159 | for (var i = 0; i < res['playlists'].length; i++) { 160 | server.playlists.add(res['playlists'][i]); 161 | } 162 | } catch (err) { 163 | if (throwErr) { 164 | throw err; 165 | } 166 | } 167 | } 168 | 169 | Future removeServer( 170 | Server removeThisServer, bool removeSyncedFiles) async { 171 | serverList.remove(removeThisServer); 172 | _serverListStream.sink.add(serverList); 173 | 174 | if (serverList.length == 0) { 175 | // force the browser to rerender so it displays 176 | BrowserManager().noServerScreen(); 177 | 178 | currentServer = null; 179 | _currentServerStream.sink.add(currentServer); 180 | } else if (removeThisServer == currentServer) { 181 | currentServer = serverList[0]; 182 | // clear the browser 183 | BrowserManager().goToNavScreen(); 184 | _currentServerStream.sink.add(currentServer); 185 | } 186 | 187 | await writeServerFile(); 188 | 189 | // delete synced files 190 | // if (removeSyncedFiles == true) { 191 | // _deleteServeDirectory(removeThisServer); 192 | // } 193 | } 194 | 195 | Future callAfterEditServer() async { 196 | _serverListStream.sink.add(serverList); 197 | await writeServerFile(); 198 | } 199 | 200 | Future _deleteServeDirectory(Server removedServer) async { 201 | Directory? directory = 202 | await FileExplorer().getDownloadDir(removedServer.saveToSdCard); 203 | if (directory != null) { 204 | Directory dir = new Directory(path.join( 205 | directory.path.toString(), "media/${removedServer.localname}")); 206 | dir.delete(recursive: true); 207 | } 208 | } 209 | 210 | void makeDefault(int i) { 211 | Server s = serverList[i]; 212 | 213 | serverList.remove(s); 214 | serverList.insert(0, s); 215 | 216 | _serverListStream.sink.add(serverList); 217 | } 218 | 219 | Server lookupServer(String id) { 220 | return serverList.firstWhere((e) => e.localname == id); 221 | } 222 | 223 | void dispose() { 224 | _serverListStream.close(); 225 | _currentServerStream.close(); 226 | } //initializes the subject with element already; 227 | 228 | Stream get curentServerStream => _currentServerStream.stream; 229 | 230 | Stream> get serverListStream => _serverListStream.stream; 231 | } 232 | -------------------------------------------------------------------------------- /lib/media/common.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | import 'package:audio_service/audio_service.dart'; 3 | import 'package:rxdart/rxdart.dart'; 4 | 5 | class LoggingAudioHandler extends CompositeAudioHandler { 6 | LoggingAudioHandler(AudioHandler inner) : super(inner) { 7 | playbackState.listen((state) { 8 | _log('playbackState changed: $state'); 9 | }); 10 | queue.listen((queue) { 11 | _log('queue changed: $queue'); 12 | }); 13 | queueTitle.listen((queueTitle) { 14 | _log('queueTitle changed: $queueTitle'); 15 | }); 16 | mediaItem.listen((mediaItem) { 17 | _log('mediaItem changed: $mediaItem'); 18 | }); 19 | ratingStyle.listen((ratingStyle) { 20 | _log('ratingStyle changed: $ratingStyle'); 21 | }); 22 | androidPlaybackInfo.listen((androidPlaybackInfo) { 23 | _log('androidPlaybackInfo changed: $androidPlaybackInfo'); 24 | }); 25 | customEvent.listen((dynamic customEventStream) { 26 | _log('customEvent changed: $customEventStream'); 27 | }); 28 | customState.listen((dynamic customState) { 29 | _log('customState changed: $customState'); 30 | }); 31 | } 32 | 33 | // TODO: Use logger. Use different log levels. 34 | void _log(String s) => print('----- LOG: $s'); 35 | 36 | @override 37 | Future prepare() { 38 | _log('prepare()'); 39 | return super.prepare(); 40 | } 41 | 42 | @override 43 | Future prepareFromMediaId(String mediaId, 44 | [Map? extras]) { 45 | _log('prepareFromMediaId($mediaId, $extras)'); 46 | return super.prepareFromMediaId(mediaId, extras); 47 | } 48 | 49 | @override 50 | Future prepareFromSearch(String query, [Map? extras]) { 51 | _log('prepareFromSearch($query, $extras)'); 52 | return super.prepareFromSearch(query, extras); 53 | } 54 | 55 | @override 56 | Future prepareFromUri(Uri uri, [Map? extras]) { 57 | _log('prepareFromSearch($uri, $extras)'); 58 | return super.prepareFromUri(uri, extras); 59 | } 60 | 61 | @override 62 | Future play() { 63 | _log('play()'); 64 | return super.play(); 65 | } 66 | 67 | @override 68 | Future playFromMediaId(String mediaId, [Map? extras]) { 69 | _log('playFromMediaId($mediaId, $extras)'); 70 | return super.playFromMediaId(mediaId, extras); 71 | } 72 | 73 | @override 74 | Future playFromSearch(String query, [Map? extras]) { 75 | _log('playFromSearch($query, $extras)'); 76 | return super.playFromSearch(query, extras); 77 | } 78 | 79 | @override 80 | Future playFromUri(Uri uri, [Map? extras]) { 81 | _log('playFromUri($uri, $extras)'); 82 | return super.playFromUri(uri, extras); 83 | } 84 | 85 | @override 86 | Future playMediaItem(MediaItem mediaItem) { 87 | _log('playMediaItem($mediaItem)'); 88 | return super.playMediaItem(mediaItem); 89 | } 90 | 91 | @override 92 | Future pause() { 93 | _log('pause()'); 94 | return super.pause(); 95 | } 96 | 97 | @override 98 | Future click([MediaButton button = MediaButton.media]) { 99 | _log('click($button)'); 100 | return super.click(button); 101 | } 102 | 103 | @override 104 | Future stop() { 105 | _log('stop()'); 106 | return super.stop(); 107 | } 108 | 109 | @override 110 | Future addQueueItem(MediaItem mediaItem) { 111 | _log('addQueueItem($mediaItem)'); 112 | return super.addQueueItem(mediaItem); 113 | } 114 | 115 | @override 116 | Future addQueueItems(List mediaItems) { 117 | _log('addQueueItems($mediaItems)'); 118 | return super.addQueueItems(mediaItems); 119 | } 120 | 121 | @override 122 | Future insertQueueItem(int index, MediaItem mediaItem) { 123 | _log('insertQueueItem($index, $mediaItem)'); 124 | return super.insertQueueItem(index, mediaItem); 125 | } 126 | 127 | @override 128 | Future updateQueue(List queue) { 129 | _log('updateQueue($queue)'); 130 | return super.updateQueue(queue); 131 | } 132 | 133 | @override 134 | Future updateMediaItem(MediaItem mediaItem) { 135 | _log('updateMediaItem($mediaItem)'); 136 | return super.updateMediaItem(mediaItem); 137 | } 138 | 139 | @override 140 | Future removeQueueItem(MediaItem mediaItem) { 141 | _log('removeQueueItem($mediaItem)'); 142 | return super.removeQueueItem(mediaItem); 143 | } 144 | 145 | @override 146 | Future removeQueueItemAt(int index) { 147 | _log('removeQueueItemAt($index)'); 148 | return super.removeQueueItemAt(index); 149 | } 150 | 151 | @override 152 | Future skipToNext() { 153 | _log('skipToNext()'); 154 | return super.skipToNext(); 155 | } 156 | 157 | @override 158 | Future skipToPrevious() { 159 | _log('skipToPrevious()'); 160 | return super.skipToPrevious(); 161 | } 162 | 163 | @override 164 | Future fastForward() { 165 | _log('fastForward()'); 166 | return super.fastForward(); 167 | } 168 | 169 | @override 170 | Future rewind() { 171 | _log('rewind()'); 172 | return super.rewind(); 173 | } 174 | 175 | @override 176 | Future skipToQueueItem(int index) { 177 | _log('skipToQueueItem($index)'); 178 | return super.skipToQueueItem(index); 179 | } 180 | 181 | @override 182 | Future seek(Duration position) { 183 | _log('seek($position)'); 184 | return super.seek(position); 185 | } 186 | 187 | @override 188 | Future setRating(Rating rating, [Map? extras]) { 189 | _log('setRating($rating, $extras)'); 190 | return super.setRating(rating, extras); 191 | } 192 | 193 | @override 194 | Future setCaptioningEnabled(bool enabled) { 195 | _log('setCaptioningEnabled($enabled)'); 196 | return super.setCaptioningEnabled(enabled); 197 | } 198 | 199 | @override 200 | Future setRepeatMode(AudioServiceRepeatMode repeatMode) { 201 | _log('setRepeatMode($repeatMode)'); 202 | return super.setRepeatMode(repeatMode); 203 | } 204 | 205 | @override 206 | Future setShuffleMode(AudioServiceShuffleMode shuffleMode) { 207 | _log('setShuffleMode($shuffleMode)'); 208 | return super.setShuffleMode(shuffleMode); 209 | } 210 | 211 | @override 212 | Future seekBackward(bool begin) { 213 | _log('seekBackward($begin)'); 214 | return super.seekBackward(begin); 215 | } 216 | 217 | @override 218 | Future seekForward(bool begin) { 219 | _log('seekForward($begin)'); 220 | return super.seekForward(begin); 221 | } 222 | 223 | @override 224 | Future setSpeed(double speed) { 225 | _log('setSpeed($speed)'); 226 | return super.setSpeed(speed); 227 | } 228 | 229 | @override 230 | Future customAction(String name, 231 | [Map? extras]) async { 232 | _log('customAction($name, extras)'); 233 | final dynamic result = await super.customAction(name, extras); 234 | _log('customAction -> $result'); 235 | return result; 236 | } 237 | 238 | @override 239 | Future onTaskRemoved() { 240 | _log('onTaskRemoved()'); 241 | return super.onTaskRemoved(); 242 | } 243 | 244 | @override 245 | Future onNotificationDeleted() { 246 | _log('onNotificationDeleted()'); 247 | return super.onNotificationDeleted(); 248 | } 249 | 250 | @override 251 | Future> getChildren(String parentMediaId, 252 | [Map? options]) async { 253 | _log('getChildren($parentMediaId, $options)'); 254 | final result = await super.getChildren(parentMediaId, options); 255 | _log('getChildren -> $result'); 256 | return result; 257 | } 258 | 259 | @override 260 | ValueStream> subscribeToChildren(String parentMediaId) { 261 | _log('subscribeToChildren($parentMediaId)'); 262 | final result = super.subscribeToChildren(parentMediaId); 263 | result.listen((options) { 264 | _log('$parentMediaId children changed with options $options'); 265 | }); 266 | return result; 267 | } 268 | 269 | @override 270 | Future getMediaItem(String mediaId) async { 271 | _log('getMediaItem($mediaId)'); 272 | final result = await super.getMediaItem(mediaId); 273 | _log('getMediaItem -> $result'); 274 | return result; 275 | } 276 | 277 | @override 278 | Future> search(String query, 279 | [Map? extras]) async { 280 | _log('search($query, $extras)'); 281 | final result = await super.search(query, extras); 282 | _log('search -> $result'); 283 | return result; 284 | } 285 | 286 | @override 287 | Future androidSetRemoteVolume(int volumeIndex) { 288 | _log('androidSetRemoteVolume($volumeIndex)'); 289 | return super.androidSetRemoteVolume(volumeIndex); 290 | } 291 | 292 | @override 293 | Future androidAdjustRemoteVolume(AndroidVolumeDirection direction) { 294 | _log('androidAdjustRemoteVolume($direction)'); 295 | return super.androidAdjustRemoteVolume(direction); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /lib/screens/manage_server.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:mstream_music/singletons/file_explorer.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | import '../objects/server.dart'; 6 | import '../singletons/server_list.dart'; 7 | import 'add_server.dart'; 8 | 9 | class ManageServersScreen extends StatelessWidget { 10 | Widget generateDropdownMenu(BuildContext context, int index) { 11 | return PopupMenuButton( 12 | onSelected: (String command) { 13 | if (command == 'edit') { 14 | Navigator.push( 15 | context, 16 | MaterialPageRoute( 17 | builder: (context) => 18 | EditServerScreen(editThisServer: index))); 19 | } 20 | if (command == 'default') { 21 | ServerManager().makeDefault(index); 22 | } 23 | if (command == 'info') { 24 | // SimpleDialog(children: []); 25 | FileExplorer() 26 | .getServerDir(ServerManager().serverList[index]) 27 | .then((dir) { 28 | showDialog( 29 | context: context, 30 | builder: (BuildContext context) { 31 | return AlertDialog( 32 | title: Text('Server Info'), 33 | content: Column( 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Text(ServerManager().serverList[index].url), 38 | Text(''), 39 | Text('Download Folder:'), 40 | Text(''), 41 | Text(dir) 42 | ]), 43 | actions: [ 44 | TextButton( 45 | child: Text("Copy Download Path"), 46 | onPressed: () { 47 | Clipboard.setData(ClipboardData(text: dir)); 48 | Navigator.of(context).pop(); 49 | 50 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 51 | content: Text('Path Copied to Clipboard'))); 52 | }, 53 | ) 54 | ]); 55 | }, 56 | ); 57 | }); 58 | } 59 | if (command == 'delete') { 60 | showDialog( 61 | context: context, 62 | builder: (BuildContext context) { 63 | // return object of type Dialog 64 | return DeleteServerDialog( 65 | cServer: ServerManager().serverList[index]); 66 | }, 67 | ); 68 | } 69 | }, 70 | icon: Icon( 71 | Icons.arrow_drop_down, 72 | color: Colors.black, 73 | ), 74 | itemBuilder: (BuildContext context) { 75 | List> popUpWidgetList = [ 76 | PopupMenuItem( 77 | value: 'info', 78 | child: Row(children: [ 79 | Icon( 80 | Icons.info, 81 | color: Colors.black, 82 | ), 83 | Text(' Info', style: TextStyle(color: Colors.black)) 84 | ]), 85 | ), 86 | PopupMenuItem( 87 | value: 'edit', 88 | child: Row(children: [ 89 | Icon( 90 | Icons.edit, 91 | color: Colors.black, 92 | ), 93 | Text(' Edit', style: TextStyle(color: Colors.black)) 94 | ]), 95 | ), 96 | PopupMenuItem( 97 | value: 'delete', 98 | child: Row(children: [ 99 | Icon( 100 | Icons.delete, 101 | color: Colors.redAccent, 102 | ), 103 | Text(' Delete', style: TextStyle(color: Colors.black)) 104 | ]), 105 | ) 106 | ]; 107 | 108 | if (index != 0) { 109 | popUpWidgetList.insert( 110 | 0, 111 | PopupMenuItem( 112 | value: 'default', 113 | child: Row(children: [ 114 | Icon( 115 | Icons.arrow_upward_rounded, 116 | color: Colors.black, 117 | ), 118 | Text(' Make Default', 119 | style: TextStyle(color: Colors.black)) 120 | ]), 121 | )); 122 | } 123 | 124 | return popUpWidgetList; 125 | }); 126 | } 127 | 128 | Widget build(BuildContext context) { 129 | return Scaffold( 130 | appBar: AppBar( 131 | title: Text("Manage Servers"), 132 | ), 133 | floatingActionButton: FloatingActionButton( 134 | onPressed: () { 135 | Navigator.push( 136 | context, 137 | MaterialPageRoute(builder: (context) => AddServerScreen()), 138 | ); 139 | }, 140 | child: Icon( 141 | Icons.add, 142 | color: Colors.black, 143 | ), 144 | backgroundColor: Color(0xFFFFAB00), 145 | ), 146 | body: Row(children: [ 147 | Expanded( 148 | child: SizedBox( 149 | child: StreamBuilder>( 150 | stream: ServerManager().serverListStream, 151 | builder: (context, snapshot) { 152 | final List cServerList = snapshot.data ?? []; 153 | return Container( 154 | decoration: BoxDecoration( 155 | border: Border( 156 | bottom: 157 | BorderSide(color: Color(0xFFbdbdbd)))), 158 | child: ListView.separated( 159 | physics: const AlwaysScrollableScrollPhysics(), 160 | itemCount: cServerList.length, 161 | separatorBuilder: 162 | (BuildContext context, int index) => 163 | Divider(height: 3, color: Colors.white), 164 | itemBuilder: (BuildContext context, int index) { 165 | return Container( 166 | decoration: BoxDecoration( 167 | border: Border( 168 | bottom: BorderSide( 169 | color: Color(0xFFbdbdbd)))), 170 | child: ListTile( 171 | title: Text(cServerList[index].url, 172 | style: TextStyle( 173 | color: Colors.black, 174 | fontSize: 18)), 175 | trailing: generateDropdownMenu( 176 | context, index))); 177 | })); 178 | }))) 179 | ])); 180 | } 181 | } 182 | 183 | class DeleteServerDialog extends StatefulWidget { 184 | final Server cServer; 185 | 186 | DeleteServerDialog({required this.cServer}); 187 | 188 | @override 189 | _DeleteServerDialogState createState() => _DeleteServerDialogState(); 190 | } 191 | 192 | class _DeleteServerDialogState extends State { 193 | bool isRemoveFilesOnServerDeleteSelected = false; 194 | 195 | @override 196 | Widget build(BuildContext context) { 197 | return AlertDialog( 198 | title: Text("Confirm Remove Server"), 199 | content: Row(children: [ 200 | Checkbox( 201 | value: isRemoveFilesOnServerDeleteSelected, 202 | onChanged: (bool? value) { 203 | setState(() { 204 | isRemoveFilesOnServerDeleteSelected = 205 | !isRemoveFilesOnServerDeleteSelected; 206 | }); 207 | }), 208 | Flexible(child: Text("Remove synced files from device?")) 209 | ]), 210 | actions: [ 211 | TextButton( 212 | child: Text("Go Back"), 213 | onPressed: () { 214 | Navigator.of(context).pop(); 215 | }, 216 | ), 217 | TextButton( 218 | child: Text( 219 | "Delete", 220 | style: TextStyle(color: Colors.red), 221 | ), 222 | onPressed: () { 223 | try { 224 | ServerManager().removeServer( 225 | widget.cServer, isRemoveFilesOnServerDeleteSelected); 226 | } catch (err) {} 227 | Navigator.of(context).pop(); 228 | }, 229 | ), 230 | ], 231 | ); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /lib/screens/auto_dj.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:mstream_music/objects/server.dart'; 3 | import '../singletons/server_list.dart'; 4 | import '../singletons/media.dart'; 5 | 6 | class AutoDJScreen extends StatelessWidget { 7 | setAutoDJ(Server? server) { 8 | MediaManager() 9 | .audioHandler 10 | .customAction('setAutoDJ', {'autoDJServer': server}); 11 | } 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | // Handle No Servers 16 | if (ServerManager().serverList.length == 0) { 17 | return Scaffold( 18 | backgroundColor: Color(0xFF3f3f3f), 19 | appBar: AppBar( 20 | title: Text("Auto DJ"), 21 | ), 22 | body: Container( 23 | padding: EdgeInsets.all(40.0), 24 | child: ListView(children: [ 25 | Text('Please add a server'), 26 | ]))); 27 | } 28 | 29 | return Scaffold( 30 | backgroundColor: Color(0xFF3f3f3f), 31 | appBar: AppBar( 32 | title: Text("Auto DJ"), 33 | ), 34 | body: Container( 35 | padding: EdgeInsets.all(40.0), 36 | child: ListView(children: [ 37 | StreamBuilder( 38 | stream: MediaManager().audioHandler.customState, 39 | builder: (context, snapshot) { 40 | final Server? autoDJState = 41 | (snapshot.data?.autoDJState as Server?); 42 | return ElevatedButton( 43 | style: ElevatedButton.styleFrom( 44 | backgroundColor: autoDJState == null 45 | ? Colors.green 46 | : Colors.orange.shade900, 47 | textStyle: const TextStyle(fontSize: 20)), 48 | child: autoDJState == null 49 | ? Text('Enable') 50 | : Text('Disable'), 51 | onPressed: () => { 52 | setAutoDJ(autoDJState == null 53 | ? ServerManager().currentServer 54 | : null) 55 | }); 56 | }), 57 | Container( 58 | height: 20, 59 | ), 60 | if (ServerManager().serverList.length > 1) ...[ 61 | StreamBuilder( 62 | stream: MediaManager().audioHandler.customState, 63 | builder: (context, snapshot) { 64 | final Server? autoDJState = 65 | (snapshot.data?.autoDJState as Server?); 66 | if (autoDJState == null) { 67 | return Container(); 68 | } 69 | 70 | return Text('Auto DJ Server: ${autoDJState.url}', 71 | style: TextStyle(fontWeight: FontWeight.bold)); 72 | }), 73 | StreamBuilder( 74 | stream: MediaManager().audioHandler.customState, 75 | builder: (context, snapshot) { 76 | final Server? autoDJState = 77 | (snapshot.data?.autoDJState as Server?); 78 | 79 | if (autoDJState == null) { 80 | return Container(); 81 | } 82 | 83 | List> lol = []; 84 | 85 | ServerManager().serverList.forEach((value) { 86 | if (value == autoDJState) { 87 | return; 88 | } 89 | 90 | lol.add(DropdownMenuItem( 91 | value: value, 92 | child: Text(value.url.toString()), 93 | )); 94 | }); 95 | 96 | return DropdownButton( 97 | //value: autoDJState, 98 | hint: Text('Change Server'), 99 | items: lol, 100 | onChanged: (newValue) { 101 | setAutoDJ(newValue); 102 | }, 103 | ); 104 | }), 105 | ], 106 | Container( 107 | height: 20, 108 | ), 109 | StreamBuilder( 110 | stream: MediaManager().audioHandler.customState, 111 | builder: (context, snapshot) { 112 | final Server? autoDJState = 113 | (snapshot.data?.autoDJState as Server?); 114 | if (autoDJState == null) { 115 | return Container(); 116 | } 117 | 118 | return Column( 119 | crossAxisAlignment: CrossAxisAlignment.start, 120 | children: [ 121 | Text('Min Rating', 122 | style: TextStyle(fontWeight: FontWeight.bold)), 123 | DropdownButton( 124 | value: autoDJState.autoDJminRating, 125 | items: [ 126 | DropdownMenuItem( 127 | value: null, 128 | child: Text('N/A'), 129 | ), 130 | DropdownMenuItem( 131 | value: 1, 132 | child: Text('0.5'), 133 | ), 134 | DropdownMenuItem( 135 | value: 2, 136 | child: Text('1'), 137 | ), 138 | DropdownMenuItem( 139 | value: 3, 140 | child: Text('1.5'), 141 | ), 142 | DropdownMenuItem( 143 | value: 4, 144 | child: Text('2'), 145 | ), 146 | DropdownMenuItem( 147 | value: 5, 148 | child: Text('2.5'), 149 | ), 150 | DropdownMenuItem( 151 | value: 6, 152 | child: Text('3'), 153 | ), 154 | DropdownMenuItem( 155 | value: 7, 156 | child: Text('3.5'), 157 | ), 158 | DropdownMenuItem( 159 | value: 8, 160 | child: Text('4'), 161 | ), 162 | DropdownMenuItem( 163 | value: 9, 164 | child: Text('4.5'), 165 | ), 166 | DropdownMenuItem( 167 | value: 10, 168 | child: Text('5'), 169 | ), 170 | ], 171 | onChanged: (int? newValue) { 172 | autoDJState.autoDJminRating = newValue; 173 | 174 | MediaManager() 175 | .audioHandler 176 | .customAction('forceAutoDJRefresh'); 177 | 178 | ServerManager().callAfterEditServer(); 179 | 180 | print(ServerManager() 181 | .currentServer 182 | ?.autoDJminRating); 183 | return; 184 | }, 185 | ) 186 | ]); 187 | }), 188 | StreamBuilder( 189 | stream: MediaManager().audioHandler.customState, 190 | builder: (context, snapshot) { 191 | final Server? autoDJState = 192 | (snapshot.data?.autoDJState as Server?); 193 | if (autoDJState == null || 194 | autoDJState.autoDJPaths.length < 2) { 195 | return Container(); 196 | } 197 | 198 | List lol = [ 199 | Container( 200 | height: 20, 201 | ), 202 | Text('Select Folders', 203 | style: TextStyle(fontWeight: FontWeight.bold)) 204 | ]; 205 | 206 | autoDJState.autoDJPaths.forEach((k, v) { 207 | lol.add(ListTile( 208 | leading: Switch( 209 | value: v, 210 | onChanged: (value) { 211 | bool falseFlag = false; 212 | autoDJState.autoDJPaths.forEach((key, value) { 213 | if (key == k) { 214 | return; 215 | } 216 | 217 | if (value == true) { 218 | falseFlag = true; 219 | } 220 | }); 221 | 222 | if (falseFlag == false) { 223 | ScaffoldMessenger.of(context).showSnackBar( 224 | SnackBar( 225 | content: Text( 226 | 'You must have 1 folder selected'))); 227 | return; 228 | } 229 | 230 | autoDJState.autoDJPaths[k] = value; 231 | MediaManager() 232 | .audioHandler 233 | .customAction('forceAutoDJRefresh'); 234 | ServerManager().callAfterEditServer(); 235 | }), 236 | title: Text(k), 237 | )); 238 | }); 239 | 240 | return Column( 241 | crossAxisAlignment: CrossAxisAlignment.start, 242 | children: lol); 243 | }), 244 | ]))); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /lib/media/audio_stuff.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:audio_service/audio_service.dart'; 5 | import 'package:just_audio/just_audio.dart'; 6 | import 'package:mstream_music/main.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | import 'package:uuid/uuid.dart'; 9 | import 'package:http/http.dart' as http; 10 | import '../objects/server.dart'; 11 | 12 | /// An [AudioHandler] for playing a list of podcast episodes. 13 | class AudioPlayerHandler extends BaseAudioHandler 14 | with QueueHandler, SeekHandler { 15 | // ignore: close_sinks 16 | final BehaviorSubject> _recentSubject = 17 | BehaviorSubject>(); 18 | final _player = AudioPlayer(); 19 | final _playlist = ConcatenatingAudioSource(children: []); 20 | 21 | int? get index => _player.currentIndex; 22 | 23 | Server? autoDJServer; 24 | 25 | var jsonAutoDJIgnoreList; 26 | 27 | AudioPlayerHandler() { 28 | _init(); 29 | } 30 | 31 | Future _init() async { 32 | // AudioSession.instance.then((session) { 33 | // session.configure(const AudioSessionConfiguration.music()); 34 | // }); 35 | // final session = await AudioSession.instance; 36 | 37 | // // Handle unplugged headphones. 38 | // session.becomingNoisyEventStream.listen((_) { 39 | // if (_playing) pause(); 40 | // }); 41 | 42 | // For Android 11, record the most recent item so it can be resumed. 43 | mediaItem 44 | .whereType() 45 | .listen((item) => _recentSubject.add([item])); 46 | // Broadcast media item changes. 47 | _player.currentIndexStream.listen((index) { 48 | print(index); 49 | print(queue.value.length); 50 | 51 | if (index == queue.value.length - 1) { 52 | autoDJ(); 53 | } 54 | 55 | if (index != null && queue.value.isNotEmpty) 56 | mediaItem.add(queue.value[index]); 57 | }); 58 | // Magic 59 | _player.durationStream.listen((duration) { 60 | if (index != null && duration != null) { 61 | mediaItem.add(queue.value[index!.toInt()].copyWith(duration: duration)); 62 | } 63 | }); 64 | // Propagate all events from the audio player to AudioService clients. 65 | _player.playbackEventStream.listen(_broadcastState); 66 | // In this example, the service stops when reaching the end. 67 | _player.processingStateStream.listen((state) { 68 | if (state == ProcessingState.completed) stop(); 69 | }); 70 | try { 71 | print("### _player.load"); 72 | // After a cold restart (on Android), _player.load jumps straight from 73 | // the loading state to the completed state. Inserting a delay makes it 74 | // work. Not sure why! 75 | //await Future.delayed(Duration(seconds: 2)); // magic delay 76 | queue.value.forEach((element) { 77 | _playlist.add(AudioSource.uri(Uri.parse(element.id))); 78 | }); 79 | 80 | await _player.setAudioSource(_playlist); 81 | // TODO: We might need this later 82 | // await _player.setAudioSource(ConcatenatingAudioSource( 83 | // children: queue.value! 84 | // .map((item) => AudioSource.uri(Uri.parse(item.id))) 85 | // .toList(), 86 | // )); 87 | print("### loaded"); 88 | } catch (e) { 89 | print("Error: $e"); 90 | } 91 | } 92 | 93 | @override 94 | BehaviorSubject customState = 95 | BehaviorSubject.seeded(CustomEvent(null)); 96 | 97 | @override 98 | Future skipToQueueItem(int index) async { 99 | // Then default implementations of skipToNext and skipToPrevious provided by 100 | // the [QueueHandler] mixin will delegate to this method. 101 | if (index < 0 || index > queue.value.length) return; 102 | // This jumps to the beginning of the queue item at newIndex. 103 | _player.seek(Duration.zero, index: index); 104 | } 105 | 106 | @override 107 | Future addQueueItem(MediaItem item) async { 108 | queue.add(queue.value..add(item)); 109 | 110 | if (item.extras?['localPath'] != null) { 111 | _playlist.add(AudioSource.uri(Uri.parse(item.extras!['localPath']))); 112 | } else { 113 | _playlist.add(AudioSource.uri(Uri.parse(item.id))); 114 | } 115 | } 116 | 117 | @override 118 | Future play() => _player.play(); 119 | 120 | @override 121 | Future pause() => _player.pause(); 122 | 123 | @override 124 | Future skipToNext() async { 125 | _player.seekToNext(); 126 | } 127 | 128 | @override 129 | Future skipToPrevious() async { 130 | _player.seekToPrevious(); 131 | } 132 | 133 | @override 134 | Future seek(Duration position) => _player.seek(position); 135 | 136 | @override 137 | Future stop() async { 138 | await _player.stop(); 139 | await super.stop(); 140 | } 141 | 142 | @override 143 | Future setShuffleMode(AudioServiceShuffleMode doesntMatter) async { 144 | if (_player.shuffleModeEnabled == true) { 145 | _player.setShuffleModeEnabled(false); 146 | await super.setShuffleMode(AudioServiceShuffleMode.none); 147 | } else { 148 | _player.setShuffleModeEnabled(true); 149 | await super.setShuffleMode(AudioServiceShuffleMode.all); 150 | } 151 | 152 | _broadcastState(new PlaybackEvent()); 153 | } 154 | 155 | @override 156 | Future setRepeatMode(AudioServiceRepeatMode doesntMatter) async { 157 | if (_player.loopMode == LoopMode.all) { 158 | _player.setLoopMode(LoopMode.off); 159 | await super.setRepeatMode(AudioServiceRepeatMode.none); 160 | } else { 161 | _player.setLoopMode(LoopMode.all); 162 | await super.setRepeatMode(AudioServiceRepeatMode.all); 163 | } 164 | 165 | _broadcastState(new PlaybackEvent()); 166 | } 167 | 168 | @override 169 | Future removeQueueItem(MediaItem i) async { 170 | await super.removeQueueItem(i); 171 | // TODO: See removeQueueItemAt 172 | } 173 | 174 | @override 175 | Future removeQueueItemAt(int i) async { 176 | await super.removeQueueItemAt(i); 177 | await _playlist.removeAt(i); 178 | queue.add(queue.value..removeAt(i)); 179 | } 180 | 181 | customAction(String name, [Map? extras]) async { 182 | switch (name) { 183 | case 'clearPlaylist': 184 | await _player.stop(); 185 | await super.stop(); 186 | await _playlist.clear(); 187 | queue.add(queue.value..clear()); 188 | _broadcastState(new PlaybackEvent()); 189 | break; 190 | case 'forceAutoDJRefresh': 191 | customState.add(CustomEvent(autoDJServer)); 192 | break; 193 | case 'setAutoDJ': 194 | if (autoDJServer == null || autoDJServer != extras?['autoDJServer']) { 195 | jsonAutoDJIgnoreList = null; 196 | } 197 | autoDJServer = extras?['autoDJServer']; 198 | 199 | customState.add(CustomEvent(autoDJServer)); 200 | 201 | if (queue.value.length == 0 || 202 | queue.value.length == 1 || 203 | index == queue.value.length - 1) { 204 | if (queue.value.length == 0) { 205 | await autoDJ(autoPlay: true); 206 | autoDJ(); 207 | } else if (index == queue.value.length - 1 && 208 | _player.processingState == ProcessingState.idle) { 209 | autoDJ(autoPlay: true, incrementIndex: true); 210 | } else { 211 | autoDJ(); 212 | } 213 | } 214 | 215 | break; 216 | } 217 | } 218 | 219 | /// Broadcasts the current state to all clients. 220 | void _broadcastState(PlaybackEvent event) { 221 | final playing = _player.playing; 222 | final AudioServiceShuffleMode shuffle = _player.shuffleModeEnabled == true 223 | ? AudioServiceShuffleMode.all 224 | : AudioServiceShuffleMode.none; 225 | 226 | final AudioServiceRepeatMode repeat = _player.loopMode == LoopMode.all 227 | ? AudioServiceRepeatMode.all 228 | : AudioServiceRepeatMode.none; 229 | 230 | playbackState.add(playbackState.value.copyWith( 231 | controls: [ 232 | MediaControl.skipToPrevious, 233 | if (playing) MediaControl.pause else MediaControl.play, 234 | MediaControl.stop, 235 | MediaControl.skipToNext, 236 | ], 237 | systemActions: { 238 | MediaAction.seek, 239 | MediaAction.seekForward, 240 | MediaAction.seekBackward, 241 | }, 242 | shuffleMode: shuffle, 243 | repeatMode: repeat, 244 | androidCompactActionIndices: [0, 1, 3], 245 | processingState: { 246 | ProcessingState.idle: AudioProcessingState.idle, 247 | ProcessingState.loading: AudioProcessingState.loading, 248 | ProcessingState.buffering: AudioProcessingState.buffering, 249 | ProcessingState.ready: AudioProcessingState.ready, 250 | ProcessingState.completed: AudioProcessingState.completed, 251 | }[_player.processingState]!, 252 | playing: playing, 253 | updatePosition: _player.position, 254 | bufferedPosition: _player.bufferedPosition, 255 | speed: _player.speed, 256 | queueIndex: event.currentIndex, 257 | )); 258 | } 259 | 260 | Future autoDJ( 261 | {bool autoPlay = false, bool incrementIndex = false}) async { 262 | if (autoDJServer == null) { 263 | return; 264 | } 265 | 266 | try { 267 | Uri currentUri = Uri.parse(autoDJServer!.url.toString()) 268 | .resolve('/api/v1/db/random-songs'); 269 | 270 | bool flagIt = false; 271 | String ignoreVPathString = '['; 272 | autoDJServer?.autoDJPaths.forEach((key, value) { 273 | if (value == false) { 274 | ignoreVPathString += '${flagIt == false ? '' : ','} "$key"'; 275 | flagIt = true; 276 | } 277 | }); 278 | ignoreVPathString += '],'; 279 | 280 | print(ignoreVPathString); 281 | 282 | String payload = '''{"minRating":${autoDJServer?.autoDJminRating}, 283 | ${flagIt == true ? '"ignoreVPaths": $ignoreVPathString' : ''} 284 | "ignoreList":${json.encode(jsonAutoDJIgnoreList)}}'''; 285 | 286 | var res = await http.post(currentUri, 287 | headers: { 288 | 'Content-Type': 'application/json', 289 | 'x-access-token': autoDJServer?.jwt ?? '' 290 | }, 291 | body: payload); 292 | 293 | var decoded = jsonDecode(res.body); 294 | 295 | String p = ''; 296 | decoded['songs'][0]['filepath'].split("/").forEach((element) { 297 | if (element.length == 0) { 298 | return; 299 | } 300 | p += "/" + Uri.encodeComponent(element); 301 | }); 302 | 303 | String lolUrl = autoDJServer!.url.toString() + 304 | '/media' + 305 | p + 306 | '?app_uuid=' + 307 | Uuid().v4() + 308 | (autoDJServer?.jwt == null ? '' : '&token=' + autoDJServer!.jwt!); 309 | 310 | MediaItem item = new MediaItem( 311 | id: lolUrl, 312 | title: decoded['songs'][0]['metadata']['title'] ?? 313 | decoded['songs'][0]['filepath'].split("/").removeLast(), 314 | album: decoded['songs'][0]['metadata']['album'], 315 | artist: decoded['songs'][0]['metadata']['artist'], 316 | artUri: decoded['songs'][0]['metadata']['album-art'] != null 317 | ? Uri.parse(autoDJServer!.url.toString()).resolve('/album-art/' + 318 | decoded['songs'][0]['metadata']['album-art'] + 319 | '?compress=l&token=' + 320 | (autoDJServer?.jwt ?? '')) 321 | : Uri.parse(autoDJServer!.url.toString()) 322 | .resolve('/assets/img/default.png'), 323 | extras: { 324 | 'path': decoded['songs'][0]['filepath'], 325 | 'year': decoded['songs'][0]['year'] 326 | }); 327 | 328 | jsonAutoDJIgnoreList = decoded['ignoreList']; 329 | 330 | addQueueItem(item); 331 | 332 | if (incrementIndex == true) { 333 | _player.seek(Duration.zero, index: index! + 1); 334 | } 335 | if (autoPlay == true) { 336 | play(); 337 | } 338 | } catch (err) {} 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /lib/screens/add_server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:http/http.dart' as http; 5 | import 'package:uuid/uuid.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; 8 | import '../objects/server.dart'; 9 | import '../singletons/server_list.dart'; 10 | 11 | class AddServerScreen extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text("Add Server"), 17 | ), 18 | body: MyCustomForm()); 19 | } 20 | } 21 | 22 | class EditServerScreen extends StatelessWidget { 23 | final int editThisServer; 24 | const EditServerScreen({Key? key, required this.editThisServer}) 25 | : super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: Text("Edit Server"), 32 | ), 33 | body: MyCustomForm(editThisServer: editThisServer)); 34 | } 35 | } 36 | 37 | class MyCustomForm extends StatefulWidget { 38 | final int? editThisServer; 39 | const MyCustomForm({Key? key, this.editThisServer}) : super(key: key); 40 | 41 | @override 42 | MyCustomFormState createState() { 43 | return MyCustomFormState(editThisServer: editThisServer); 44 | } 45 | } 46 | 47 | // Create a corresponding State class. This class will hold the data related to 48 | // the form. 49 | class MyCustomFormState extends State { 50 | // Create a global key that will uniquely identify the Form widget and allow 51 | // us to validate the form 52 | // Note: This is a GlobalKey, not a GlobalKey! 53 | final _formKey = GlobalKey(); 54 | 55 | TextEditingController _urlCtrl = TextEditingController(); 56 | TextEditingController _usernameCtrl = TextEditingController(); 57 | TextEditingController _passwordCtrl = TextEditingController(); 58 | 59 | bool submitPending = false; 60 | bool saveToSdCard = false; 61 | 62 | final int? editThisServer; 63 | MyCustomFormState({this.editThisServer}) : super(); 64 | 65 | @protected 66 | @mustCallSuper 67 | void initState() { 68 | super.initState(); 69 | 70 | try { 71 | Server s = ServerManager().serverList[editThisServer ?? -1]; 72 | _urlCtrl.text = s.url; 73 | _usernameCtrl.text = s.username ?? ''; 74 | _passwordCtrl.text = s.password ?? ''; 75 | saveToSdCard = s.saveToSdCard; 76 | } catch (err) {} 77 | } 78 | 79 | @override 80 | void dispose() { 81 | _urlCtrl.dispose(); 82 | _usernameCtrl.dispose(); 83 | _passwordCtrl.dispose(); 84 | 85 | super.dispose(); 86 | } 87 | 88 | checkServer() async { 89 | setState(() { 90 | submitPending = true; 91 | }); 92 | Uri lol = Uri.parse(this._urlCtrl.text); 93 | var response; 94 | 95 | try { 96 | // Do a quick check on /ping to see if this server even needs authentication 97 | response = await http 98 | .get(lol.resolve('/api/v1/ping')) 99 | .timeout(Duration(seconds: 5)); 100 | 101 | if (response.statusCode == 200) { 102 | // setState(() { 103 | // submitPending = false; 104 | // }); 105 | ScaffoldMessenger.of(context) 106 | .showSnackBar(SnackBar(content: Text('Connection Successful!'))); 107 | saveServer(lol); 108 | return; 109 | } 110 | } catch (err) {} 111 | 112 | // Try logging in 113 | try { 114 | response = await http.post(lol.resolve('/api/v1/auth/login'), body: { 115 | "username": this._usernameCtrl.text, 116 | "password": this._passwordCtrl.text 117 | }).timeout(Duration(seconds: 6)); 118 | 119 | if (response.statusCode != 200) { 120 | throw Exception('Failed to connect to server'); 121 | } 122 | 123 | var res = jsonDecode(response.body); 124 | 125 | // Save 126 | saveServer(lol, res['token']); 127 | } catch (err) { 128 | print(err); 129 | try { 130 | setState(() { 131 | submitPending = false; 132 | }); 133 | } catch (e) {} 134 | 135 | ScaffoldMessenger.of(context) 136 | .showSnackBar(SnackBar(content: Text('Failed to Login'))); 137 | return; 138 | } 139 | } 140 | 141 | Future saveServer(Uri lol, [String jwt = '']) async { 142 | bool shouldUpdate = false; 143 | try { 144 | ServerManager().serverList[editThisServer ?? -1]; 145 | shouldUpdate = true; 146 | } catch (err) {} 147 | 148 | if (shouldUpdate) { 149 | ServerManager().editServer(editThisServer!, _urlCtrl.text, 150 | _usernameCtrl.text, _passwordCtrl.text, saveToSdCard); 151 | await ServerManager() 152 | .getServerPaths(ServerManager().serverList[editThisServer!]); 153 | await ServerManager().callAfterEditServer(); 154 | } else { 155 | Server newServer = new Server(lol.origin, this._usernameCtrl.text, 156 | this._passwordCtrl.text, jwt, Uuid().v4()); 157 | if (saveToSdCard == true) { 158 | newServer.saveToSdCard = true; 159 | } 160 | await ServerManager().getServerPaths(newServer); 161 | 162 | await ServerManager().addServer(newServer); 163 | } 164 | 165 | // Save Server List 166 | Navigator.pop(context); 167 | } 168 | 169 | Map parseQrCode(String qrValue) { 170 | if (qrValue[0] != '|') { 171 | throw new Error(); 172 | } 173 | 174 | List explodeArr = qrValue.split("|"); 175 | if (explodeArr.length < 4) { 176 | throw new Error(); 177 | } 178 | 179 | return { 180 | 'url': explodeArr[1], 181 | 'username': explodeArr[2], 182 | 'password': explodeArr[3], 183 | }; 184 | } 185 | 186 | @override 187 | Widget build(BuildContext context) { 188 | // Build a Form widget using the _formKey we created above 189 | return Container( 190 | color: Color(0xFF3f3f3f), 191 | padding: EdgeInsets.all(20.0), 192 | child: Form( 193 | key: _formKey, 194 | child: Column( 195 | crossAxisAlignment: CrossAxisAlignment.start, 196 | children: [ 197 | TextFormField( 198 | controller: _urlCtrl, 199 | validator: (value) { 200 | if (value!.isEmpty) { 201 | return 'Server URL is needed'; 202 | } 203 | try { 204 | var lol = Uri.parse(value); 205 | if (lol.origin is Error || lol.origin.length < 1) { 206 | return 'Cannot Parse URL'; 207 | } 208 | } catch (err) { 209 | return 'Cannot Parse URL'; 210 | } 211 | return null; 212 | }, 213 | keyboardType: TextInputType.emailAddress, 214 | decoration: InputDecoration( 215 | hintText: 'https://mstream.io', 216 | labelText: 'Server URL', 217 | ), 218 | onSaved: (String? value) { 219 | this._urlCtrl.text = value ?? ''; 220 | }), 221 | Container( 222 | width: MediaQuery.of(context).size.width, 223 | child: Row( 224 | mainAxisAlignment: MainAxisAlignment.spaceAround, 225 | children: [ 226 | Expanded( 227 | child: TextFormField( 228 | controller: _usernameCtrl, 229 | validator: (value) { 230 | return null; 231 | }, 232 | keyboardType: TextInputType.emailAddress, 233 | decoration: InputDecoration( 234 | hintText: 'Username', labelText: 'Username'), 235 | onSaved: (String? value) { 236 | this._usernameCtrl.text = value!; 237 | })), 238 | Container(width: 8), // Make a gap between the buttons 239 | Expanded( 240 | child: TextFormField( 241 | controller: _passwordCtrl, 242 | validator: (value) { 243 | return null; 244 | }, 245 | obscureText: 246 | true, // Use secure text for passwords. 247 | decoration: InputDecoration( 248 | hintText: 'Password', labelText: 'Password'), 249 | onSaved: (String? value) { 250 | this._passwordCtrl.text = value!; 251 | })), 252 | ])), 253 | Container( 254 | height: 20, 255 | ), 256 | // if (this.editThisServer == null) ...[ 257 | Row( 258 | children: [ 259 | Switch( 260 | value: this.saveToSdCard, 261 | onChanged: (value) { 262 | setState(() { 263 | this.saveToSdCard = value; 264 | }); 265 | }, 266 | ), 267 | Text('Download to SD Card') 268 | ], 269 | ), 270 | // ], 271 | Container( 272 | width: MediaQuery.of(context).size.width, 273 | child: Row( 274 | mainAxisAlignment: MainAxisAlignment.spaceAround, 275 | children: [ 276 | Expanded( 277 | child: ElevatedButton( 278 | style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), 279 | child: Row( 280 | mainAxisAlignment: MainAxisAlignment.center, 281 | children: [ 282 | Icon(Icons.photo_camera, color: Colors.white), 283 | Container(width: 8), 284 | Text('QR Code', 285 | style: TextStyle(color: Colors.white)), 286 | ]), 287 | onPressed: submitPending 288 | ? null 289 | : () { 290 | FlutterBarcodeScanner.scanBarcode( 291 | '#ff6666', 'Cancel', false, ScanMode.QR) 292 | .then((qrValue) { 293 | if (qrValue == '-1' || qrValue == '') { 294 | return; 295 | } 296 | 297 | try { 298 | Map parsedValues = 299 | parseQrCode(qrValue); 300 | _urlCtrl.text = parsedValues['url'] ?? ''; 301 | _usernameCtrl.text = 302 | parsedValues['username'] ?? ''; 303 | _passwordCtrl.text = 304 | parsedValues['password'] ?? ''; 305 | } catch (err) { 306 | // Scaffold.of(context).showSnackBar(SnackBar( 307 | // content: Text('Invalid Code'))); 308 | } 309 | }); 310 | }, 311 | ), 312 | ), 313 | Container(width: 8), // Make a gap between the buttons 314 | Expanded( 315 | child: ElevatedButton( 316 | style: ElevatedButton.styleFrom(backgroundColor: Colors.green), 317 | child: Text(submitPending ? 'Checking Server' : 'Save', 318 | style: TextStyle(color: Colors.white)), 319 | onPressed: submitPending 320 | ? null 321 | : () { 322 | // Validate will return true if the form is valid, or false if 323 | // the form is invalid. 324 | if (!_formKey.currentState!.validate()) { 325 | return; 326 | } 327 | 328 | _formKey.currentState! 329 | .save(); // Save our form now. 330 | 331 | // Ping server 332 | checkServer(); 333 | }, 334 | ), 335 | ), 336 | ]), 337 | margin: EdgeInsets.only(top: 20.0), 338 | ), 339 | ], 340 | ), 341 | ), 342 | ); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /lib/singletons/api.dart: -------------------------------------------------------------------------------- 1 | import './server_list.dart'; 2 | import './browser_list.dart'; 3 | import '../objects/server.dart'; 4 | import '../objects/display_item.dart'; 5 | import '../objects/metadata.dart'; 6 | import 'media.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:path/path.dart' as path; 9 | import 'package:uuid/uuid.dart'; 10 | import 'package:audio_service/audio_service.dart'; 11 | 12 | import 'package:http/http.dart' as http; 13 | import 'dart:convert'; 14 | 15 | class ApiManager { 16 | ApiManager._privateConstructor(); 17 | static final ApiManager _instance = ApiManager._privateConstructor(); 18 | factory ApiManager() { 19 | return _instance; 20 | } 21 | 22 | Future makeServerCall(Server? currentServer, String location, Map payload, 23 | String getOrPost) async { 24 | Server server = ServerManager().currentServer ?? 25 | (throw Exception('No Server Selected')); 26 | 27 | Uri currentUri = Uri.parse(server.url).resolve(location); 28 | 29 | var response; 30 | if (getOrPost == 'GET') { 31 | response = await http 32 | .get(currentUri, headers: {'x-access-token': server.jwt ?? ''}); 33 | } else { 34 | response = await http.post(currentUri, 35 | body: json.encode(payload), 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | 'x-access-token': server.jwt ?? '' 39 | }); 40 | } 41 | 42 | if (response.statusCode > 299) { 43 | throw Exception('Server Call Failed'); 44 | } 45 | 46 | return jsonDecode(response.body); 47 | } 48 | 49 | Future getRecursiveFiles(String directory, 50 | {Server? useThisServer}) async { 51 | var res; 52 | try { 53 | res = await makeServerCall(useThisServer, 54 | '/api/v1/file-explorer/recursive', {"directory": directory}, 'POST'); 55 | } catch (err) { 56 | // TODO: Handle Errors 57 | print(err); 58 | return; 59 | } 60 | 61 | // String prefix = 62 | // TranscodeManager().transcodeOn == true ? '/transcode' : '/media'; 63 | 64 | res.forEach((e) { 65 | String lolUrl = Uri.encodeFull(useThisServer!.url + 66 | '/media' + 67 | (e.toString()[0] != '/' ? '/' : '') + 68 | e + 69 | '?app_uuid=' + 70 | Uuid().v4() + 71 | (useThisServer.jwt == null ? '' : '&token=' + useThisServer.jwt!)); 72 | 73 | MediaItem lol = new MediaItem( 74 | id: lolUrl, 75 | title: e.split("/").last, 76 | extras: {'server': useThisServer.localname, 'path': e}); 77 | MediaManager().audioHandler.addQueueItem(lol); 78 | }); 79 | } 80 | 81 | Future getPlaylists({Server? useThisServer}) async { 82 | var res; 83 | try { 84 | res = await makeServerCall( 85 | useThisServer, '/api/v1/playlist/getall', {}, 'GET'); 86 | } catch (err) { 87 | // TODO: Handle Errors 88 | print(err); 89 | return; 90 | } 91 | 92 | BrowserManager().setBrowserLabel('Playlists'); 93 | 94 | List newList = []; 95 | res.forEach((e) { 96 | DisplayItem newItem = new DisplayItem( 97 | useThisServer, 98 | e['name'], 99 | 'playlist', 100 | e['name'], 101 | Icon(Icons.queue_music, color: Colors.black), 102 | null); 103 | newList.add(newItem); 104 | }); 105 | 106 | BrowserManager().addListToStack(newList); 107 | } 108 | 109 | Future removePlaylist(String playlistId, 110 | {Server? useThisServer}) async { 111 | var res; 112 | try { 113 | res = await makeServerCall(useThisServer, '/api/v1/playlist/delete', 114 | {'playlistname': playlistId}, 'POST'); 115 | } catch (err) { 116 | // TODO: Handle Errors 117 | print(err); 118 | return; 119 | } 120 | 121 | BrowserManager().removeAll(playlistId, useThisServer!, 'playlist'); 122 | } 123 | 124 | Future searchServer(String search) async { 125 | try { 126 | var res = await makeServerCall(null, '/api/v1/db/search', 127 | {'noFiles': true, 'search': search}, 'POST'); 128 | 129 | BrowserManager().setBrowserLabel('Search'); 130 | List newList = []; 131 | res['artists'].forEach((e) { 132 | DisplayItem newItem = new DisplayItem( 133 | ServerManager().currentServer, 134 | e['name'], 135 | 'artist', 136 | e['name'], 137 | Icon(Icons.library_music, color: Colors.black), 138 | 'artist'); 139 | newItem.altAlbumArt = e['album_art_file']; 140 | newList.add(newItem); 141 | }); 142 | 143 | res['albums'].forEach((e) { 144 | DisplayItem newItem = new DisplayItem( 145 | ServerManager().currentServer, 146 | e['name'], 147 | 'album', 148 | e['name'], 149 | Icon(Icons.library_music, color: Colors.black), 150 | 'album'); 151 | newItem.altAlbumArt = e['album_art_file']; 152 | newList.add(newItem); 153 | }); 154 | 155 | res['title'].forEach((e) { 156 | DisplayItem newItem = new DisplayItem( 157 | ServerManager().currentServer, 158 | e['name'], 159 | 'file', 160 | '/' + e['filepath'], 161 | Icon(Icons.music_note, color: Colors.blue), 162 | 'song'); 163 | newItem.altAlbumArt = e['album_art_file']; 164 | newList.add(newItem); 165 | }); 166 | 167 | BrowserManager().addListToStack(newList); 168 | } catch (err) { 169 | print(err); 170 | } 171 | } 172 | 173 | Future getAlbums({Server? useThisServer}) async { 174 | var res; 175 | try { 176 | res = await makeServerCall(useThisServer, '/api/v1/db/albums', {}, 'GET'); 177 | } catch (err) { 178 | // TODO: Handle Errors 179 | print(err); 180 | return; 181 | } 182 | 183 | BrowserManager().setBrowserLabel('Albums'); 184 | 185 | List newList = []; 186 | res['albums'].forEach((e) { 187 | DisplayItem newItem = new DisplayItem( 188 | useThisServer, 189 | e['name'], 190 | 'album', 191 | e['name'], 192 | Icon(Icons.album, color: Colors.black), 193 | e['year']?.toString() ?? ''); 194 | newItem.altAlbumArt = e['album_art_file']; 195 | newList.add(newItem); 196 | }); 197 | 198 | BrowserManager().addListToStack(newList); 199 | } 200 | 201 | Future getAlbumSongs(String? album, {Server? useThisServer}) async { 202 | var res; 203 | try { 204 | res = await makeServerCall( 205 | useThisServer, '/api/v1/db/album-songs', {'album': album}, 'POST'); 206 | } catch (err) { 207 | // TODO: Handle Errors 208 | print(err); 209 | return; 210 | } 211 | 212 | List newList = []; 213 | res.forEach((e) { 214 | MusicMetadata m = new MusicMetadata( 215 | e['metadata']['artist'], 216 | e['metadata']['album'], 217 | e['metadata']['title'], 218 | e['metadata']['track'], 219 | e['metadata']['disc'], 220 | e['metadata']['year'], 221 | e['metadata']['hash'], 222 | e['metadata']['rating'], 223 | e['metadata']['album-art']); 224 | 225 | DisplayItem newItem = new DisplayItem( 226 | useThisServer, 227 | e['filepath'], 228 | 'file', 229 | '/' + e['filepath'], 230 | Icon(Icons.music_note, color: Colors.blue), 231 | null); 232 | 233 | newItem.metadata = m; 234 | 235 | newList.add(newItem); 236 | }); 237 | 238 | BrowserManager().addListToStack(newList); 239 | } 240 | 241 | Future getRecentlyAdded({Server? useThisServer}) async { 242 | var res; 243 | try { 244 | res = await makeServerCall( 245 | useThisServer, '/api/v1/db/recent/added', {'limit': 100}, 'POST'); 246 | } catch (err) { 247 | // TODO: Handle Errors 248 | print(err); 249 | return; 250 | } 251 | 252 | BrowserManager().setBrowserLabel('Recent'); 253 | 254 | List newList = []; 255 | res.forEach((e) { 256 | MusicMetadata m = new MusicMetadata( 257 | e['metadata']['artist'], 258 | e['metadata']['album'], 259 | e['metadata']['title'], 260 | e['metadata']['track'], 261 | e['metadata']['disc'], 262 | e['metadata']['year'], 263 | e['metadata']['hash'], 264 | e['metadata']['rating'], 265 | e['metadata']['album-art']); 266 | 267 | DisplayItem newItem = new DisplayItem( 268 | useThisServer, 269 | e['filepath'], 270 | 'file', 271 | '/' + e['filepath'], 272 | Icon(Icons.music_note, color: Colors.blue), 273 | null); 274 | 275 | newItem.metadata = m; 276 | 277 | newList.add(newItem); 278 | }); 279 | BrowserManager().addListToStack(newList); 280 | } 281 | 282 | Future getRated({Server? useThisServer}) async { 283 | var res; 284 | try { 285 | res = await makeServerCall(useThisServer, '/api/v1/db/rated', {}, 'GET'); 286 | } catch (err) { 287 | // TODO: Handle Errors 288 | print(err); 289 | return; 290 | } 291 | 292 | BrowserManager().setBrowserLabel('Rated'); 293 | 294 | List newList = []; 295 | res.forEach((e) { 296 | MusicMetadata m = new MusicMetadata( 297 | e['metadata']['artist'], 298 | e['metadata']['album'], 299 | e['metadata']['title'], 300 | e['metadata']['track'], 301 | e['metadata']['disc'], 302 | e['metadata']['year'], 303 | e['metadata']['hash'], 304 | e['metadata']['rating'], 305 | e['metadata']['album-art']); 306 | 307 | DisplayItem newItem = new DisplayItem( 308 | useThisServer, 309 | e['filepath'], 310 | 'file', 311 | '/' + e['filepath'], 312 | Icon(Icons.music_note, color: Colors.blue), 313 | m.artist); 314 | 315 | newItem.metadata = m; 316 | newItem.showRating = true; 317 | 318 | newList.add(newItem); 319 | }); 320 | BrowserManager().addListToStack(newList); 321 | } 322 | 323 | Future getArtists({Server? useThisServer}) async { 324 | var res; 325 | try { 326 | res = 327 | await makeServerCall(useThisServer, '/api/v1/db/artists', {}, 'GET'); 328 | } catch (err) { 329 | // TODO: Handle Errors 330 | print(err); 331 | return; 332 | } 333 | 334 | BrowserManager().setBrowserLabel('Artists'); 335 | 336 | List newList = []; 337 | res['artists'].forEach((e) { 338 | DisplayItem newItem = new DisplayItem(useThisServer, e, 'artist', e, 339 | Icon(Icons.library_music, color: Colors.black), null); 340 | newList.add(newItem); 341 | }); 342 | BrowserManager().addListToStack(newList); 343 | } 344 | 345 | Future getArtistAlbums(String artist, {Server? useThisServer}) async { 346 | var res; 347 | try { 348 | res = await makeServerCall(useThisServer, '/api/v1/db/artists-albums', 349 | {'artist': artist}, 'POST'); 350 | } catch (err) { 351 | // TODO: Handle Errors 352 | print(err); 353 | return; 354 | } 355 | 356 | List newList = []; 357 | res['albums'].forEach((e) { 358 | String name = e['name'] ?? 'SINGLES'; 359 | 360 | // TODO: Errors on singles 361 | DisplayItem newItem = new DisplayItem( 362 | useThisServer, 363 | name, 364 | 'album', 365 | e['name'], 366 | Icon(Icons.album, color: Colors.black), 367 | e['year']?.toString() ?? ''); 368 | newItem.altAlbumArt = e['album_art_file']; 369 | 370 | newList.add(newItem); 371 | }); 372 | 373 | BrowserManager().addListToStack(newList); 374 | } 375 | 376 | Future getPlaylistContents(String playlistName, 377 | {Server? useThisServer}) async { 378 | var res; 379 | try { 380 | res = await makeServerCall(useThisServer, '/api/v1/playlist/load', 381 | {'playlistname': playlistName}, 'POST'); 382 | } catch (err) { 383 | // TODO: Handle Errors 384 | print(err); 385 | return; 386 | } 387 | 388 | List newList = []; 389 | res.forEach((e) { 390 | MusicMetadata m = new MusicMetadata( 391 | e['metadata']['artist'], 392 | e['metadata']['album'], 393 | e['metadata']['title'], 394 | e['metadata']['track'], 395 | e['metadata']['disc'], 396 | e['metadata']['year'], 397 | e['metadata']['hash'], 398 | e['metadata']['rating'], 399 | e['metadata']['album-art']); 400 | 401 | DisplayItem newItem = new DisplayItem( 402 | useThisServer, 403 | e['filepath'], 404 | 'file', 405 | '/' + e['filepath'], 406 | Icon(Icons.music_note, color: Colors.blue), 407 | null); 408 | 409 | newItem.metadata = m; 410 | newList.add(newItem); 411 | }); 412 | 413 | BrowserManager().addListToStack(newList); 414 | } 415 | 416 | Future getFileList(String directory, {Server? useThisServer}) async { 417 | var res; 418 | try { 419 | res = await makeServerCall(useThisServer, '/api/v1/file-explorer', 420 | {"directory": directory}, 'POST'); 421 | } catch (err) { 422 | // TODO: Handle Errors 423 | print(err); 424 | return; 425 | } 426 | 427 | BrowserManager().setBrowserLabel('File Explorer'); 428 | 429 | List newList = []; 430 | res['directories'].forEach((e) { 431 | DisplayItem newItem = new DisplayItem( 432 | useThisServer, 433 | e['name'], 434 | 'directory', 435 | path.join(res['path'], e['name']), 436 | Icon(Icons.folder, color: Color(0xFFffab00)), 437 | null); 438 | newList.add(newItem); 439 | }); 440 | 441 | res['files'].forEach((e) { 442 | DisplayItem newItem = new DisplayItem( 443 | useThisServer, 444 | e['name'], 445 | 'file', 446 | path.join(res['path'], e['name']), 447 | Icon(Icons.music_note, color: Colors.blue), 448 | null); 449 | newList.add(newItem); 450 | }); 451 | 452 | BrowserManager().addListToStack(newList); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "3.6.1" 12 | args: 13 | dependency: transitive 14 | description: 15 | name: args 16 | sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.5.0" 20 | async: 21 | dependency: transitive 22 | description: 23 | name: async 24 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.11.0" 28 | audio_service: 29 | dependency: "direct main" 30 | description: 31 | name: audio_service 32 | sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "0.18.15" 36 | audio_service_platform_interface: 37 | dependency: transitive 38 | description: 39 | name: audio_service_platform_interface 40 | sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "0.1.1" 44 | audio_service_web: 45 | dependency: transitive 46 | description: 47 | name: audio_service_web 48 | sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "0.1.3" 52 | audio_session: 53 | dependency: "direct main" 54 | description: 55 | name: audio_session 56 | sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "0.1.21" 60 | boolean_selector: 61 | dependency: transitive 62 | description: 63 | name: boolean_selector 64 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.1.1" 68 | characters: 69 | dependency: transitive 70 | description: 71 | name: characters 72 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.0" 76 | checked_yaml: 77 | dependency: transitive 78 | description: 79 | name: checked_yaml 80 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.0.3" 84 | cli_util: 85 | dependency: transitive 86 | description: 87 | name: cli_util 88 | sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "0.4.1" 92 | clock: 93 | dependency: transitive 94 | description: 95 | name: clock 96 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.1.1" 100 | collection: 101 | dependency: transitive 102 | description: 103 | name: collection 104 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.18.0" 108 | crypto: 109 | dependency: transitive 110 | description: 111 | name: crypto 112 | sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.0.5" 116 | cupertino_icons: 117 | dependency: "direct main" 118 | description: 119 | name: cupertino_icons 120 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.0.8" 124 | disk_space: 125 | dependency: "direct main" 126 | description: 127 | name: disk_space 128 | sha256: fb27eb2d09ac04784f45b95b1355538b2355c76a081eeaa8439d1a5cfa263888 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.2.1" 132 | fake_async: 133 | dependency: transitive 134 | description: 135 | name: fake_async 136 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.3.1" 140 | ffi: 141 | dependency: transitive 142 | description: 143 | name: ffi 144 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "2.1.3" 148 | file: 149 | dependency: transitive 150 | description: 151 | name: file 152 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "7.0.0" 156 | fixnum: 157 | dependency: transitive 158 | description: 159 | name: fixnum 160 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.1.0" 164 | flutter: 165 | dependency: "direct main" 166 | description: flutter 167 | source: sdk 168 | version: "0.0.0" 169 | flutter_barcode_scanner: 170 | dependency: "direct main" 171 | description: 172 | name: flutter_barcode_scanner 173 | sha256: a4ba37daf9933f451a5e812c753ddd045d6354e4a3280342d895b07fecaab3fa 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "2.0.0" 177 | flutter_cache_manager: 178 | dependency: transitive 179 | description: 180 | name: flutter_cache_manager 181 | sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "3.4.1" 185 | flutter_downloader: 186 | dependency: "direct main" 187 | description: 188 | name: flutter_downloader 189 | sha256: b6da5495b6258aa7c243d0f0a5281e3430b385bccac11cc508f981e653b25aa6 190 | url: "https://pub.dev" 191 | source: hosted 192 | version: "1.11.8" 193 | flutter_launcher_icons: 194 | dependency: "direct main" 195 | description: 196 | name: flutter_launcher_icons 197 | sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" 198 | url: "https://pub.dev" 199 | source: hosted 200 | version: "0.13.1" 201 | flutter_plugin_android_lifecycle: 202 | dependency: transitive 203 | description: 204 | name: flutter_plugin_android_lifecycle 205 | sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" 206 | url: "https://pub.dev" 207 | source: hosted 208 | version: "2.0.22" 209 | flutter_slidable: 210 | dependency: "direct main" 211 | description: 212 | name: flutter_slidable 213 | sha256: c7607eb808cdef19c8468246e95a133308aeaeb3971cdd9edfb9d5e31cedfbe9 214 | url: "https://pub.dev" 215 | source: hosted 216 | version: "0.6.0" 217 | flutter_test: 218 | dependency: "direct dev" 219 | description: flutter 220 | source: sdk 221 | version: "0.0.0" 222 | flutter_web_plugins: 223 | dependency: transitive 224 | description: flutter 225 | source: sdk 226 | version: "0.0.0" 227 | http: 228 | dependency: "direct main" 229 | description: 230 | name: http 231 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "1.2.2" 235 | http_parser: 236 | dependency: transitive 237 | description: 238 | name: http_parser 239 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "4.0.2" 243 | image: 244 | dependency: transitive 245 | description: 246 | name: image 247 | sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "4.2.0" 251 | js: 252 | dependency: transitive 253 | description: 254 | name: js 255 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "0.7.1" 259 | json_annotation: 260 | dependency: transitive 261 | description: 262 | name: json_annotation 263 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "4.9.0" 267 | just_audio: 268 | dependency: "direct main" 269 | description: 270 | name: just_audio 271 | sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "0.9.40" 275 | just_audio_platform_interface: 276 | dependency: transitive 277 | description: 278 | name: just_audio_platform_interface 279 | sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "4.3.0" 283 | just_audio_web: 284 | dependency: transitive 285 | description: 286 | name: just_audio_web 287 | sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "0.4.12" 291 | leak_tracker: 292 | dependency: transitive 293 | description: 294 | name: leak_tracker 295 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "10.0.5" 299 | leak_tracker_flutter_testing: 300 | dependency: transitive 301 | description: 302 | name: leak_tracker_flutter_testing 303 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "3.0.5" 307 | leak_tracker_testing: 308 | dependency: transitive 309 | description: 310 | name: leak_tracker_testing 311 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "3.0.1" 315 | matcher: 316 | dependency: transitive 317 | description: 318 | name: matcher 319 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "0.12.16+1" 323 | material_color_utilities: 324 | dependency: transitive 325 | description: 326 | name: material_color_utilities 327 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "0.11.1" 331 | meta: 332 | dependency: transitive 333 | description: 334 | name: meta 335 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "1.15.0" 339 | path: 340 | dependency: "direct main" 341 | description: 342 | name: path 343 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "1.9.0" 347 | path_provider: 348 | dependency: "direct main" 349 | description: 350 | name: path_provider 351 | sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "2.1.4" 355 | path_provider_android: 356 | dependency: transitive 357 | description: 358 | name: path_provider_android 359 | sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "2.2.10" 363 | path_provider_foundation: 364 | dependency: transitive 365 | description: 366 | name: path_provider_foundation 367 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "2.4.0" 371 | path_provider_linux: 372 | dependency: transitive 373 | description: 374 | name: path_provider_linux 375 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "2.2.1" 379 | path_provider_platform_interface: 380 | dependency: transitive 381 | description: 382 | name: path_provider_platform_interface 383 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 384 | url: "https://pub.dev" 385 | source: hosted 386 | version: "2.1.2" 387 | path_provider_windows: 388 | dependency: transitive 389 | description: 390 | name: path_provider_windows 391 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "2.3.0" 395 | petitparser: 396 | dependency: transitive 397 | description: 398 | name: petitparser 399 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "6.0.2" 403 | platform: 404 | dependency: transitive 405 | description: 406 | name: platform 407 | sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "3.1.5" 411 | plugin_platform_interface: 412 | dependency: transitive 413 | description: 414 | name: plugin_platform_interface 415 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "2.1.8" 419 | rxdart: 420 | dependency: "direct main" 421 | description: 422 | name: rxdart 423 | sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "0.28.0" 427 | sky_engine: 428 | dependency: transitive 429 | description: flutter 430 | source: sdk 431 | version: "0.0.99" 432 | source_span: 433 | dependency: transitive 434 | description: 435 | name: source_span 436 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "1.10.0" 440 | sprintf: 441 | dependency: transitive 442 | description: 443 | name: sprintf 444 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "7.0.0" 448 | sqflite: 449 | dependency: transitive 450 | description: 451 | name: sqflite 452 | sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "2.3.3+1" 456 | sqflite_common: 457 | dependency: transitive 458 | description: 459 | name: sqflite_common 460 | sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "2.5.4+2" 464 | stack_trace: 465 | dependency: transitive 466 | description: 467 | name: stack_trace 468 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "1.11.1" 472 | stream_channel: 473 | dependency: transitive 474 | description: 475 | name: stream_channel 476 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "2.1.2" 480 | string_scanner: 481 | dependency: transitive 482 | description: 483 | name: string_scanner 484 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "1.2.0" 488 | synchronized: 489 | dependency: transitive 490 | description: 491 | name: synchronized 492 | sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "3.2.0" 496 | term_glyph: 497 | dependency: transitive 498 | description: 499 | name: term_glyph 500 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "1.2.1" 504 | test_api: 505 | dependency: transitive 506 | description: 507 | name: test_api 508 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "0.7.2" 512 | typed_data: 513 | dependency: transitive 514 | description: 515 | name: typed_data 516 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "1.3.2" 520 | uuid: 521 | dependency: "direct main" 522 | description: 523 | name: uuid 524 | sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "4.5.0" 528 | vector_math: 529 | dependency: transitive 530 | description: 531 | name: vector_math 532 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "2.1.4" 536 | vm_service: 537 | dependency: transitive 538 | description: 539 | name: vm_service 540 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "14.2.5" 544 | web: 545 | dependency: transitive 546 | description: 547 | name: web 548 | sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 549 | url: "https://pub.dev" 550 | source: hosted 551 | version: "1.0.0" 552 | xdg_directories: 553 | dependency: transitive 554 | description: 555 | name: xdg_directories 556 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 557 | url: "https://pub.dev" 558 | source: hosted 559 | version: "1.0.4" 560 | xml: 561 | dependency: transitive 562 | description: 563 | name: xml 564 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 565 | url: "https://pub.dev" 566 | source: hosted 567 | version: "6.5.0" 568 | yaml: 569 | dependency: transitive 570 | description: 571 | name: yaml 572 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 573 | url: "https://pub.dev" 574 | source: hosted 575 | version: "3.1.2" 576 | sdks: 577 | dart: ">=3.5.0 <4.0.0" 578 | flutter: ">=3.22.0" 579 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1020; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | FRAMEWORK_SEARCH_PATHS = ( 293 | "$(inherited)", 294 | "$(PROJECT_DIR)/Flutter", 295 | ); 296 | INFOPLIST_FILE = Runner/Info.plist; 297 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 298 | LIBRARY_SEARCH_PATHS = ( 299 | "$(inherited)", 300 | "$(PROJECT_DIR)/Flutter", 301 | ); 302 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mstreamMusic; 303 | PRODUCT_NAME = "$(TARGET_NAME)"; 304 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 305 | SWIFT_VERSION = 5.0; 306 | VERSIONING_SYSTEM = "apple-generic"; 307 | }; 308 | name = Profile; 309 | }; 310 | 97C147031CF9000F007C117D /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | CLANG_ANALYZER_NONNULL = YES; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 316 | CLANG_CXX_LIBRARY = "libc++"; 317 | CLANG_ENABLE_MODULES = YES; 318 | CLANG_ENABLE_OBJC_ARC = YES; 319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_COMMA = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INFINITE_RECURSION = YES; 328 | CLANG_WARN_INT_CONVERSION = YES; 329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 334 | CLANG_WARN_STRICT_PROTOTYPES = YES; 335 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 336 | CLANG_WARN_UNREACHABLE_CODE = YES; 337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 338 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 339 | COPY_PHASE_STRIP = NO; 340 | DEBUG_INFORMATION_FORMAT = dwarf; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | ENABLE_TESTABILITY = YES; 343 | GCC_C_LANGUAGE_STANDARD = gnu99; 344 | GCC_DYNAMIC_NO_PIC = NO; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_OPTIMIZATION_LEVEL = 0; 347 | GCC_PREPROCESSOR_DEFINITIONS = ( 348 | "DEBUG=1", 349 | "$(inherited)", 350 | ); 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 358 | MTL_ENABLE_DEBUG_INFO = YES; 359 | ONLY_ACTIVE_ARCH = YES; 360 | SDKROOT = iphoneos; 361 | TARGETED_DEVICE_FAMILY = "1,2"; 362 | }; 363 | name = Debug; 364 | }; 365 | 97C147041CF9000F007C117D /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | CLANG_ANALYZER_NONNULL = YES; 370 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 371 | CLANG_CXX_LIBRARY = "libc++"; 372 | CLANG_ENABLE_MODULES = YES; 373 | CLANG_ENABLE_OBJC_ARC = YES; 374 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 375 | CLANG_WARN_BOOL_CONVERSION = YES; 376 | CLANG_WARN_COMMA = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INFINITE_RECURSION = YES; 383 | CLANG_WARN_INT_CONVERSION = YES; 384 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 386 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 387 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 388 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 389 | CLANG_WARN_STRICT_PROTOTYPES = YES; 390 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 394 | COPY_PHASE_STRIP = NO; 395 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 396 | ENABLE_NS_ASSERTIONS = NO; 397 | ENABLE_STRICT_OBJC_MSGSEND = YES; 398 | GCC_C_LANGUAGE_STANDARD = gnu99; 399 | GCC_NO_COMMON_BLOCKS = YES; 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 407 | MTL_ENABLE_DEBUG_INFO = NO; 408 | SDKROOT = iphoneos; 409 | SUPPORTED_PLATFORMS = iphoneos; 410 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | VALIDATE_PRODUCT = YES; 413 | }; 414 | name = Release; 415 | }; 416 | 97C147061CF9000F007C117D /* Debug */ = { 417 | isa = XCBuildConfiguration; 418 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | CLANG_ENABLE_MODULES = YES; 422 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 423 | ENABLE_BITCODE = NO; 424 | FRAMEWORK_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "$(PROJECT_DIR)/Flutter", 427 | ); 428 | INFOPLIST_FILE = Runner/Info.plist; 429 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 430 | LIBRARY_SEARCH_PATHS = ( 431 | "$(inherited)", 432 | "$(PROJECT_DIR)/Flutter", 433 | ); 434 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mstreamMusic; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 437 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 438 | SWIFT_VERSION = 5.0; 439 | VERSIONING_SYSTEM = "apple-generic"; 440 | }; 441 | name = Debug; 442 | }; 443 | 97C147071CF9000F007C117D /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | CLANG_ENABLE_MODULES = YES; 449 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 450 | ENABLE_BITCODE = NO; 451 | FRAMEWORK_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "$(PROJECT_DIR)/Flutter", 454 | ); 455 | INFOPLIST_FILE = Runner/Info.plist; 456 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 457 | LIBRARY_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "$(PROJECT_DIR)/Flutter", 460 | ); 461 | PRODUCT_BUNDLE_IDENTIFIER = com.example.mstreamMusic; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 464 | SWIFT_VERSION = 5.0; 465 | VERSIONING_SYSTEM = "apple-generic"; 466 | }; 467 | name = Release; 468 | }; 469 | /* End XCBuildConfiguration section */ 470 | 471 | /* Begin XCConfigurationList section */ 472 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 97C147031CF9000F007C117D /* Debug */, 476 | 97C147041CF9000F007C117D /* Release */, 477 | 249021D3217E4FDB00AE95B9 /* Profile */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 483 | isa = XCConfigurationList; 484 | buildConfigurations = ( 485 | 97C147061CF9000F007C117D /* Debug */, 486 | 97C147071CF9000F007C117D /* Release */, 487 | 249021D4217E4FDB00AE95B9 /* Profile */, 488 | ); 489 | defaultConfigurationIsVisible = 0; 490 | defaultConfigurationName = Release; 491 | }; 492 | /* End XCConfigurationList section */ 493 | }; 494 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 495 | } -------------------------------------------------------------------------------- /lib/screens/browser.dart: -------------------------------------------------------------------------------- 1 | import 'package:audio_service/audio_service.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:mstream_music/singletons/downloads.dart'; 4 | import 'package:mstream_music/singletons/file_explorer.dart'; 5 | import '../singletons/browser_list.dart'; 6 | import '../singletons/api.dart'; 7 | import '../singletons/transcode.dart'; 8 | import '../objects/display_item.dart'; 9 | import 'package:uuid/uuid.dart'; 10 | import 'package:flutter_slidable/flutter_slidable.dart'; 11 | 12 | import 'dart:io'; 13 | 14 | import '../singletons/media.dart'; 15 | 16 | import 'add_server.dart'; 17 | 18 | class Browser extends StatelessWidget { 19 | void handleTap( 20 | List browserList, int index, BuildContext context) { 21 | if (browserList[index].type == 'addServer') { 22 | Navigator.push( 23 | context, 24 | MaterialPageRoute(builder: (context) => AddServerScreen()), 25 | ); 26 | return; 27 | } 28 | 29 | if (browserList[index].type == 'directory') { 30 | ApiManager().getFileList(browserList[index].data ?? '', 31 | useThisServer: browserList[index].server); 32 | return; 33 | } 34 | 35 | if (browserList[index].type == 'playlist') { 36 | ApiManager().getPlaylistContents(browserList[index].data ?? '', 37 | useThisServer: browserList[index].server); 38 | return; 39 | } 40 | 41 | if (browserList[index].type == 'execAction' && 42 | browserList[index].data == 'playlists') { 43 | ApiManager().getPlaylists(useThisServer: browserList[index].server); 44 | return; 45 | } 46 | 47 | if (browserList[index].type == 'execAction' && 48 | browserList[index].data == 'fileExplorer') { 49 | ApiManager().getFileList("~", useThisServer: browserList[index].server); 50 | return; 51 | } 52 | 53 | if (browserList[index].type == 'execAction' && 54 | browserList[index].data == 'recent') { 55 | ApiManager().getRecentlyAdded(useThisServer: browserList[index].server); 56 | return; 57 | } 58 | 59 | if (browserList[index].type == 'execAction' && 60 | browserList[index].data == 'rated') { 61 | ApiManager().getRated(useThisServer: browserList[index].server); 62 | return; 63 | } 64 | 65 | if (browserList[index].type == 'execAction' && 66 | browserList[index].data == 'albums') { 67 | ApiManager().getAlbums(useThisServer: browserList[index].server); 68 | return; 69 | } 70 | 71 | if (browserList[index].type == 'execAction' && 72 | browserList[index].data == 'localFiles') { 73 | FileExplorer().getPathForServer(browserList[index].server!); 74 | return; 75 | } 76 | 77 | if (browserList[index].type == 'execAction' && 78 | browserList[index].data == 'artists') { 79 | ApiManager().getArtists(useThisServer: browserList[index].server); 80 | return; 81 | } 82 | 83 | if (browserList[index].type == 'artist') { 84 | ApiManager().getArtistAlbums(browserList[index].data ?? '', 85 | useThisServer: browserList[index].server); 86 | return; 87 | } 88 | 89 | if (browserList[index].type == 'album') { 90 | ApiManager().getAlbumSongs(browserList[index].data, 91 | useThisServer: browserList[index].server); 92 | return; 93 | } 94 | 95 | if (browserList[index].type == 'file') { 96 | addFile(browserList[index]); 97 | return; 98 | } 99 | 100 | if (browserList[index].type == 'localDirectory') { 101 | FileExplorer() 102 | .getLocalFiles(browserList[index].data, browserList[index].server!); 103 | return; 104 | } 105 | 106 | if (browserList[index].type == 'localFile') { 107 | addLocalFile(browserList[index]); 108 | return; 109 | } 110 | } 111 | 112 | void addLocalFile(DisplayItem i) { 113 | MediaItem item = new MediaItem( 114 | id: Uuid().v4(), 115 | title: i.name.split('/').last, 116 | extras: {'path': i.data, 'localPath': i.data!}); 117 | MediaManager().audioHandler.addQueueItem(item); 118 | } 119 | 120 | void addFile(DisplayItem i) async { 121 | // Check for song locally 122 | String downloadDirectory = i.server!.localname + i.data!; 123 | final dir = await FileExplorer().getDownloadDir(i.server!.saveToSdCard); 124 | if (dir == null) { 125 | return; 126 | } 127 | String finalString = '${dir.path}/media/$downloadDirectory'; 128 | 129 | if (new File(finalString).existsSync() == true) { 130 | print('exists!'); 131 | 132 | MediaItem item = new MediaItem( 133 | id: Uuid().v4(), 134 | title: i.metadata?.title ?? i.name, 135 | album: i.metadata?.album, 136 | artist: i.metadata?.artist, 137 | artUri: i.metadata?.albumArt != null 138 | ? Uri.parse(i.server!.url.toString()).resolve('/album-art/' + 139 | i.metadata!.albumArt! + 140 | '?compress=l&token=' + 141 | (i.server!.jwt ?? '')) 142 | : Uri.parse(i.server!.url.toString()) 143 | .resolve('/assets/img/default.png'), 144 | extras: { 145 | 'path': i.data, 146 | 'localPath': finalString, 147 | 'year': i.metadata?.year 148 | }); 149 | MediaManager().audioHandler.addQueueItem(item); 150 | return; 151 | } 152 | 153 | String prefix = 154 | TranscodeManager().transcodeOn == true ? '/transcode' : '/media'; 155 | 156 | String p = ''; 157 | i.data!.split("/").forEach((element) { 158 | if (element.length == 0) { 159 | return; 160 | } 161 | p += "/" + Uri.encodeComponent(element); 162 | }); 163 | 164 | String lolUrl = i.server!.url + 165 | prefix + 166 | p + 167 | '?app_uuid=' + 168 | Uuid().v4() + 169 | (i.server!.jwt == null ? '' : '&token=' + i.server!.jwt!); 170 | 171 | MediaItem item = new MediaItem( 172 | id: lolUrl, 173 | title: i.metadata?.title ?? i.name, 174 | album: i.metadata?.album, 175 | artist: i.metadata?.artist, 176 | artUri: i.metadata?.albumArt != null 177 | ? Uri.parse(i.server!.url.toString()).resolve('/album-art/' + 178 | i.metadata!.albumArt! + 179 | '?compress=l&token=' + 180 | (i.server!.jwt ?? '')) 181 | : Uri.parse(i.server!.url.toString()) 182 | .resolve('/assets/img/default.png'), 183 | extras: { 184 | 'server': i.server!.localname, 185 | 'path': i.data, 186 | 'year': i.metadata?.year 187 | }); 188 | 189 | MediaManager().audioHandler.addQueueItem(item); 190 | 191 | // TODO: Fire of request for metadata 192 | } 193 | 194 | Widget makeListItem(List b, int i, BuildContext c) { 195 | switch (b[i].type) { 196 | case "file": 197 | { 198 | return makeFileWidget(b, i, c); 199 | } 200 | case "playlist": 201 | { 202 | return makePlaylistWidget(b, i, c); 203 | } 204 | case "directory": 205 | { 206 | return makeFolderWidget(b, i, c); 207 | } 208 | case "localDirectory": 209 | { 210 | return makeLocalFolderWidget(b, i, c); 211 | } 212 | case "localFile": 213 | { 214 | return makeLocalFileWidget(b, i, c); 215 | } 216 | default: 217 | { 218 | return makeBasicWidget(b, i, c); 219 | } 220 | } 221 | } 222 | 223 | Widget makePlaylistWidget(List b, int i, BuildContext c) { 224 | final _slidableKey = GlobalKey(); 225 | 226 | return Container( 227 | decoration: BoxDecoration( 228 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 229 | child: Slidable( 230 | key: _slidableKey, 231 | actionPane: SlidableDrawerActionPane(), 232 | secondaryActions: [ 233 | IconSlideAction( 234 | color: Colors.redAccent, 235 | icon: Icons.remove_circle, 236 | caption: 'Delete', 237 | onTap: () { 238 | showDialog( 239 | context: c, 240 | builder: (BuildContext context) { 241 | return AlertDialog( 242 | title: Text("Confirm Delete Playlist"), 243 | content: b[i].getText(), 244 | actions: [ 245 | TextButton( 246 | child: Text("Go Back"), 247 | onPressed: () { 248 | Navigator.of(context).pop(); 249 | }, 250 | ), 251 | TextButton( 252 | child: Text( 253 | "Delete", 254 | style: TextStyle(color: Colors.red), 255 | ), 256 | onPressed: () { 257 | ApiManager().removePlaylist(b[i].data!, 258 | useThisServer: b[i].server); 259 | Navigator.of(context).pop(); 260 | }) 261 | ]); 262 | }); 263 | }) 264 | ], 265 | child: ListTile( 266 | leading: b[i].icon ?? null, 267 | title: b[i].getText(), 268 | subtitle: b[i].getSubText(), 269 | trailing: IconButton( 270 | icon: Icon( 271 | Icons.keyboard_arrow_left, 272 | size: 20.0, 273 | color: Colors.brown[900], 274 | ), 275 | onPressed: () { 276 | _slidableKey.currentState?.open( 277 | actionType: SlideActionType.secondary, 278 | ); 279 | }, 280 | ), 281 | onTap: () { 282 | handleTap(b, i, c); 283 | })), 284 | ); 285 | } 286 | 287 | Widget makeLocalFolderWidget(List b, int i, BuildContext c) { 288 | final _slidableKey = GlobalKey(); 289 | 290 | return Container( 291 | decoration: BoxDecoration( 292 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 293 | child: Slidable( 294 | key: _slidableKey, 295 | actionPane: SlidableDrawerActionPane(), 296 | secondaryActions: [ 297 | IconSlideAction( 298 | color: Colors.red, 299 | icon: Icons.delete, 300 | caption: 'Delete', 301 | onTap: () { 302 | showDialog( 303 | context: c, 304 | builder: (BuildContext context) { 305 | return AlertDialog( 306 | title: Text("Confirm Delete Folder"), 307 | content: b[i].getText(), 308 | actions: [ 309 | TextButton( 310 | child: Text("Go Back"), 311 | onPressed: () { 312 | Navigator.of(context).pop(); 313 | }, 314 | ), 315 | TextButton( 316 | child: Text( 317 | "Delete", 318 | style: TextStyle(color: Colors.red), 319 | ), 320 | onPressed: () { 321 | FileExplorer().deleteDirectory( 322 | b[i].data!, b[i].server); 323 | Navigator.of(context).pop(); 324 | }) 325 | ]); 326 | }); 327 | }) 328 | ], 329 | child: ListTile( 330 | leading: b[i].icon ?? null, 331 | title: b[i].getText(), 332 | subtitle: b[i].getSubText(), 333 | trailing: IconButton( 334 | icon: Icon( 335 | Icons.keyboard_arrow_left, 336 | size: 20.0, 337 | color: Colors.brown[900], 338 | ), 339 | onPressed: () { 340 | _slidableKey.currentState?.open( 341 | actionType: SlideActionType.secondary, 342 | ); 343 | }, 344 | ), 345 | onTap: () { 346 | handleTap(b, i, c); 347 | }))); 348 | } 349 | 350 | Widget makeLocalFileWidget(List b, int i, BuildContext c) { 351 | final _slidableKey = GlobalKey(); 352 | 353 | return Container( 354 | decoration: BoxDecoration( 355 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 356 | child: Slidable( 357 | key: _slidableKey, 358 | actionPane: SlidableDrawerActionPane(), 359 | secondaryActions: [ 360 | IconSlideAction( 361 | color: Colors.red, 362 | icon: Icons.delete, 363 | caption: 'Delete', 364 | onTap: () { 365 | // ApiManager().getRecursiveFiles(b[i].data!, 366 | // useThisServer: b[i].server); 367 | FileExplorer().deleteFile(b[i].data!, b[i].server); 368 | }) 369 | ], 370 | child: ListTile( 371 | leading: b[i].icon ?? null, 372 | title: b[i].getText(), 373 | subtitle: b[i].getSubText(), 374 | trailing: IconButton( 375 | icon: Icon( 376 | Icons.keyboard_arrow_left, 377 | size: 20.0, 378 | color: Colors.brown[900], 379 | ), 380 | onPressed: () { 381 | _slidableKey.currentState?.open( 382 | actionType: SlideActionType.secondary, 383 | ); 384 | }, 385 | ), 386 | onTap: () { 387 | handleTap(b, i, c); 388 | }))); 389 | } 390 | 391 | Widget makeFolderWidget(List b, int i, BuildContext c) { 392 | final _slidableKey = GlobalKey(); 393 | 394 | return Container( 395 | decoration: BoxDecoration( 396 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 397 | child: Slidable( 398 | key: _slidableKey, 399 | actionPane: SlidableDrawerActionPane(), 400 | secondaryActions: [ 401 | IconSlideAction( 402 | color: Colors.blueGrey, 403 | icon: Icons.add_to_queue, 404 | caption: 'Add All', 405 | onTap: () { 406 | ApiManager().getRecursiveFiles(b[i].data!, 407 | useThisServer: b[i].server); 408 | }) 409 | ], 410 | child: ListTile( 411 | leading: b[i].icon ?? null, 412 | title: b[i].getText(), 413 | subtitle: b[i].getSubText(), 414 | trailing: IconButton( 415 | icon: Icon( 416 | Icons.keyboard_arrow_left, 417 | size: 20.0, 418 | color: Colors.brown[900], 419 | ), 420 | onPressed: () { 421 | _slidableKey.currentState?.open( 422 | actionType: SlideActionType.secondary, 423 | ); 424 | }, 425 | ), 426 | onTap: () { 427 | handleTap(b, i, c); 428 | }))); 429 | } 430 | 431 | Widget makeBasicWidget(List b, int i, BuildContext c) { 432 | return Container( 433 | decoration: BoxDecoration( 434 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 435 | child: ListTile( 436 | leading: b[i].getImage(), 437 | title: b[i].getText(), 438 | subtitle: b[i].getSubText(), 439 | onTap: () { 440 | handleTap(b, i, c); 441 | })); 442 | } 443 | 444 | Widget makeFileWidget(List b, int i, BuildContext c) { 445 | return Container( 446 | decoration: BoxDecoration( 447 | border: Border(bottom: BorderSide(color: Color(0xFFbdbdbd)))), 448 | child: Material( 449 | color: Color(0xFFe1e2e1), 450 | child: InkWell( 451 | splashColor: Colors.blue, 452 | child: IntrinsicHeight( 453 | child: Row( 454 | crossAxisAlignment: CrossAxisAlignment.stretch, 455 | children: [ 456 | Container( 457 | width: 4, 458 | child: RotatedBox( 459 | quarterTurns: 3, 460 | child: LinearProgressIndicator( 461 | // value: displayList[index].downloadProgress/100, 462 | value: BrowserManager() 463 | .browserList[i] 464 | .downloadProgress / 465 | 100, 466 | valueColor: new AlwaysStoppedAnimation(Colors.blue), 467 | backgroundColor: Colors.white.withOpacity(0), 468 | ), 469 | ), 470 | ), 471 | Expanded( 472 | child: ListTile( 473 | leading: b[i].getImage(), 474 | title: b[i].getText(), 475 | subtitle: b[i].getSubText(), 476 | onTap: () { 477 | handleTap(b, i, c); 478 | })) 479 | ]))))); 480 | } 481 | 482 | Widget build(BuildContext context) { 483 | return Column(children: [ 484 | Material( 485 | color: Color(0xFFffffff), 486 | child: StreamBuilder>( 487 | stream: BrowserManager().browserListStream, 488 | builder: (context, snapshot) { 489 | final List browserList = snapshot.data ?? []; 490 | 491 | if (browserList.length > 0) { 492 | print(browserList[0].type); 493 | } 494 | return Row( 495 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 496 | children: [ 497 | if (browserList.length == 0 || 498 | browserList[0].type != 'execAction') ...[ 499 | IconButton( 500 | icon: Icon(Icons.keyboard_arrow_left, 501 | color: Colors.black), 502 | tooltip: 'Go Back', 503 | onPressed: () { 504 | BrowserManager().popBrowser(); 505 | }), 506 | Row(children: [ 507 | IconButton( 508 | icon: Icon( 509 | Icons.download_sharp, 510 | color: Colors.black, 511 | ), 512 | tooltip: 'Download', 513 | onPressed: () { 514 | int count = 0; 515 | 516 | BrowserManager().browserList.forEach((e) { 517 | if (e.type == 'file') { 518 | String downloadUrl = e.server!.url + 519 | '/media' + 520 | e.data! + 521 | (e.server!.jwt == null 522 | ? '' 523 | : '?token=' + e.server!.jwt!); 524 | 525 | DownloadManager().downloadOneFile( 526 | downloadUrl, 527 | e.server!.localname, 528 | e.data!, 529 | e.server!.saveToSdCard); 530 | count++; 531 | } 532 | }); 533 | 534 | ScaffoldMessenger.of(context).showSnackBar( 535 | SnackBar( 536 | content: 537 | Text('$count downloads started'))); 538 | }), 539 | IconButton( 540 | icon: Icon( 541 | Icons.library_add, 542 | color: Colors.black, 543 | ), 544 | tooltip: 'Add All', 545 | onPressed: () { 546 | int n = 0; 547 | 548 | BrowserManager().browserList.forEach((element) { 549 | if (element.type == 'localFile') { 550 | if (element.data!.substring( 551 | element.data!.length - 4) == 552 | '.m3u') { 553 | return; 554 | } 555 | addLocalFile(element); 556 | n++; 557 | } else if (element.type == 'file') { 558 | if (element.data!.substring( 559 | element.data!.length - 4) == 560 | '.m3u') { 561 | return; 562 | } 563 | addFile(element); 564 | n++; 565 | } 566 | }); 567 | 568 | if (n > 0) { 569 | ScaffoldMessenger.of(context).showSnackBar( 570 | SnackBar( 571 | content: Text(n.toString() + 572 | " songs added to queue"))); 573 | } 574 | }) 575 | ]) 576 | ] else ...[ 577 | Expanded( 578 | child: TextField( 579 | onSubmitted: (text) { 580 | ApiManager().searchServer(text); 581 | print('First text field: $text'); 582 | }, 583 | style: TextStyle(color: Colors.black), 584 | decoration: InputDecoration( 585 | prefixIcon: Icon( 586 | Icons.search, 587 | color: Colors.black, 588 | ), 589 | hintStyle: TextStyle( 590 | color: Colors.black, 591 | ), 592 | labelStyle: TextStyle( 593 | color: Colors.black, 594 | ), 595 | hintText: 'Search Database', 596 | ))) 597 | ] 598 | ]); 599 | }), 600 | ), 601 | Expanded( 602 | child: SizedBox( 603 | child: StreamBuilder>( 604 | stream: BrowserManager().browserListStream, 605 | builder: (context, snapshot) { 606 | final List browserList = snapshot.data ?? []; 607 | return ListView.separated( 608 | controller: BrowserManager().sc, 609 | physics: const AlwaysScrollableScrollPhysics(), 610 | separatorBuilder: (BuildContext context, int index) => 611 | Divider(height: 3, color: Colors.white), 612 | itemCount: browserList.length, 613 | itemBuilder: (BuildContext context, int index) { 614 | // Fixes an odd rendering bug when going between tabs 615 | if (browserList.length == 0) { 616 | return Container(); 617 | } 618 | 619 | return makeListItem(browserList, index, context); 620 | }); 621 | }))) 622 | ]); 623 | } 624 | } 625 | --------------------------------------------------------------------------------