Add swingmusic_mobile as submodule

This commit is contained in:
Tomas Dvorak
2026-03-18 19:44:39 +01:00
parent 6ca75aedf3
commit 5152d9dfeb
161 changed files with 4 additions and 18429 deletions
+3
View File
@@ -14,3 +14,6 @@
path = swingmusic_mobile path = swingmusic_mobile
url = https://github.com/Dvorinka/swingmusic-mobile.git url = https://github.com/Dvorinka/swingmusic-mobile.git
branch = master branch = master
[submodule "swingmusic_mobile"]
path = swingmusic_mobile
url = https://github.com/Dvorinka/swingmusic-mobile.git
+1
Submodule swingmusic_mobile added at 1208150e98
-157
View File
@@ -1,157 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VS Code related
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/android/gradlew
/android/gradlew.bat
/android/local.properties
/android/.gradle/
/android/captures/
/android/gradlew
/android/gradlew.bat
/android/local.properties
/android/.gradle/
# iOS/Xcode related
/ios/Flutter/Flutter.framework
/ios/Flutter/Flutter.podspec
/ios/.symlinks/
/ios/Pods/
/ios/.symlinks/
/ios/Pods/
/ios/Flutter/App.framework
/ios/Flutter/Flutter.framework
/ios/Flutter/Flutter.podspec
/ios/Flutter/Generated.xcconfig
/ios/Flutter/ephemeral/
/ios/app.flx
/ios/app.zip
/ios/app_flutter/
/ios/flutter_assets/
/ios/service_account.json
# Web related
/web/
# Windows related
/windows/flutter/generated_plugin_registrant.cc
/windows/flutter/generated_plugin_registrant.h
/windows/flutter/generated_plugins.cmake
# Linux related
/linux/flutter/generated_plugin_registrant.cc
/linux/flutter/generated_plugin_registrant.h
/linux/flutter/generated_plugins.cmake
# macOS related
/macos/Flutter/Generated.xcconfig
/macos/Flutter/ephemeral/
/macos/Flutter/flutter_assets/
/macos/Flutter/App.framework
/macos/Flutter/Flutter.framework
/macos/Flutter/Flutter.podspec
# Test coverage
/coverage/
# Environment files
.env
.env.local
.env.development
.env.test
.env.production
# API keys and secrets
**/api_keys.dart
**/secrets.dart
**/config.dart
# Temporary files
*.tmp
*.temp
*.bak
*.backup
# Logs
logs/
*.log
# Database files
*.db
*.sqlite
*.sqlite3
# Generated files
*.g.dart
*.freezed.dart
*.mocks.dart
# Node.js (if using for build tools)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Firebase
google-services.json
GoogleService-Info.plist
# Localization
*.arb
# Assets that should be managed separately
/assets/audio/
/assets/images/large/
*.mp3
*.flac
*.wav
*.aac
*.ogg
*.m4a
-45
View File
@@ -1,45 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: ios
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: linux
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: macos
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: web
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: windows
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
-16
View File
@@ -1,16 +0,0 @@
# swingmusic_mobile
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
-28
View File
@@ -1,28 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
-14
View File
@@ -1,14 +0,0 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -1,44 +0,0 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.dvorinka.swingmusic.swingmusic_mobile"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.dvorinka.swingmusic.swingmusic_mobile"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,46 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.swingmusic_mobile">
<application
android:label="swingmusic_mobile"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -1,5 +0,0 @@
package com.dvorinka.swingmusic.swingmusic_mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,24 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -1,2 +0,0 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
@@ -1,26 +0,0 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
-34
View File
@@ -1,34 +0,0 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
@@ -1 +0,0 @@
#include "Generated.xcconfig"
@@ -1 +0,0 @@
#include "Generated.xcconfig"
@@ -1,616 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.dvorinka.swingmusic.swingmusicMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -1,13 +0,0 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -1,122 +0,0 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

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

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

@@ -1,5 +0,0 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
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.
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
-49
View File
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Swingmusic Mobile</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>swingmusic_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -1 +0,0 @@
#import "GeneratedPluginRegistrant.h"
@@ -1,12 +0,0 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
@@ -1,71 +0,0 @@
class AppConstants {
// App Info
static const String appName = 'SwingMusic';
static const String appVersion = '1.0.0';
// API Configuration
static const String defaultApiUrl = 'http://localhost:8181';
static const Duration apiTimeout = Duration(seconds: 30);
static const int maxRetries = 3;
// Audio Configuration
static const Duration audioFadeDuration = Duration(milliseconds: 500);
static const int maxAudioCacheSize = 100 * 1024 * 1024; // 100MB
static const String audioCacheKey = 'audio_cache';
// UI Configuration
static const double defaultPadding = 16.0;
static const double smallPadding = 8.0;
static const double largePadding = 24.0;
static const double borderRadius = 12.0;
static const double cardBorderRadius = 16.0;
// Animation Durations
static const Duration fastAnimation = Duration(milliseconds: 200);
static const Duration mediumAnimation = Duration(milliseconds: 300);
static const Duration slowAnimation = Duration(milliseconds: 500);
// Image Dimensions
static const double albumArtSize = 56.0;
static const double largeAlbumArtSize = 200.0;
static const double artistImageSize = 80.0;
// Storage Keys
static const String themeKey = 'app_theme';
static const String authTokenKey = 'auth_token';
static const String userProfileKey = 'user_profile';
static const String settingsKey = 'app_settings';
static const String favoritesKey = 'favorites';
static const String playlistsKey = 'playlists';
// Pagination
static const int defaultPageSize = 20;
static const int searchPageSize = 15;
// Audio Quality
static const Map<String, String> audioQualities = {
'low': '96kbps',
'medium': '192kbps',
'high': '320kbps',
'lossless': 'FLAC',
};
// Error Messages
static const String networkErrorMessage = 'Please check your internet connection';
static const String serverErrorMessage = 'Server is temporarily unavailable';
static const String authErrorMessage = 'Please login to continue';
static const String genericErrorMessage = 'Something went wrong. Please try again';
// Routes
static const String homeRoute = '/home';
static const String libraryRoute = '/library';
static const String playerRoute = '/player';
static const String searchRoute = '/search';
static const String playlistsRoute = '/playlists';
static const String settingsRoute = '/settings';
static const String authRoute = '/auth';
static const String qrRoute = '/qr';
static const String offlineRoute = '/offline';
static const String analyticsRoute = '/analytics';
static const String profileRoute = '/profile';
}
@@ -1,97 +0,0 @@
import 'package:flutter/material.dart';
/// Unified icon constants and sizes matching web client
class AppIcons {
// Navigation icons
static const IconData home = Icons.home_outlined;
static const IconData homeFilled = Icons.home;
static const IconData search = Icons.search_outlined;
static const IconData searchFilled = Icons.search;
static const IconData library = Icons.library_music_outlined;
static const IconData libraryFilled = Icons.library_music;
// Media control icons
static const IconData play = Icons.play_arrow;
static const IconData pause = Icons.pause;
static const IconData skipBack = Icons.skip_previous;
static const IconData skipForward = Icons.skip_next;
static const IconData volume = Icons.volume_up_outlined;
static const IconData volumeMuted = Icons.volume_off_outlined;
// Content icons
static const IconData album = Icons.album;
static const IconData artist = Icons.person;
static const IconData track = Icons.music_note;
static const IconData folder = Icons.folder;
static const IconData playlist = Icons.playlist_play;
static const IconData favorite = Icons.favorite_border;
static const IconData favoriteFilled = Icons.favorite;
// Action icons
static const IconData more = Icons.more_vert;
static const IconData add = Icons.add;
static const IconData download = Icons.download;
static const IconData share = Icons.share;
static const IconData settings = Icons.settings;
static const IconData notifications = Icons.notifications_outlined;
static const IconData user = Icons.person;
// Status icons
static const IconData playing = Icons.equalizer;
static const IconData success = Icons.check_circle;
static const IconData error = Icons.error;
static const IconData warning = Icons.warning;
static const IconData info = Icons.info;
}
/// Unified icon sizes matching web client
class AppIconSizes {
static const double xs = 16.0;
static const double sm = 20.0;
static const double md = 24.0;
static const double lg = 32.0;
static const double xl = 48.0;
static const double xxl = 64.0;
// Navigation icons
static const double navigationSize = sm;
// Media control icons
static const double mediaControlSize = lg;
static const double mediaControlSmallSize = md;
// Content icons
static const double contentIconSize = md;
static const double contentIconLargeSize = lg;
// Action icons
static const double actionIconSize = sm;
static const double actionIconLargeSize = md;
// Status icons
static const double statusIconSize = sm;
static const double statusIconLargeSize = md;
}
/// Icon widget with consistent styling
class AppIcon extends StatelessWidget {
final IconData icon;
final double? size;
final Color? color;
const AppIcon({
super.key,
required this.icon,
this.size,
this.color,
});
@override
Widget build(BuildContext context) {
return Icon(
icon,
size: size ?? AppIconSizes.contentIconSize,
color: color ?? Theme.of(context).colorScheme.onSurface,
);
}
}
@@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
/// Unified spacing constants matching web client design tokens exactly
class AppSpacing {
// Base spacing unit (4px) matching web client
static const double xs = 4.0; // 0.25rem = $smallest
static const double sm = 8.0; // 0.5rem = $smaller
static const double md = 12.0; // 0.75rem
static const double lg = 16.0; // 1rem = $small
static const double xl = 20.0; // 1.25rem
static const double xxl = 24.0; // 1.5rem = $medium
static const double xxxl = 32.0; // 2rem = $large
static const double larger = 32.0; // $larger = 2rem
// Web client exact sizing from _variables.scss
static const double bannerHeight = 288.0; // $banner-height: 18rem
static const double songItemHeight = 64.0; // $song-item-height: 4rem
static const double contentPaddingBottom = 32.0; // $content-padding-bottom: 2rem
static const double navHeight = 72.0; // $navheight: 4.5rem
static const double cardWidth = 172.0; // $cardwidth: 10.75rem
static const double maxPadLeft = 80.0; // $maxpadleft: 5rem
static const double padBottom = 64.0; // $padbottom: 4rem
// Web client specific component sizing
static const double buttonHeight = 36.0; // 2.25rem from basic.scss
static const double buttonMoreWidth = 40.0; // 2.5rem from basic.scss
static const double progressBarHeight = 4.8; // 0.3rem from ProgressBar.scss
static const double searchHeight = 36.0; // 2.25rem from inputs.scss
static const double tabHeight = 32.0; // 2rem from search-tabheaders.scss
static const double stateSize = 32.0; // 2rem from state.scss
static const double explicitIconWidth = 14.4; // 0.9rem from basic.scss
static const double spinnerSize = 20.0; // 1.25rem from basic.scss
// Grid spacing matching web client album-grid.scss
static const double gridPadding = 16.0; // 1rem padding
static const double gridGap = 16.0; // 1rem gap
static const double gridGapVertical = 32.0; // 2rem vertical gap
static const double gridMinWidth = 144.0; // 9rem min-width
// Consistent padding
static const EdgeInsets paddingXS = EdgeInsets.all(xs);
static const EdgeInsets paddingSM = EdgeInsets.all(sm);
static const EdgeInsets paddingMD = EdgeInsets.all(md);
static const EdgeInsets paddingLG = EdgeInsets.all(lg);
static const EdgeInsets paddingXL = EdgeInsets.all(xl);
// Consistent margins
static const EdgeInsets marginXS = EdgeInsets.all(xs);
static const EdgeInsets marginSM = EdgeInsets.all(sm);
static const EdgeInsets marginMD = EdgeInsets.all(md);
static const EdgeInsets marginLG = EdgeInsets.all(lg);
static const EdgeInsets marginXL = EdgeInsets.all(xl);
// Horizontal spacing
static const EdgeInsets horizontalXS = EdgeInsets.symmetric(horizontal: xs);
static const EdgeInsets horizontalSM = EdgeInsets.symmetric(horizontal: sm);
static const EdgeInsets horizontalMD = EdgeInsets.symmetric(horizontal: md);
static const EdgeInsets horizontalLG = EdgeInsets.symmetric(horizontal: lg);
static const EdgeInsets horizontalXL = EdgeInsets.symmetric(horizontal: xl);
// Vertical spacing
static const EdgeInsets verticalXS = EdgeInsets.symmetric(vertical: xs);
static const EdgeInsets verticalSM = EdgeInsets.symmetric(vertical: sm);
static const EdgeInsets verticalMD = EdgeInsets.symmetric(vertical: md);
static const EdgeInsets verticalLG = EdgeInsets.symmetric(vertical: lg);
static const EdgeInsets verticalXL = EdgeInsets.symmetric(vertical: xl);
// Card spacing matching web client album card
static const EdgeInsets cardPadding = EdgeInsets.all(lg);
static const EdgeInsets cardMargin = EdgeInsets.all(sm);
// List spacing
static const EdgeInsets listPadding = EdgeInsets.symmetric(vertical: sm);
static const double listItemSpacing = sm;
// Section spacing
static const double sectionSpacing = xxl;
static const EdgeInsets sectionPadding = EdgeInsets.all(lg);
// Button spacing matching web client
static const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: lg, vertical: md);
// Form spacing
static const double formFieldSpacing = md;
static const EdgeInsets formPadding = EdgeInsets.all(lg);
// Animation and timing constants matching web client
static const Duration transitionFast = Duration(milliseconds: 200); // 0.2s ease-out
static const Duration transitionNormal = Duration(milliseconds: 250); // 0.25s ease
static const Duration transitionSlow = Duration(milliseconds: 300); // 0.3s ease
static const Duration spinnerDuration = Duration(milliseconds: 450); // 0.45s linear infinite
static const Duration pulseDuration = Duration(milliseconds: 600); // 0.6s infinite
static const Duration pulseDelay = Duration(milliseconds: 120); // $i * 0.12s
// Z-index values matching web client
static const int dimmerZIndex = 1001; // From Global/index.scss
}
/// Unified border radius constants matching web client exactly
class AppBorderRadius {
static const double xs = 4.0; // 0.25rem
static const double sm = 8.0; // 0.5rem = $small
static const double md = 12.0; // 0.75rem
static const double lg = 16.0; // 1rem = $rounded
static const double xl = 20.0; // 1.25rem = $rounded-lg
static const double xxl = 24.0; // 1.5rem = $rounded-md
static const double circular = 160.0; // 10rem = .circular
static const double full = 9999.0;
// Web client specific border radius values
static const double progressBar = 5.0; // 5px from ProgressBar.scss
static const double input = 3.0; // 3px from inputs.scss
static const double scrollbar = 16.0; // 16px from scrollbars.scss
static const double searchInput = 3.0; // 3px from inputs.scss
static const double duration = 8.0; // 0.5rem from BottomBar.scss
static const double dragImage = 4.0; // $smaller from basic.scss
static const double badge = 4.0; // $smaller from basic.scss
static const double explicitIcon = 4.0; // $smaller from basic.scss
static BorderRadius circularXS = BorderRadius.circular(xs);
static BorderRadius circularSM = BorderRadius.circular(sm);
static BorderRadius circularMD = BorderRadius.circular(md);
static BorderRadius circularLG = BorderRadius.circular(lg);
static BorderRadius circularXL = BorderRadius.circular(xl);
static BorderRadius circularXXL = BorderRadius.circular(xxl);
static BorderRadius circularFull = BorderRadius.circular(full);
static BorderRadius circularCircular = BorderRadius.circular(circular);
}
@@ -1,24 +0,0 @@
enum AuthState {
loggedOut,
authenticating,
authenticated,
error;
String get displayName {
switch (this) {
case AuthState.loggedOut:
return 'Logged Out';
case AuthState.authenticating:
return 'Authenticating';
case AuthState.authenticated:
return 'Authenticated';
case AuthState.error:
return 'Error';
}
}
bool get isLoggedIn => this == AuthState.authenticated;
bool get isLoggedOut => this == AuthState.loggedOut;
bool get isAuthenticating => this == AuthState.authenticating;
bool get hasError => this == AuthState.error;
}
@@ -1,50 +0,0 @@
enum RepeatMode {
off,
one,
all;
String get displayName {
switch (this) {
case RepeatMode.off:
return 'Off';
case RepeatMode.one:
return 'Repeat One';
case RepeatMode.all:
return 'Repeat All';
}
}
RepeatMode next() {
switch (this) {
case RepeatMode.off:
return RepeatMode.all;
case RepeatMode.all:
return RepeatMode.one;
case RepeatMode.one:
return RepeatMode.off;
}
}
}
enum ShuffleMode {
off,
on;
String get displayName {
switch (this) {
case ShuffleMode.off:
return 'Off';
case ShuffleMode.on:
return 'On';
}
}
ShuffleMode toggle() {
switch (this) {
case ShuffleMode.off:
return ShuffleMode.on;
case ShuffleMode.on:
return ShuffleMode.off;
}
}
}
@@ -1,237 +0,0 @@
import 'package:flutter/material.dart';
class AppTheme {
// Unified color scheme matching web client exactly
static const Color primaryColor = Color(0xFF006EFF); // $highlight-blue
static const Color secondaryColor = Color(0xFF8B5CF6); // --color-secondary
static const Color tertiaryColor = Color(0xFF10B981); // --color-accent
// Web client exact colors
static const Color highlightBlue = Color(0xFF006EFF); // $highlight-blue
static const Color darkestBlue = Color(0xFF234ECE); // $darkestblue
static const Color darkBlue = Color(0xFF055EE2); // $darkblue
// Apple human design guideline colors (exact match)
static const Color black = Color(0xFF181A1C); // $black
static const Color white = Color(0xFFFFFFDE); // $white (with alpha)
static const Color gray = Color(0xFF1A1919); // $gray
static const Color gray1 = Color(0xFF8E8E93); // $gray1
static const Color gray2 = Color(0xFF636366); // $gray2
static const Color gray3 = Color(0xFF48484A); // $gray3
static const Color gray4 = Color(0xFF3A3A3C); // $gray4
static const Color gray5 = Color(0xFF2C2C2E); // $gray5
static const Color body = Color(0xFF000000); // $body
// Semantic colors (exact match)
static const Color red = Color(0xFFF7635C); // $red
static const Color blue = Color(0xFF0A84FF); // $blue
static const Color green = Color(0xFF5EF784); // $green
static const Color yellow = Color(0xFFFFD60A); // $yellow
static const Color orange = Color(0xFFFF9F0A); // $orange
static const Color pink = Color(0xFFFF375F); // $pink
static const Color purple = Color(0xFFBF5AF2); // $purple
static const Color brown = Color(0xFFAC8E68); // $brown
static const Color indigo = Color(0xFF5E5CE6); // $indigo
static const Color teal = Color(0xFF40C8E0); // $teal
static const Color lightBrown = Color(0xFFEBCA89); // $lightbrown
static const Color surfaceColor = Color(0xFFFAFAFA); // --color-surface
static const Color surfaceVariantColor = Color(0xFFF5F5F5); // --color-surface-variant
static const Color backgroundColor = Color(0xFFFFFFFF); // --color-background
static const Color onPrimaryColor = Color(0xFFFFFFFF); // --color-on-primary
static const Color onSecondaryColor = Color(0xFFFFFFFF); // --color-on-secondary
static const Color onTertiaryColor = Color(0xFFFFFFFF); // --color-on-accent
static const Color onSurfaceColor = Color(0xFF1C1C1C); // --color-on-surface
static const Color onBackgroundColor = Color(0xFF1C1C1C); // --color-on-background
static const Color outlineColor = Color(0xFFE5E7EB); // --color-border
static const Color outlineVariantColor = Color(0xFFF3F4F6); // --color-divider
// Status colors
static const Color successColor = Color(0xFF10B981);
static const Color warningColor = Color(0xFFF59E0B);
static const Color errorColor = Color(0xFFEF4444);
static const Color infoColor = Color(0xFF3B82F6);
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
// Exact font family matching web client with fallbacks
fontFamily: 'SF Compact Display',
colorScheme: const ColorScheme.light(
primary: primaryColor,
secondary: secondaryColor,
tertiary: tertiaryColor,
surface: surfaceColor,
surfaceVariant: surfaceVariantColor,
background: backgroundColor,
onPrimary: onPrimaryColor,
onSecondary: onSecondaryColor,
onTertiary: onTertiaryColor,
onSurface: onSurfaceColor,
onBackground: onBackgroundColor,
outline: outlineColor,
outlineVariant: outlineVariantColor,
),
// Consistent transitions matching web client
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
},
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: onSurfaceColor,
),
cardTheme: CardThemeData(
elevation: 0, // Match web client flat design
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Match web client rounded-sm ($small)
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), // Consistent padding
elevation: 0, // Match web client flat design
backgroundColor: primaryColor,
foregroundColor: onPrimaryColor,
textStyle: const TextStyle(
fontFamily: 'SF Compact Display',
fontWeight: FontWeight.w700, // Match web client font-weight
fontSize: 14, // Match web client font-size (0.9rem)
),
),
),
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 36, // Match web client larger headings
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0,
height: 1.2,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0,
height: 1.2,
),
headlineSmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0,
height: 1.2,
),
titleLarge: TextStyle(
fontSize: 20, // Match web client title size
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0,
height: 1.3,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, // Match web client font-weight
letterSpacing: 0.15,
height: 1.3,
),
titleSmall: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0.1,
height: 1.3,
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400, // Match web client font-weight
letterSpacing: 0.5,
height: 1.4,
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400, // Match web client font-weight
letterSpacing: 0.25,
height: 1.4,
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400, // Match web client font-weight
letterSpacing: 0.4,
height: 1.4,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0.1,
height: 1.2,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0.5,
height: 1.2,
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700, // Match web client font-weight
letterSpacing: 0.5,
height: 1.2,
),
),
);
static ThemeData darkTheme = ThemeData(
useMaterial3: true,
// Exact font family matching web client with fallbacks
fontFamily: 'SF Compact Display',
colorScheme: const ColorScheme.dark(
primary: Color(0xFF4A90E2), // Lighter version of $highlight-blue for dark mode
secondary: Color(0xFFA78BFA), // Lighter secondary for dark mode
tertiary: Color(0xFF34D399), // Lighter accent for dark mode
surface: gray4, // Match web client $gray4 exactly
surfaceVariant: gray5, // Match web client $gray5 exactly
background: body, // Match web client $body exactly
onPrimary: Color(0xFF1C1C1C), // Dark text on light primary
onSecondary: Color(0xFF1C1C1C), // Dark text on light secondary
onTertiary: Color(0xFF1C1C1C), // Dark text on light accent
onSurface: white, // Match web client $white
onBackground: white, // Match web client $white
outline: gray3, // Match web client $gray4
outlineVariant: gray4, // Match web client $gray5
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: white,
),
cardTheme: CardThemeData(
elevation: 0, // Match web client flat design
color: gray4, // Match web client card background
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Match web client rounded-sm ($small)
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), // Consistent padding
elevation: 0, // Match web client flat design
backgroundColor: const Color(0xFF4A90E2), // Updated primary color for dark mode
foregroundColor: const Color(0xFF1C1C1C), // Dark text on light primary
textStyle: const TextStyle(
fontFamily: 'SF Compact Display',
fontWeight: FontWeight.w700, // Match web client font-weight
fontSize: 14, // Match web client font-size (0.9rem)
),
),
),
);
}
@@ -1,201 +0,0 @@
import 'package:flutter/material.dart';
import '../../data/models/album_model.dart';
import '../../core/constants/app_spacing.dart';
import '../../core/themes/app_theme.dart';
class AlbumCard extends StatefulWidget {
final AlbumModel album;
final VoidCallback? onTap;
final double? width;
final double? height;
const AlbumCard({
super.key,
required this.album,
this.onTap,
this.width,
this.height,
});
@override
State<AlbumCard> createState() => _AlbumCardState();
}
class _AlbumCardState extends State<AlbumCard> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: Card(
clipBehavior: Clip.antiAlias,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: AppBorderRadius.circularLG,
),
color: _isHovered ? AppTheme.gray5 : null, // Match web client hover background
child: InkWell(
onTap: onTap,
borderRadius: AppBorderRadius.circularLG,
child: Container(
width: width ?? 160,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Album Art
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: AppBorderRadius.circularLG,
),
child: ClipRRect(
borderRadius: AppBorderRadius.circularLG,
child: Stack(
children: [
if (album.image.isNotEmpty)
Image.network(
album.image,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAlbumArt(context);
},
)
else
_buildDefaultAlbumArt(context),
// Gradient overlay matching web client
Positioned.fill(
child: AnimatedOpacity(
opacity: _isHovered ? 1.0 : 0.0,
duration: AppSpacing.transitionNormal, // 0.25s ease from web client
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.6),
Colors.transparent,
],
stops: const [0.0, 0.8],
),
),
),
),
),
// Play button overlay matching web client PlayBtn.vue exactly
Positioned(
bottom: 12,
right: 12,
child: AnimatedContainer(
duration: AppSpacing.transitionNormal,
transform: Matrix4.translationValues(
0,
_isHovered ? 0 : 16, // translateY(1rem) = 16px
0,
),
child: AnimatedOpacity(
opacity: _isHovered ? 1.0 : 0.0,
duration: AppSpacing.transitionNormal,
child: Container(
width: 40, // 2.5rem = 40px
height: 40,
decoration: BoxDecoration(
color: AppTheme.darkBlue, // $darkblue exact match
shape: BoxShape.circle,
boxShadow: [
// Match web client shadow effects
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.play_arrow,
color: AppTheme.onPrimaryColor,
size: 28, // 1.75rem = 28px
),
),
),
),
),
],
),
),
),
),
// Album Info
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.displayTitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 15, // 0.95rem from web client
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
album.artistNames,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.75),
fontWeight: FontWeight.w700,
fontSize: 13, // 0.8rem from web client
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (album.year.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
album.year,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 12,
),
),
],
],
),
),
],
),
),
),
),
);
}
Widget _buildDefaultAlbumArt(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: Icon(
Icons.album,
size: 48,
color: Theme.of(context).colorScheme.onPrimary,
),
);
}
}
@@ -1,169 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
class MiniPlayer extends StatelessWidget {
const MiniPlayer({super.key});
@override
Widget build(BuildContext context) {
return Consumer<AudioProvider>(
builder: (context, audioProvider, child) {
final currentTrack = audioProvider.currentTrack;
if (currentTrack == null) {
return const SizedBox.shrink();
}
return Container(
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: InkWell(
onTap: () {
// Navigate to full player
Navigator.pushNamed(context, '/player');
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
// Album Art
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
child: currentTrack.image.isNotEmpty
? Image.network(
currentTrack.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultArt(context);
},
)
: _buildDefaultArt(context),
),
),
const SizedBox(width: 12),
// Track Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
currentTrack.displayTitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
currentTrack.artistNames,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
// Progress Indicator
if (audioProvider.duration.inMilliseconds > 0)
SizedBox(
width: 40,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${audioProvider.positionFormatted} / ${audioProvider.durationFormatted}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: audioProvider.progress,
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
),
const SizedBox(width: 8),
// Play/Pause Button
IconButton(
onPressed: () {
if (audioProvider.isPlaying) {
audioProvider.pause();
} else {
audioProvider.play();
}
},
icon: audioProvider.isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
)
: Icon(
audioProvider.isPlaying ? Icons.pause : Icons.play_arrow,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
);
},
);
}
Widget _buildDefaultArt(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: Icon(
Icons.music_note,
size: 24,
color: Theme.of(context).colorScheme.onPrimary,
),
);
}
}
@@ -1,141 +0,0 @@
import 'package:flutter/material.dart';
import '../../data/models/track_model.dart';
class TrackListTile extends StatelessWidget {
final TrackModel track;
final VoidCallback? onTap;
final VoidCallback? onPlay;
final bool isPlaying;
final bool showAlbumArt;
final Widget? trailing;
const TrackListTile({
super.key,
required this.track,
this.onTap,
this.onPlay,
this.isPlaying = false,
this.showAlbumArt = true,
this.trailing,
});
@override
Widget build(BuildContext context) {
return ListTile(
onTap: onTap,
dense: true,
leading: showAlbumArt
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
child: track.image.isNotEmpty
? Image.network(
track.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultArt(context);
},
)
: _buildDefaultArt(context),
),
)
: SizedBox(
width: 24,
child: Center(
child: isPlaying
? Icon(
Icons.equalizer,
size: 16,
color: Theme.of(context).colorScheme.primary,
)
: Text(
'${track.track}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
title: Text(
track.displayTitle,
style: TextStyle(
fontWeight: isPlaying ? FontWeight.w600 : FontWeight.normal,
color: isPlaying
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.artistNames,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (track.displayAlbum.isNotEmpty)
Text(
track.displayAlbum,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: trailing ??
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
track.durationFormatted,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
if (onPlay != null)
IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: Theme.of(context).colorScheme.primary,
),
onPressed: onPlay,
visualDensity: VisualDensity.compact,
),
],
),
);
}
Widget _buildDefaultArt(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: Icon(
Icons.music_note,
size: 24,
color: Theme.of(context).colorScheme.onPrimary,
),
);
}
}
@@ -1,236 +0,0 @@
import 'package:equatable/equatable.dart';
import 'track_model.dart';
class AlbumModel extends Equatable {
final List<ArtistModel> albumartists;
final String albumhash;
final List<String> artisthashes;
final String baseTitle;
final String color;
final int createdDate;
final int date;
final int duration;
final List<GenreModel> genres;
final List<String> genrehashes;
final String originalTitle;
final String title;
final int trackcount;
final int lastplayed;
final int playcount;
final int playduration;
final Map<String, dynamic> extra;
final String pathhash;
final int id;
final String type;
final String image;
final double score;
final List<String> versions;
final List<int> favUserids;
final String weakHash;
final bool isFavorite;
const AlbumModel({
required this.albumartists,
required this.albumhash,
required this.artisthashes,
required this.baseTitle,
this.color = '#6750A4',
required this.createdDate,
required this.date,
required this.duration,
required this.genres,
required this.genrehashes,
this.originalTitle = '',
required this.title,
required this.trackcount,
this.lastplayed = 0,
this.playcount = 0,
this.playduration = 0,
this.extra = const {},
this.pathhash = '',
this.id = -1,
this.type = 'album',
this.image = '',
this.score = 0.0,
this.versions = const [],
this.favUserids = const [],
this.weakHash = '',
this.isFavorite = false,
});
AlbumModel copyWith({
List<ArtistModel>? albumartists,
String? albumhash,
List<String>? artisthashes,
String? baseTitle,
String? color,
int? createdDate,
int? date,
int? duration,
List<GenreModel>? genres,
List<String>? genrehashes,
String? originalTitle,
String? title,
int? trackcount,
int? lastplayed,
int? playcount,
int? playduration,
Map<String, dynamic>? extra,
String? pathhash,
int? id,
String? type,
String? image,
double? score,
List<String>? versions,
List<int>? favUserids,
String? weakHash,
bool? isFavorite,
}) {
return AlbumModel(
albumartists: albumartists ?? this.albumartists,
albumhash: albumhash ?? this.albumhash,
artisthashes: artisthashes ?? this.artisthashes,
baseTitle: baseTitle ?? this.baseTitle,
color: color ?? this.color,
createdDate: createdDate ?? this.createdDate,
date: date ?? this.date,
duration: duration ?? this.duration,
genres: genres ?? this.genres,
genrehashes: genrehashes ?? this.genrehashes,
originalTitle: originalTitle ?? this.originalTitle,
title: title ?? this.title,
trackcount: trackcount ?? this.trackcount,
lastplayed: lastplayed ?? this.lastplayed,
playcount: playcount ?? this.playcount,
playduration: playduration ?? this.playduration,
extra: extra ?? this.extra,
pathhash: pathhash ?? this.pathhash,
id: id ?? this.id,
type: type ?? this.type,
image: image ?? this.image,
score: score ?? this.score,
versions: versions ?? this.versions,
favUserids: favUserids ?? this.favUserids,
weakHash: weakHash ?? this.weakHash,
isFavorite: isFavorite ?? this.isFavorite,
);
}
@override
List<Object?> get props => [
albumartists,
albumhash,
artisthashes,
baseTitle,
color,
createdDate,
date,
duration,
genres,
genrehashes,
originalTitle,
title,
trackcount,
lastplayed,
playcount,
playduration,
extra,
pathhash,
id,
type,
image,
score,
versions,
favUserids,
weakHash,
isFavorite,
];
String get displayTitle => originalTitle.isNotEmpty ? originalTitle : title;
String get artistNames => albumartists.map((artist) => artist.name).join(', ');
String get durationFormatted {
final hours = duration ~/ 3600;
final minutes = (duration % 3600) ~/ 60;
final seconds = duration % 60;
if (hours > 0) {
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
} else {
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}
String get year {
if (date == 0) return '';
final dateTime = DateTime.fromMillisecondsSinceEpoch(date * 1000);
return dateTime.year.toString();
}
factory AlbumModel.fromJson(Map<String, dynamic> json) {
return AlbumModel(
albumartists: (json['albumartists'] as List<dynamic>?)
?.map((artist) => ArtistModel.fromJson(artist))
.toList() ?? [],
albumhash: json['albumhash'] ?? '',
artisthashes: List<String>.from(json['artisthashes'] ?? []),
baseTitle: json['base_title'] ?? '',
color: json['color'] ?? '#6750A4',
createdDate: json['created_date'] ?? 0,
date: json['date'] ?? 0,
duration: json['duration'] ?? 0,
genres: (json['genres'] as List<dynamic>?)
?.map((genre) => GenreModel.fromJson(genre))
.toList() ?? [],
genrehashes: List<String>.from(json['genrehashes'] ?? []),
originalTitle: json['original_title'] ?? '',
title: json['title'] ?? '',
trackcount: json['trackcount'] ?? 0,
lastplayed: json['lastplayed'] ?? 0,
playcount: json['playcount'] ?? 0,
playduration: json['playduration'] ?? 0,
extra: json['extra'] ?? {},
pathhash: json['pathhash'] ?? '',
id: json['id'] ?? -1,
type: json['type'] ?? 'album',
image: json['image'] ?? '',
score: (json['score'] ?? 0).toDouble(),
versions: List<String>.from(json['versions'] ?? []),
favUserids: List<int>.from(json['fav_userids'] ?? []),
weakHash: json['weak_hash'] ?? '',
isFavorite: json['is_favorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'albumartists': albumartists.map((artist) => artist.toJson()).toList(),
'albumhash': albumhash,
'artisthashes': artisthashes,
'base_title': baseTitle,
'color': color,
'created_date': createdDate,
'date': date,
'duration': duration,
'genres': genres.map((genre) => genre.toJson()).toList(),
'genrehashes': genrehashes,
'original_title': originalTitle,
'title': title,
'trackcount': trackcount,
'lastplayed': lastplayed,
'playcount': playcount,
'playduration': playduration,
'extra': extra,
'pathhash': pathhash,
'id': id,
'type': type,
'image': image,
'score': score,
'versions': versions,
'fav_userids': favUserids,
'weak_hash': weakHash,
'is_favorite': isFavorite,
};
}
}
@@ -1,96 +0,0 @@
import 'package:equatable/equatable.dart';
import 'album_model.dart';
import 'track_model.dart';
class ArtistModel extends Equatable {
final String name;
final String artisthash;
final String image;
final int trackcount;
final int albumcount;
final int duration;
final int lastplayed;
final int playcount;
final int playduration;
final List<int> favUserids;
final bool isFavorite;
final List<AlbumModel> albums;
final List<TrackModel> tracks;
const ArtistModel({
required this.name,
required this.artisthash,
this.image = '',
this.trackcount = 0,
this.albumcount = 0,
this.duration = 0,
this.lastplayed = 0,
this.playcount = 0,
this.playduration = 0,
this.favUserids = const [],
this.isFavorite = false,
this.albums = const [],
this.tracks = const [],
});
ArtistModel copyWith({
String? name,
String? artisthash,
String? image,
int? trackcount,
int? albumcount,
int? duration,
int? lastplayed,
int? playcount,
int? playduration,
List<int>? favUserids,
bool? isFavorite,
List<AlbumModel>? albums,
List<TrackModel>? tracks,
}) {
return ArtistModel(
name: name ?? this.name,
artisthash: artisthash ?? this.artisthash,
image: image ?? this.image,
trackcount: trackcount ?? this.trackcount,
albumcount: albumcount ?? this.albumcount,
duration: duration ?? this.duration,
lastplayed: lastplayed ?? this.lastplayed,
playcount: playcount ?? this.playcount,
playduration: playduration ?? this.playduration,
favUserids: favUserids ?? this.favUserids,
isFavorite: isFavorite ?? this.isFavorite,
albums: albums ?? this.albums,
tracks: tracks ?? this.tracks,
);
}
@override
List<Object?> get props => [
name,
artisthash,
image,
trackcount,
albumcount,
duration,
lastplayed,
playcount,
playduration,
favUserids,
isFavorite,
albums,
tracks,
];
String get durationFormatted {
final hours = duration ~/ 3600;
final minutes = (duration % 3600) ~/ 60;
final seconds = duration % 60;
if (hours > 0) {
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
} else {
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}
}
@@ -1,123 +0,0 @@
import 'package:equatable/equatable.dart';
import 'track_model.dart';
class FolderModel extends Equatable {
final String name;
final String path;
final String? parent;
final int trackcount;
final List<FolderModel> subfolders;
final List<TrackModel> tracks;
final String? image;
final bool isFavorite;
const FolderModel({
required this.name,
required this.path,
this.parent,
this.trackcount = 0,
this.subfolders = const [],
this.tracks = const [],
this.image,
this.isFavorite = false,
});
FolderModel copyWith({
String? name,
String? path,
String? parent,
int? trackcount,
List<FolderModel>? subfolders,
List<TrackModel>? tracks,
String? image,
bool? isFavorite,
}) {
return FolderModel(
name: name ?? this.name,
path: path ?? this.path,
parent: parent ?? this.parent,
trackcount: trackcount ?? this.trackcount,
subfolders: subfolders ?? this.subfolders,
tracks: tracks ?? this.tracks,
image: image ?? this.image,
isFavorite: isFavorite ?? this.isFavorite,
);
}
factory FolderModel.fromJson(Map<String, dynamic> json) {
return FolderModel(
name: json['name'] ?? '',
path: json['path'] ?? '',
parent: json['parent'],
trackcount: json['trackcount'] ?? 0,
subfolders: (json['subfolders'] as List<dynamic>?)
?.map((folder) => FolderModel.fromJson(folder))
.toList() ?? [],
tracks: (json['tracks'] as List<dynamic>?)
?.map((track) => TrackModel.fromJson(track))
.toList() ?? [],
image: json['image'],
isFavorite: json['is_favorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'path': path,
'parent': parent,
'trackcount': trackcount,
'subfolders': subfolders.map((folder) => folder.toJson()).toList(),
'tracks': tracks.map((track) => track.toJson()).toList(),
'image': image,
'is_favorite': isFavorite,
};
}
@override
List<Object?> get props => [
name,
path,
parent,
trackcount,
subfolders,
tracks,
image,
isFavorite,
];
}
class FoldersAndTracksModel extends Equatable {
final List<FolderModel> folders;
final List<TrackModel> tracks;
final String currentPath;
const FoldersAndTracksModel({
required this.folders,
required this.tracks,
required this.currentPath,
});
factory FoldersAndTracksModel.fromJson(Map<String, dynamic> json) {
return FoldersAndTracksModel(
folders: (json['folders'] as List<dynamic>?)
?.map((folder) => FolderModel.fromJson(folder))
.toList() ?? [],
tracks: (json['tracks'] as List<dynamic>?)
?.map((track) => TrackModel.fromJson(track))
.toList() ?? [],
currentPath: json['current_path'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'folders': folders.map((folder) => folder.toJson()).toList(),
'tracks': tracks.map((track) => track.toJson()).toList(),
'current_path': currentPath,
};
}
@override
List<Object?> get props => [folders, tracks, currentPath];
}
@@ -1,152 +0,0 @@
import 'package:equatable/equatable.dart';
import 'track_model.dart';
class PlaylistModel extends Equatable {
final String id;
final String name;
final String description;
final String image;
final List<TrackModel> tracks;
final int trackcount;
final int duration;
final DateTime createdDate;
final DateTime lastModified;
final bool isPublic;
final bool isCollaborative;
final String owner;
final List<String> collaboratorIds;
final Map<String, dynamic> extra;
const PlaylistModel({
required this.id,
required this.name,
this.description = '',
this.image = '',
this.tracks = const [],
this.trackcount = 0,
this.duration = 0,
required this.createdDate,
required this.lastModified,
this.isPublic = false,
this.isCollaborative = false,
this.owner = '',
this.collaboratorIds = const [],
this.extra = const {},
});
PlaylistModel copyWith({
String? id,
String? name,
String? description,
String? image,
List<TrackModel>? tracks,
int? trackcount,
int? duration,
DateTime? createdDate,
DateTime? lastModified,
bool? isPublic,
bool? isCollaborative,
String? owner,
List<String>? collaboratorIds,
Map<String, dynamic>? extra,
}) {
return PlaylistModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
image: image ?? this.image,
tracks: tracks ?? this.tracks,
trackcount: trackcount ?? this.trackcount,
duration: duration ?? this.duration,
createdDate: createdDate ?? this.createdDate,
lastModified: lastModified ?? this.lastModified,
isPublic: isPublic ?? this.isPublic,
isCollaborative: isCollaborative ?? this.isCollaborative,
owner: owner ?? this.owner,
collaboratorIds: collaboratorIds ?? this.collaboratorIds,
extra: extra ?? this.extra,
);
}
@override
List<Object?> get props => [
id,
name,
description,
image,
tracks,
trackcount,
duration,
createdDate,
lastModified,
isPublic,
isCollaborative,
owner,
collaboratorIds,
extra,
];
String get durationFormatted {
final hours = duration ~/ 3600;
final minutes = (duration % 3600) ~/ 60;
final seconds = duration % 60;
if (hours > 0) {
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
} else {
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}
String get createdDateFormatted {
return '${createdDate.day.toString().padLeft(2, '0')}/${createdDate.month.toString().padLeft(2, '0')}/${createdDate.year}';
}
String get lastModifiedFormatted {
return '${lastModified.day.toString().padLeft(2, '0')}/${lastModified.month.toString().padLeft(2, '0')}/${lastModified.year}';
}
factory PlaylistModel.fromJson(Map<String, dynamic> json) {
return PlaylistModel(
id: json['id'] ?? '',
name: json['name'] ?? '',
description: json['description'] ?? '',
image: json['image'] ?? '',
tracks: (json['tracks'] as List<dynamic>?)
?.map((track) => TrackModel.fromJson(track))
.toList() ?? [],
trackcount: json['trackcount'] ?? 0,
duration: json['duration'] ?? 0,
createdDate: json['created_date'] != null
? DateTime.parse(json['created_date'])
: DateTime.now(),
lastModified: json['last_modified'] != null
? DateTime.parse(json['last_modified'])
: DateTime.now(),
isPublic: json['is_public'] ?? false,
isCollaborative: json['is_collaborative'] ?? false,
owner: json['owner'] ?? '',
collaboratorIds: List<String>.from(json['collaborator_ids'] ?? []),
extra: json['extra'] ?? {},
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'image': image,
'tracks': tracks.map((track) => track.toJson()).toList(),
'trackcount': trackcount,
'duration': duration,
'created_date': createdDate.toIso8601String(),
'last_modified': lastModified.toIso8601String(),
'is_public': isPublic,
'is_collaborative': isCollaborative,
'owner': owner,
'collaborator_ids': collaboratorIds,
'extra': extra,
};
}
}
@@ -1,153 +0,0 @@
import 'package:equatable/equatable.dart';
import 'album_model.dart';
import 'folder_model.dart';
import 'playlist_model.dart';
import 'track_model.dart';
class SearchResultsModel extends Equatable {
final List<TrackModel> tracks;
final List<AlbumModel> albums;
final List<ArtistModel> artists;
final List<FolderModel> folders;
final List<PlaylistModel> playlists;
const SearchResultsModel({
this.tracks = const [],
this.albums = const [],
this.artists = const [],
this.folders = const [],
this.playlists = const [],
});
SearchResultsModel copyWith({
List<TrackModel>? tracks,
List<AlbumModel>? albums,
List<ArtistModel>? artists,
List<FolderModel>? folders,
List<PlaylistModel>? playlists,
}) {
return SearchResultsModel(
tracks: tracks ?? this.tracks,
albums: albums ?? this.albums,
artists: artists ?? this.artists,
folders: folders ?? this.folders,
playlists: playlists ?? this.playlists,
);
}
factory SearchResultsModel.fromJson(Map<String, dynamic> json) {
return SearchResultsModel(
tracks: (json['tracks'] as List<dynamic>?)
?.map((track) => TrackModel.fromJson(track))
.toList() ?? [],
albums: (json['albums'] as List<dynamic>?)
?.map((album) => AlbumModel.fromJson(album))
.toList() ?? [],
artists: (json['artists'] as List<dynamic>?)
?.map((artist) => ArtistModel.fromJson(artist))
.toList() ?? [],
folders: (json['folders'] as List<dynamic>?)
?.map((folder) => FolderModel.fromJson(folder))
.toList() ?? [],
playlists: (json['playlists'] as List<dynamic>?)
?.map((playlist) => PlaylistModel.fromJson(playlist))
.toList() ?? [],
);
}
Map<String, dynamic> toJson() {
return {
'tracks': tracks.map((track) => track.toJson()).toList(),
'albums': albums.map((album) => album.toJson()).toList(),
'artists': artists.map((artist) => artist.toJson()).toList(),
'folders': folders.map((folder) => folder.toJson()).toList(),
'playlists': playlists.map((playlist) => playlist.toJson()).toList(),
};
}
bool get isEmpty =>
tracks.isEmpty &&
albums.isEmpty &&
artists.isEmpty &&
folders.isEmpty &&
playlists.isEmpty;
bool get isNotEmpty => !isEmpty;
@override
List<Object?> get props => [
tracks,
albums,
artists,
folders,
playlists,
];
}
class TopSearchResultsModel extends Equatable {
final List<TopResultItemModel> topResults;
final SearchResultsModel allResults;
const TopSearchResultsModel({
required this.topResults,
required this.allResults,
});
factory TopSearchResultsModel.fromJson(Map<String, dynamic> json) {
return TopSearchResultsModel(
topResults: (json['top_results'] as List<dynamic>?)
?.map((item) => TopResultItemModel.fromJson(item))
.toList() ?? [],
allResults: SearchResultsModel.fromJson(json['all_results'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'top_results': topResults.map((item) => item.toJson()).toList(),
'all_results': allResults.toJson(),
};
}
@override
List<Object?> get props => [topResults, allResults];
}
class TopResultItemModel extends Equatable {
final String type; // 'track', 'album', 'artist', 'folder', 'playlist'
final String title;
final String subtitle;
final String? image;
final dynamic data; // The actual model object
const TopResultItemModel({
required this.type,
required this.title,
required this.subtitle,
this.image,
this.data,
});
factory TopResultItemModel.fromJson(Map<String, dynamic> json) {
return TopResultItemModel(
type: json['type'] ?? '',
title: json['title'] ?? '',
subtitle: json['subtitle'] ?? '',
image: json['image'],
data: json['data'],
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'title': title,
'subtitle': subtitle,
'image': image,
'data': data,
};
}
@override
List<Object?> get props => [type, title, subtitle, image, data];
}
@@ -1,15 +0,0 @@
class SearchSuggestion {
final String id;
final String title;
final String? imageUrl;
final String type; // 'track', 'album', 'artist', 'playlist'
final dynamic data;
SearchSuggestion({
required this.id,
required this.title,
this.imageUrl,
required this.type,
this.data,
});
}
@@ -1,316 +0,0 @@
import 'package:equatable/equatable.dart';
class TrackModel extends Equatable {
final int id;
final String title;
final String album;
final String originalTitle;
final String albumhash;
final String originalAlbum;
final List<ArtistModel> artists;
final List<ArtistModel> albumartists;
final List<String> artisthashes;
final int track;
final int disc;
final int duration;
final int bitrate;
final String filepath;
final String folder;
final List<GenreModel> genres;
final List<String> genrehashes;
final String copyright;
final int date;
final int lastModified;
final String trackhash;
final String image;
final String weakHash;
final Map<String, dynamic> extra;
final int lastplayed;
final int playcount;
final int playduration;
final bool explicit;
final List<int> favUserids;
final bool isFavorite;
final double score;
const TrackModel({
required this.id,
required this.title,
required this.album,
this.originalTitle = '',
required this.albumhash,
this.originalAlbum = '',
required this.artists,
required this.albumartists,
required this.artisthashes,
required this.track,
required this.disc,
required this.duration,
required this.bitrate,
required this.filepath,
required this.folder,
required this.genres,
required this.genrehashes,
this.copyright = '',
required this.date,
required this.lastModified,
required this.trackhash,
this.image = '',
this.weakHash = '',
required this.extra,
this.lastplayed = 0,
this.playcount = 0,
this.playduration = 0,
this.explicit = false,
this.favUserids = const [],
this.isFavorite = false,
this.score = 0.0,
});
TrackModel copyWith({
int? id,
String? title,
String? album,
String? originalTitle,
String? albumhash,
String? originalAlbum,
List<ArtistModel>? artists,
List<ArtistModel>? albumartists,
List<String>? artisthashes,
int? track,
int? disc,
int? duration,
int? bitrate,
String? filepath,
String? folder,
List<GenreModel>? genres,
List<String>? genrehashes,
String? copyright,
int? date,
int? lastModified,
String? trackhash,
String? image,
String? weakHash,
Map<String, dynamic>? extra,
int? lastplayed,
int? playcount,
int? playduration,
bool? explicit,
List<int>? favUserids,
bool? isFavorite,
double? score,
}) {
return TrackModel(
id: id ?? this.id,
title: title ?? this.title,
album: album ?? this.album,
originalTitle: originalTitle ?? this.originalTitle,
albumhash: albumhash ?? this.albumhash,
originalAlbum: originalAlbum ?? this.originalAlbum,
artists: artists ?? this.artists,
albumartists: albumartists ?? this.albumartists,
artisthashes: artisthashes ?? this.artisthashes,
track: track ?? this.track,
disc: disc ?? this.disc,
duration: duration ?? this.duration,
bitrate: bitrate ?? this.bitrate,
filepath: filepath ?? this.filepath,
folder: folder ?? this.folder,
genres: genres ?? this.genres,
genrehashes: genrehashes ?? this.genrehashes,
copyright: copyright ?? this.copyright,
date: date ?? this.date,
lastModified: lastModified ?? this.lastModified,
trackhash: trackhash ?? this.trackhash,
image: image ?? this.image,
weakHash: weakHash ?? this.weakHash,
extra: extra ?? this.extra,
lastplayed: lastplayed ?? this.lastplayed,
playcount: playcount ?? this.playcount,
playduration: playduration ?? this.playduration,
explicit: explicit ?? this.explicit,
favUserids: favUserids ?? this.favUserids,
isFavorite: isFavorite ?? this.isFavorite,
score: score ?? this.score,
);
}
@override
List<Object?> get props => [
id,
title,
album,
originalTitle,
albumhash,
originalAlbum,
artists,
albumartists,
artisthashes,
track,
disc,
duration,
bitrate,
filepath,
folder,
genres,
genrehashes,
copyright,
date,
lastModified,
trackhash,
image,
weakHash,
extra,
lastplayed,
playcount,
playduration,
explicit,
favUserids,
isFavorite,
score,
];
String get displayTitle => originalTitle.isNotEmpty ? originalTitle : title;
String get displayAlbum => originalAlbum.isNotEmpty ? originalAlbum : album;
String get artistNames => artists.map((artist) => artist.name).join(', ');
String get durationFormatted {
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
factory TrackModel.fromJson(Map<String, dynamic> json) {
return TrackModel(
id: json['id'] ?? 0,
title: json['title'] ?? '',
album: json['album'] ?? '',
originalTitle: json['original_title'] ?? '',
albumhash: json['albumhash'] ?? '',
originalAlbum: json['original_album'] ?? '',
artists: (json['artists'] as List<dynamic>?)
?.map((artist) => ArtistModel.fromJson(artist))
.toList() ?? [],
albumartists: (json['albumartists'] as List<dynamic>?)
?.map((artist) => ArtistModel.fromJson(artist))
.toList() ?? [],
artisthashes: List<String>.from(json['artisthashes'] ?? []),
track: json['track'] ?? 0,
disc: json['disc'] ?? 1,
duration: json['duration'] ?? 0,
bitrate: json['bitrate'] ?? 0,
filepath: json['filepath'] ?? '',
folder: json['folder'] ?? '',
genres: (json['genres'] as List<dynamic>?)
?.map((genre) => GenreModel.fromJson(genre))
.toList() ?? [],
genrehashes: List<String>.from(json['genrehashes'] ?? []),
copyright: json['copyright'] ?? '',
date: json['date'] ?? 0,
lastModified: json['last_modified'] ?? 0,
trackhash: json['trackhash'] ?? '',
image: json['image'] ?? '',
weakHash: json['weak_hash'] ?? '',
extra: json['extra'] ?? {},
lastplayed: json['lastplayed'] ?? 0,
playcount: json['playcount'] ?? 0,
playduration: json['playduration'] ?? 0,
explicit: json['explicit'] ?? false,
favUserids: List<int>.from(json['fav_userids'] ?? []),
isFavorite: json['is_favorite'] ?? false,
score: (json['score'] ?? 0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'album': album,
'original_title': originalTitle,
'albumhash': albumhash,
'original_album': originalAlbum,
'artists': artists.map((artist) => artist.toJson()).toList(),
'albumartists': albumartists.map((artist) => artist.toJson()).toList(),
'artisthashes': artisthashes,
'track': track,
'disc': disc,
'duration': duration,
'bitrate': bitrate,
'filepath': filepath,
'folder': folder,
'genres': genres.map((genre) => genre.toJson()).toList(),
'genrehashes': genrehashes,
'copyright': copyright,
'date': date,
'last_modified': lastModified,
'trackhash': trackhash,
'image': image,
'weak_hash': weakHash,
'extra': extra,
'lastplayed': lastplayed,
'playcount': playcount,
'playduration': playduration,
'explicit': explicit,
'fav_userids': favUserids,
'is_favorite': isFavorite,
'score': score,
};
}
}
class ArtistModel extends Equatable {
final String name;
final String artisthash;
const ArtistModel({
required this.name,
required this.artisthash,
});
factory ArtistModel.fromJson(Map<String, dynamic> json) {
return ArtistModel(
name: json['name'] ?? '',
artisthash: json['artisthash'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'artisthash': artisthash,
};
}
@override
List<Object?> get props => [name, artisthash];
}
class GenreModel extends Equatable {
final String name;
final String genrehash;
const GenreModel({
required this.name,
required this.genrehash,
});
factory GenreModel.fromJson(Map<String, dynamic> json) {
return GenreModel(
name: json['name'] ?? '',
genrehash: json['genrehash'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'genrehash': genrehash,
};
}
@override
List<Object?> get props => [name, genrehash];
}
@@ -1,283 +0,0 @@
import 'package:dio/dio.dart';
import '../../core/constants/app_constants.dart';
class ApiService {
late Dio _dio;
final String baseUrl;
ApiService({String? baseUrl}) : baseUrl = baseUrl ?? AppConstants.defaultApiUrl {
_dio = Dio(BaseOptions(
baseUrl: baseUrl ?? AppConstants.defaultApiUrl,
connectTimeout: AppConstants.apiTimeout,
receiveTimeout: AppConstants.apiTimeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) {
// print(obj); // Enable for debugging
},
));
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
// Handle common errors
String errorMessage = AppConstants.genericErrorMessage;
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
errorMessage = AppConstants.networkErrorMessage;
} else if (error.response?.statusCode == 500) {
errorMessage = AppConstants.serverErrorMessage;
} else if (error.response?.statusCode == 401) {
errorMessage = AppConstants.authErrorMessage;
}
// You could emit this through a state management solution
// For now, just log it
print('API Error: $errorMessage');
handler.next(error);
},
));
}
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
void clearAuthToken() {
_dio.options.headers.remove('Authorization');
}
// Tracks API
Future<List<dynamic>> getTracks({int limit = 20, int offset = 0}) async {
try {
final response = await _dio.get('/tracks', queryParameters: {
'limit': limit,
'offset': offset,
});
return response.data['tracks'] ?? [];
} catch (e) {
throw Exception('Failed to load tracks: $e');
}
}
Future<dynamic> getTrack(String trackhash) async {
try {
final response = await _dio.get('/track/$trackhash');
return response.data['track'];
} catch (e) {
throw Exception('Failed to load track: $e');
}
}
Future<List<dynamic>> searchTracks(String query, {int limit = 15}) async {
try {
final response = await _dio.get('/search/tracks', queryParameters: {
'q': query,
'limit': limit,
});
return response.data['tracks'] ?? [];
} catch (e) {
throw Exception('Failed to search tracks: $e');
}
}
// Albums API
Future<List<dynamic>> getAlbums({int limit = 20, int offset = 0}) async {
try {
final response = await _dio.get('/albums', queryParameters: {
'limit': limit,
'offset': offset,
});
return response.data['albums'] ?? [];
} catch (e) {
throw Exception('Failed to load albums: $e');
}
}
Future<dynamic> getAlbum(String albumhash) async {
try {
final response = await _dio.get('/album/$albumhash');
return response.data['album'];
} catch (e) {
throw Exception('Failed to load album: $e');
}
}
Future<List<dynamic>> getAlbumTracks(String albumhash) async {
try {
final response = await _dio.get('/album/$albumhash/tracks');
return response.data['tracks'] ?? [];
} catch (e) {
throw Exception('Failed to load album tracks: $e');
}
}
// Artists API
Future<List<dynamic>> getArtists({int limit = 20, int offset = 0}) async {
try {
final response = await _dio.get('/artists', queryParameters: {
'limit': limit,
'offset': offset,
});
return response.data['artists'] ?? [];
} catch (e) {
throw Exception('Failed to load artists: $e');
}
}
Future<dynamic> getArtist(String artisthash) async {
try {
final response = await _dio.get('/artist/$artisthash');
return response.data['artist'];
} catch (e) {
throw Exception('Failed to load artist: $e');
}
}
Future<List<dynamic>> getArtistAlbums(String artisthash) async {
try {
final response = await _dio.get('/artist/$artisthash/albums');
return response.data['albums'] ?? [];
} catch (e) {
throw Exception('Failed to load artist albums: $e');
}
}
Future<List<dynamic>> getArtistTracks(String artisthash) async {
try {
final response = await _dio.get('/artist/$artisthash/tracks');
return response.data['tracks'] ?? [];
} catch (e) {
throw Exception('Failed to load artist tracks: $e');
}
}
// Playlists API
Future<List<dynamic>> getPlaylists() async {
try {
final response = await _dio.get('/playlists');
return response.data['playlists'] ?? [];
} catch (e) {
throw Exception('Failed to load playlists: $e');
}
}
Future<dynamic> getPlaylist(String playlistId) async {
try {
final response = await _dio.get('/playlist/$playlistId');
return response.data['playlist'];
} catch (e) {
throw Exception('Failed to load playlist: $e');
}
}
Future<dynamic> createPlaylist(String name, {String description = ''}) async {
try {
final response = await _dio.post('/playlists', data: {
'name': name,
'description': description,
});
return response.data['playlist'];
} catch (e) {
throw Exception('Failed to create playlist: $e');
}
}
Future<void> addToPlaylist(String playlistId, String trackhash) async {
try {
await _dio.post('/playlist/$playlistId/add', data: {
'trackhash': trackhash,
});
} catch (e) {
throw Exception('Failed to add to playlist: $e');
}
}
Future<void> removeFromPlaylist(String playlistId, String trackhash) async {
try {
await _dio.delete('/playlist/$playlistId/remove', data: {
'trackhash': trackhash,
});
} catch (e) {
throw Exception('Failed to remove from playlist: $e');
}
}
// Favorites API
Future<void> toggleFavoriteTrack(String trackhash) async {
try {
await _dio.post('/favorites/track/toggle', data: {
'trackhash': trackhash,
});
} catch (e) {
throw Exception('Failed to toggle favorite track: $e');
}
}
Future<void> toggleFavoriteAlbum(String albumhash) async {
try {
await _dio.post('/favorites/album/toggle', data: {
'albumhash': albumhash,
});
} catch (e) {
throw Exception('Failed to toggle favorite album: $e');
}
}
Future<void> toggleFavoriteArtist(String artisthash) async {
try {
await _dio.post('/favorites/artist/toggle', data: {
'artisthash': artisthash,
});
} catch (e) {
throw Exception('Failed to toggle favorite artist: $e');
}
}
Future<List<dynamic>> getFavoriteTracks() async {
try {
final response = await _dio.get('/favorites/tracks');
return response.data['tracks'] ?? [];
} catch (e) {
throw Exception('Failed to load favorite tracks: $e');
}
}
Future<List<dynamic>> getFavoriteAlbums() async {
try {
final response = await _dio.get('/favorites/albums');
return response.data['albums'] ?? [];
} catch (e) {
throw Exception('Failed to load favorite albums: $e');
}
}
Future<List<dynamic>> getFavoriteArtists() async {
try {
final response = await _dio.get('/favorites/artists');
return response.data['artists'] ?? [];
} catch (e) {
throw Exception('Failed to load favorite artists: $e');
}
}
}
@@ -1,403 +0,0 @@
import 'dart:async';
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';
import '../models/track_model.dart';
import '../../core/enums/playback_mode.dart';
class AudioService {
static final AudioService _instance = AudioService._internal();
factory AudioService() => _instance;
AudioService._internal();
late AudioPlayer _audioPlayer;
late AudioSession _audioSession;
// Playback state
TrackModel? _currentTrack;
bool _isPlaying = false;
bool _isLoading = false;
bool _isBuffering = false;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
double _volume = 1.0;
// Playback modes
RepeatMode _repeatMode = RepeatMode.off;
ShuffleMode _shuffleMode = ShuffleMode.off;
double _playbackSpeed = 1.0;
// Playlist
List<TrackModel> _queue = [];
int _currentIndex = 0;
bool _isShuffleMode = false;
bool _isRepeatMode = false;
// Error handling
String? _errorMessage;
void _setError(String error) {
_errorMessage = error;
_errorController.add(_errorMessage);
print('Audio Error: $error');
}
void _clearError() {
if (_errorMessage != null) {
_errorMessage = null;
_errorController.add(_errorMessage);
}
}
// Stream controllers
final _positionController = StreamController<Duration>.broadcast();
final _durationController = StreamController<Duration>.broadcast();
final _playingStateController = StreamController<bool>.broadcast();
final _currentTrackController = StreamController<TrackModel?>.broadcast();
final _queueController = StreamController<List<TrackModel>>.broadcast();
final _bufferingController = StreamController<bool>.broadcast();
final _errorController = StreamController<String?>.broadcast();
final _repeatModeController = StreamController<RepeatMode>.broadcast();
final _shuffleModeController = StreamController<ShuffleMode>.broadcast();
// Getters
TrackModel? get currentTrack => _currentTrack;
bool get isPlaying => _isPlaying;
bool get isPaused => !_isPlaying && _currentTrack != null;
bool get isLoading => _isLoading;
bool get isBuffering => _isBuffering;
Duration get position => _position;
Duration get duration => _duration;
double get volume => _volume;
List<TrackModel> get queue => _queue;
int get currentIndex => _currentIndex;
bool get isShuffleMode => _isShuffleMode;
bool get isRepeatMode => _isRepeatMode;
RepeatMode get repeatMode => _repeatMode;
ShuffleMode get shuffleMode => _shuffleMode;
double get playbackSpeed => _playbackSpeed;
String? get errorMessage => _errorMessage;
// Playback state helpers
bool get hasError => _errorMessage != null;
bool get canPlay => _currentTrack != null && !hasError;
bool get canPause => _isPlaying && !hasError;
bool get canGoNext => _queue.isNotEmpty && _currentIndex < _queue.length - 1;
bool get canGoPrevious => _queue.isNotEmpty && _currentIndex > 0;
// Streams
Stream<Duration> get positionStream => _positionController.stream;
Stream<Duration> get durationStream => _durationController.stream;
Stream<bool> get playingStateStream => _playingStateController.stream;
Stream<TrackModel?> get currentTrackStream => _currentTrackController.stream;
Stream<List<TrackModel>> get queueStream => _queueController.stream;
Stream<bool> get bufferingStream => _bufferingController.stream;
Stream<String?> get errorStream => _errorController.stream;
Stream<RepeatMode> get repeatModeStream => _repeatModeController.stream;
Stream<ShuffleMode> get shuffleModeStream => _shuffleModeController.stream;
Future<void> initialize() async {
try {
_audioPlayer = AudioPlayer();
_audioSession = await AudioSession.instance;
// Configure audio session
await _audioSession.configure(const AudioSessionConfiguration.music());
// Set up listeners
_audioPlayer.positionStream.listen((position) {
_position = position;
_positionController.add(position);
});
_audioPlayer.durationStream.listen((duration) {
_duration = duration ?? Duration.zero;
_durationController.add(_duration);
});
_audioPlayer.playerStateStream.listen((state) {
_isPlaying = state.playing;
_playingStateController.add(_isPlaying);
});
// Handle player completion
_audioPlayer.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
_playNext();
}
// Handle buffering state
_isBuffering = state.processingState == ProcessingState.buffering ||
state.processingState == ProcessingState.loading;
_bufferingController.add(_isBuffering);
});
// Handle player errors
_audioPlayer.playerStateStream.listen((state) {
if (state.playing && _errorMessage != null) {
_clearError();
}
});
print('Audio service initialized successfully');
} catch (e) {
throw Exception('Failed to initialize audio service: $e');
}
}
Future<void> loadTrack(TrackModel track) async {
try {
_clearError();
_isLoading = true;
_isBuffering = true;
_currentTrack = track;
_currentTrackController.add(_currentTrack);
_bufferingController.add(_isBuffering);
// Create audio source from track filepath
final uri = Uri.parse(track.filepath);
await _audioPlayer.setAudioSource(AudioSource.uri(uri));
_isLoading = false;
_isBuffering = false;
_bufferingController.add(_isBuffering);
print('Track loaded: ${track.title}');
} catch (e) {
_isLoading = false;
_isBuffering = false;
_bufferingController.add(_isBuffering);
_setError('Failed to load track: $e');
throw Exception('Failed to load track: $e');
}
}
Future<void> play() async {
try {
_clearError();
if (_currentTrack != null && !hasError) {
await _audioPlayer.play();
_isPlaying = true;
_playingStateController.add(_isPlaying);
print('Playing: ${_currentTrack?.title}');
}
} catch (e) {
_setError('Failed to play: $e');
throw Exception('Failed to play: $e');
}
}
Future<void> pause() async {
try {
await _audioPlayer.pause();
_isPlaying = false;
_playingStateController.add(_isPlaying);
print('Paused: ${_currentTrack?.title}');
} catch (e) {
_setError('Failed to pause: $e');
throw Exception('Failed to pause: $e');
}
}
Future<void> stop() async {
try {
await _audioPlayer.stop();
_isPlaying = false;
_position = Duration.zero;
_playingStateController.add(_isPlaying);
_positionController.add(_position);
_clearError();
print('Stopped: ${_currentTrack?.title}');
} catch (e) {
_setError('Failed to stop: $e');
throw Exception('Failed to stop: $e');
}
}
Future<void> seekTo(Duration position) async {
try {
await _audioPlayer.seek(position);
_position = position;
_positionController.add(_position);
} catch (e) {
throw Exception('Failed to seek: $e');
}
}
Future<void> setVolume(double volume) async {
try {
await _audioPlayer.setVolume(volume);
} catch (e) {
throw Exception('Failed to set volume: $e');
}
}
Future<void> setSpeed(double speed) async {
try {
await _audioPlayer.setSpeed(speed);
} catch (e) {
throw Exception('Failed to set speed: $e');
}
}
// Queue management
void setQueue(List<TrackModel> tracks) {
_queue = List.from(tracks);
_currentIndex = 0;
_queueController.add(_queue);
if (_queue.isNotEmpty && _currentTrack == null) {
loadTrack(_queue[_currentIndex]);
}
}
void addToQueue(TrackModel track) {
_queue.add(track);
_queueController.add(_queue);
}
void removeFromQueue(int index) {
if (index < _queue.length) {
_queue.removeAt(index);
if (index < _currentIndex) {
_currentIndex--;
} else if (index == _currentIndex) {
if (_currentIndex >= _queue.length) {
_currentIndex = _queue.length - 1;
}
loadTrack(_queue[_currentIndex]);
}
_queueController.add(_queue);
}
}
void clearQueue() {
_queue.clear();
_currentIndex = 0;
_queueController.add(_queue);
}
Future<void> playNext() async {
if (_queue.isNotEmpty) {
_playNext();
}
}
Future<void> playPrevious() async {
if (_queue.isNotEmpty) {
if (_currentIndex > 0) {
_currentIndex--;
await loadTrack(_queue[_currentIndex]);
await play();
} else if (_repeatMode == RepeatMode.all) {
// Loop to last track
_currentIndex = _queue.length - 1;
await loadTrack(_queue[_currentIndex]);
await play();
} else {
// Restart current track if at beginning
await seekTo(Duration.zero);
await play();
}
}
}
void _playNext() {
if (_repeatMode == RepeatMode.one) {
// Repeat current track
loadTrack(_queue[_currentIndex]);
play();
} else if (_shuffleMode == ShuffleMode.on) {
// Play random track
if (_queue.isNotEmpty) {
_currentIndex = (_currentIndex + 1) % _queue.length;
loadTrack(_queue[_currentIndex]);
play();
}
} else {
// Play next track in order
if (_currentIndex < _queue.length - 1) {
_currentIndex++;
loadTrack(_queue[_currentIndex]);
play();
} else if (_repeatMode == RepeatMode.all) {
// Loop back to first track
_currentIndex = 0;
loadTrack(_queue[_currentIndex]);
play();
} else {
// End of queue
stop();
}
}
}
void jumpToIndex(int index) {
if (index >= 0 && index < _queue.length) {
_currentIndex = index;
loadTrack(_queue[_currentIndex]);
}
}
// Playback modes
void toggleShuffle() {
_shuffleMode = _shuffleMode.toggle();
_shuffleModeController.add(_shuffleMode);
if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) {
// Shuffle the queue while maintaining current track
final currentTrack = _queue[_currentIndex];
_queue.shuffle();
_currentIndex = _queue.indexOf(currentTrack);
_queueController.add(_queue);
}
}
void toggleRepeat() {
_repeatMode = _repeatMode.next();
_repeatModeController.add(_repeatMode);
}
void setShuffleMode(bool enabled) {
_shuffleMode = enabled ? ShuffleMode.on : ShuffleMode.off;
_shuffleModeController.add(_shuffleMode);
if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) {
// Shuffle the queue while maintaining current track
final currentTrack = _queue[_currentIndex];
_queue.shuffle();
_currentIndex = _queue.indexOf(currentTrack);
_queueController.add(_queue);
}
}
void setRepeatMode(RepeatMode mode) {
_repeatMode = mode;
_repeatModeController.add(_repeatMode);
}
// Utility methods
String get positionFormatted {
final minutes = _position.inMinutes;
final seconds = _position.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
String get durationFormatted {
final minutes = _duration.inMinutes;
final seconds = _duration.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
double get progress {
if (_duration.inMilliseconds == 0) return 0.0;
return _position.inMilliseconds / _duration.inMilliseconds;
}
Future<void> dispose() async {
await _positionController.close();
await _durationController.close();
await _playingStateController.close();
await _currentTrackController.close();
await _queueController.close();
await _audioPlayer.dispose();
}
}
@@ -1,294 +0,0 @@
import 'package:dio/dio.dart';
import 'package:path/path.dart';
class DownloadService {
late Dio _dio;
final String baseUrl;
final String _downloadPath;
DownloadService({String? baseUrl, String? downloadPath})
: baseUrl = baseUrl ?? 'https://your-server.com',
_downloadPath = downloadPath ?? '/storage/emulated/0/Android/data/com.example.swingmusic/files/Downloads';
DownloadService() {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) {
print('Download API: $obj');
},
));
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
String errorMessage = 'Download failed';
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
errorMessage = 'Network timeout - please check your connection';
} else if (error.response?.statusCode == 404) {
errorMessage = 'Download not found';
} else if (error.response?.statusCode == 500) {
errorMessage = 'Server error - please try again later';
} else if (error.response?.statusCode == 503) {
errorMessage = 'Service unavailable - downloads are disabled';
}
print('Download Error: $errorMessage');
handler.next(error);
},
));
}
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
Future<Map<String, dynamic>> getDownloads() async {
try {
final response = await _dio.get('/downloads');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>? ?? {};
} else {
return {};
}
} catch (e) {
print('Error fetching downloads: $e');
return {};
}
}
Future<Map<String, dynamic>?> getDownload(String downloadId) async {
try {
final response = await _dio.get('/download/$downloadId');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>?;
} else {
return null;
}
} catch (e) {
print('Error fetching download: $e');
return null;
}
}
Future<String> downloadTrack(String trackHash, {
String quality = '320kbps',
bool wifiOnly = false,
}) async {
try {
final response = await _dio.post('/download/track', data: {
'trackHash': trackHash,
'quality': quality,
'wifiOnly': wifiOnly,
});
if (response.statusCode == 200) {
final data = response.data;
return data['downloadId'] as String? ?? '';
} else {
throw Exception('Failed to start download');
}
} catch (e) {
throw Exception('Failed to start download: $e');
}
}
Future<String> downloadAlbum(String albumHash, {
String quality = '320kbps',
bool wifiOnly = false,
}) async {
try {
final response = await _dio.post('/download/album', data: {
'albumHash': albumHash,
'quality': quality,
'wifiOnly': wifiOnly,
});
if (response.statusCode == 200) {
final data = response.data;
return data['downloadId'] as String? ?? '';
} else {
throw Exception('Failed to start download');
}
} catch (e) {
throw Exception('Failed to start download: $e');
}
}
Future<String> downloadArtist(String artistHash, {
String quality = '320kbps',
bool wifiOnly = false,
}) async {
try {
final response = await _dio.post('/download/artist', data: {
'artistHash': artistHash,
'quality': quality,
'wifiOnly': wifiOnly,
});
if (response.statusCode == 200) {
final data = response.data;
return data['downloadId'] as String? ?? '';
} else {
throw Exception('Failed to start download');
}
} catch (e) {
throw Exception('Failed to start download: $e');
}
}
Future<String> downloadPlaylist(String playlistId, {
String quality = '320kbps',
bool wifiOnly = false,
}) async {
try {
final response = await _dio.post('/download/playlist', data: {
'playlistId': playlistId,
'quality': quality,
'wifiOnly': wifiOnly,
});
if (response.statusCode == 200) {
final data = response.data;
return data['downloadId'] as String? ?? '';
} else {
throw Exception('Failed to start download');
}
} catch (e) {
throw Exception('Failed to start download: $e');
}
}
Future<bool> pauseDownload(String downloadId) async {
try {
final response = await _dio.post('/download/$downloadId/pause');
return response.statusCode == 200;
} catch (e) {
print('Error pausing download: $e');
return false;
}
}
Future<bool> resumeDownload(String downloadId) async {
try {
final response = await _dio.post('/download/$downloadId/resume');
return response.statusCode == 200;
} catch (e) {
print('Error resuming download: $e');
return false;
}
}
Future<bool> cancelDownload(String downloadId) async {
try {
final response = await _dio.post('/download/$downloadId/cancel');
return response.statusCode == 200;
} catch (e) {
print('Error canceling download: $e');
return false;
}
}
Future<bool> deleteDownload(String downloadId) async {
try {
final response = await _dio.delete('/download/$downloadId');
return response.statusCode == 200;
} catch (e) {
print('Error deleting download: $e');
return false;
}
}
Future<Map<String, dynamic>> getDownloadStats() async {
try {
final response = await _dio.get('/download/stats');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>? ?? {};
} else {
return {};
}
} catch (e) {
print('Error fetching download stats: $e');
return {};
}
}
Future<String> getDownloadPath() async {
// TODO: Implement actual path resolution
return _downloadPath;
}
Future<bool> updateDownloadSettings({
String? downloadPath,
String? defaultQuality,
bool? wifiOnly,
int? maxConcurrentDownloads,
}) async {
try {
final response = await _dio.post('/download/settings', data: {
if (downloadPath != null) 'downloadPath': downloadPath,
if (defaultQuality != null) 'defaultQuality': defaultQuality,
if (wifiOnly != null) 'wifiOnly': wifiOnly,
if (maxConcurrentDownloads != null) 'maxConcurrentDownloads': maxConcurrentDownloads,
});
return response.statusCode == 200;
} catch (e) {
print('Error updating download settings: $e');
return false;
}
}
Future<Map<String, dynamic>> getDownloadSettings() async {
try {
final response = await _dio.get('/download/settings');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>? ?? {};
} else {
return {};
}
} catch (e) {
print('Error fetching download settings: $e');
return {};
}
}
Stream<Map<String, dynamic>> watchDownloadProgress(String downloadId) {
// TODO: Implement WebSocket or SSE for real-time progress updates
// For now, return periodic polling
return Stream.periodic(const Duration(seconds: 1), (count) async {
final download = await getDownload(downloadId);
if (download != null) {
return {
'downloadId': downloadId,
'progress': download['progress'] ?? 0.0,
'status': download['status'] ?? 'unknown',
'speed': download['speed'] ?? 0.0,
'eta': download['eta'] ?? 0,
};
}
return {};
});
}
}
@@ -1,545 +0,0 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/track_model.dart';
import '../models/album_model.dart';
import '../models/artist_model.dart';
import '../models/playlist_model.dart';
import '../models/search_suggestion_model.dart';
import '../../core/constants/app_constants.dart';
class EnhancedApiService {
late Dio _dio;
final String baseUrl;
final SharedPreferences _prefs;
EnhancedApiService({String? baseUrl}) : baseUrl = baseUrl ?? AppConstants.defaultApiUrl {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: AppConstants.apiTimeout,
receiveTimeout: AppConstants.apiTimeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_prefs = SharedPreferences.getInstance() as Future<SharedPreferences>;
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) {
print('API: $obj');
},
));
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
String errorMessage = AppConstants.genericErrorMessage;
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
errorMessage = AppConstants.networkErrorMessage;
} else if (error.response?.statusCode == 500) {
errorMessage = AppConstants.serverErrorMessage;
} else if (error.response?.statusCode == 401) {
errorMessage = AppConstants.authErrorMessage;
} else if (error.response?.statusCode == 404) {
errorMessage = 'Resource not found';
}
print('API Error: $errorMessage');
handler.next(error);
},
));
}
// Authentication methods
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
void clearAuthToken() {
_dio.options.headers.remove('Authorization');
}
// Track methods
Future<List<TrackModel>> getTracks({
int limit = 20,
int offset = 0,
String? search,
String? genre,
String? artist,
String? album,
String? folder,
}) async {
try {
Map<String, dynamic> queryParams = {
'limit': limit,
'offset': offset,
};
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
if (genre != null && genre.isNotEmpty) {
queryParams['genre'] = genre;
}
if (artist != null && artist.isNotEmpty) {
queryParams['artist'] = artist;
}
if (album != null && album.isNotEmpty) {
queryParams['album'] = album;
}
if (folder != null && folder.isNotEmpty) {
queryParams['folder'] = folder;
}
final response = await _dio.get('/tracks', queryParameters: queryParams);
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to load tracks: $e');
}
}
Future<TrackModel?> getTrack(String trackHash) async {
try {
final response = await _dio.get('/track/$trackHash');
final trackData = response.data['track'];
return trackData != null ? TrackModel.fromJson(trackData) : null;
} catch (e) {
throw Exception('Failed to load track: $e');
}
}
// Album methods
Future<List<AlbumModel>> getAlbums({
int limit = 20,
int offset = 0,
String? search,
String? artist,
}) async {
try {
Map<String, dynamic> queryParams = {
'limit': limit,
'offset': offset,
};
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
if (artist != null && artist.isNotEmpty) {
queryParams['artist'] = artist;
}
final response = await _dio.get('/albums', queryParameters: queryParams);
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
} catch (e) {
throw Exception('Failed to load albums: $e');
}
}
Future<AlbumModel?> getAlbum(String albumHash) async {
try {
final response = await _dio.get('/album/$albumHash');
final albumData = response.data['album'];
return albumData != null ? AlbumModel.fromJson(albumData) : null;
} catch (e) {
throw Exception('Failed to load album: $e');
}
}
Future<List<TrackModel>> getAlbumTracks(String albumHash, {
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/album/$albumHash/tracks', queryParameters: {
'limit': limit,
'offset': offset,
});
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to load album tracks: $e');
}
}
// Artist methods
Future<List<ArtistModel>> getArtists({
int limit = 20,
int offset = 0,
String? search,
}) async {
try {
Map<String, dynamic> queryParams = {
'limit': limit,
'offset': offset,
};
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
final response = await _dio.get('/artists', queryParameters: queryParams);
final artistsData = response.data['artists'] as List<dynamic>? ?? [];
return artistsData.map((artistData) => ArtistModel.fromJson(artistData)).toList();
} catch (e) {
throw Exception('Failed to load artists: $e');
}
}
Future<ArtistModel?> getArtist(String artistHash) async {
try {
final response = await _dio.get('/artist/$artistHash');
final artistData = response.data['artist'];
return artistData != null ? ArtistModel.fromJson(artistData) : null;
} catch (e) {
throw Exception('Failed to load artist: $e');
}
}
Future<List<AlbumModel>> getArtistAlbums(String artistHash, {
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/artist/$artistHash/albums', queryParameters: {
'limit': limit,
'offset': offset,
});
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
} catch (e) {
throw Exception('Failed to load artist albums: $e');
}
}
Future<List<TrackModel>> getArtistTracks(String artistHash, {
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/artist/$artistHash/tracks', queryParameters: {
'limit': limit,
'offset': offset,
});
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to load artist tracks: $e');
}
}
// Playlist methods
Future<List<PlaylistModel>> getPlaylists() async {
try {
final response = await _dio.get('/playlists');
final playlistsData = response.data['playlists'] as List<dynamic>? ?? [];
return playlistsData.map((playlistData) => PlaylistModel.fromJson(playlistData)).toList();
} catch (e) {
throw Exception('Failed to load playlists: $e');
}
}
Future<PlaylistModel?> getPlaylist(String playlistId) async {
try {
final response = await _dio.get('/playlist/$playlistId');
final playlistData = response.data['playlist'];
return playlistData != null ? PlaylistModel.fromJson(playlistData) : null;
} catch (e) {
throw Exception('Failed to load playlist: $e');
}
}
Future<PlaylistModel> createPlaylist(String name, String description) async {
try {
final response = await _dio.post('/playlists', data: {
'name': name,
'description': description,
});
return PlaylistModel.fromJson(response.data['playlist']);
} catch (e) {
throw Exception('Failed to create playlist: $e');
}
}
Future<void> addToPlaylist(String playlistId, String trackHash) async {
try {
await _dio.post('/playlist/$playlistId/add', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to add to playlist: $e');
}
}
Future<void> removeFromPlaylist(String playlistId, String trackHash) async {
try {
await _dio.delete('/playlist/$playlistId/remove', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to remove from playlist: $e');
}
}
// Favorites methods
Future<void> toggleFavoriteTrack(String trackHash) async {
try {
await _dio.post('/favorites/track/toggle', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to toggle favorite track: $e');
}
}
Future<void> toggleFavoriteAlbum(String albumHash) async {
try {
await _dio.post('/favorites/album/toggle', data: {
'albumhash': albumHash,
});
} catch (e) {
throw Exception('Failed to toggle favorite album: $e');
}
}
Future<void> toggleFavoriteArtist(String artistHash) async {
try {
await _dio.post('/favorites/artist/toggle', data: {
'artisthash': artistHash,
});
} catch (e) {
throw Exception('Failed to toggle favorite artist: $e');
}
}
Future<List<TrackModel>> getFavoriteTracks({
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/favorites/tracks', queryParameters: {
'limit': limit,
'offset': offset,
});
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to load favorite tracks: $e');
}
}
Future<List<AlbumModel>> getFavoriteAlbums({
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/favorites/albums', queryParameters: {
'limit': limit,
'offset': offset,
});
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
} catch (e) {
throw Exception('Failed to load favorite albums: $e');
}
}
Future<List<ArtistModel>> getFavoriteArtists({
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/favorites/artists', queryParameters: {
'limit': limit,
'offset': offset,
});
final artistsData = response.data['artists'] as List<dynamic>? ?? [];
return artistsData.map((artistData) => ArtistModel.fromJson(artistData)).toList();
} catch (e) {
throw Exception('Failed to load favorite artists: $e');
}
}
// Search methods
Future<List<SearchSuggestionModel>> getSearchSuggestions(String query) async {
try {
final response = await _dio.get('/search/suggestions', queryParameters: {
'q': query,
'limit': 10,
});
final suggestionsData = response.data['suggestions'] as List<dynamic>? ?? [];
return suggestionsData.map((suggestionData) => SearchSuggestionModel.fromJson(suggestionData)).toList();
} catch (e) {
throw Exception('Failed to get search suggestions: $e');
}
}
// Folder methods
Future<List<dynamic>> getFolders() async {
try {
final response = await _dio.get('/folders');
return response.data['folders'] as List<dynamic>? ?? [];
} catch (e) {
throw Exception('Failed to load folders: $e');
}
}
Future<List<TrackModel>> getFolderTracks(String folderHash, {
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get('/folder/$folderHash/tracks', queryParameters: {
'limit': limit,
'offset': offset,
});
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to load folder tracks: $e');
}
}
// User methods
Future<Map<String, dynamic>> getUserInfo() async {
try {
final response = await _dio.get('/user/info');
return response.data as Map<String, dynamic>? ?? {};
} catch (e) {
throw Exception('Failed to get user info: $e');
}
}
Future<void> updateUserPreferences(Map<String, dynamic> preferences) async {
try {
await _dio.post('/user/preferences', data: preferences);
} catch (e) {
throw Exception('Failed to update user preferences: $e');
}
}
Future<Map<String, dynamic>> getUserPreferences() async {
try {
final response = await _dio.get('/user/preferences');
return response.data as Map<String, dynamic>? ?? {};
} catch (e) {
throw Exception('Failed to get user preferences: $e');
}
}
// Statistics methods
Future<Map<String, dynamic>> getStatistics() async {
try {
final response = await _dio.get('/statistics');
return response.data as Map<String, dynamic>? ?? {};
} catch (e) {
throw Exception('Failed to get statistics: $e');
}
}
// Download methods
Future<void> downloadTrack(String trackHash) async {
try {
await _dio.post('/download/track', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to download track: $e');
}
}
Future<List<dynamic>> getDownloads() async {
try {
final response = await _dio.get('/downloads');
return response.data['downloads'] as List<dynamic>? ?? [];
} catch (e) {
throw Exception('Failed to get downloads: $e');
}
}
Future<void> deleteDownload(String downloadId) async {
try {
await _dio.delete('/download/$downloadId');
} catch (e) {
throw Exception('Failed to delete download: $e');
}
}
// Lyrics methods
Future<String?> getLyrics(String trackHash) async {
try {
final response = await _dio.get('/lyrics/$trackHash');
return response.data['lyrics'] as String?;
} catch (e) {
throw Exception('Failed to get lyrics: $e');
}
}
// Queue methods
Future<List<TrackModel>> getQueue() async {
try {
final response = await _dio.get('/queue');
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
} catch (e) {
throw Exception('Failed to get queue: $e');
}
}
Future<void> addToQueue(String trackHash) async {
try {
await _dio.post('/queue/add', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to add to queue: $e');
}
}
Future<void> removeFromQueue(String trackHash) async {
try {
await _dio.delete('/queue/remove', data: {
'trackhash': trackHash,
});
} catch (e) {
throw Exception('Failed to remove from queue: $e');
}
}
Future<void> clearQueue() async {
try {
await _dio.delete('/queue/clear');
} catch (e) {
throw Exception('Failed to clear queue: $e');
}
}
Future<void> reorderQueue(List<String> trackHashes) async {
try {
await _dio.post('/queue/reorder', data: {
'track_hashes': trackHashes,
});
} catch (e) {
throw Exception('Failed to reorder queue: $e');
}
}
}
@@ -1,114 +0,0 @@
import 'package:dio/dio.dart';
import '../models/track_model.dart';
class LyricsService {
late Dio _dio;
final String baseUrl;
LyricsService({String? baseUrl}) : baseUrl = baseUrl ?? 'https://your-server.com' {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) {
print('Lyrics API: $obj');
},
));
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
String errorMessage = 'Failed to load lyrics';
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
errorMessage = 'Network timeout - please check your connection';
} else if (error.response?.statusCode == 404) {
errorMessage = 'Lyrics not found for this track';
} else if (error.response?.statusCode == 500) {
errorMessage = 'Server error - please try again later';
}
print('Lyrics Error: $errorMessage');
handler.next(error);
},
));
}
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
Future<String?> getLyrics(String trackHash) async {
try {
final response = await _dio.get('/lyrics/$trackHash');
if (response.statusCode == 200) {
final data = response.data;
return data['lyrics'] as String?;
} else {
return null;
}
} catch (e) {
print('Error fetching lyrics: $e');
return null;
}
}
Future<String?> searchLyrics(String query, {int limit = 10}) async {
try {
final response = await _dio.get('/lyrics/search', queryParameters: {
'q': query,
'limit': limit,
});
if (response.statusCode == 200) {
final data = response.data;
final results = data['results'] as List<dynamic>? ?? [];
return results.isNotEmpty ? results.first['lyrics'] as String? : null;
} else {
return null;
}
} catch (e) {
print('Error searching lyrics: $e');
return null;
}
}
Future<bool> saveLyrics(String trackHash, String lyrics) async {
try {
final response = await _dio.post('/lyrics/save', data: {
'trackHash': trackHash,
'lyrics': lyrics,
});
return response.statusCode == 200;
} catch (e) {
print('Error saving lyrics: $e');
return false;
}
}
Future<Map<String, dynamic>> getLyricsStats() async {
try {
final response = await _dio.get('/lyrics/stats');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>? ?? {};
} else {
return {};
}
} catch (e) {
print('Error fetching lyrics stats: $e');
return {};
}
}
}
@@ -1,442 +0,0 @@
import 'package:flutter/material.dart';
class AnalyticsScreen extends StatefulWidget {
const AnalyticsScreen({super.key});
@override
State<AnalyticsScreen> createState() => _AnalyticsScreenState();
}
class _AnalyticsScreenState extends State<AnalyticsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Analytics'),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Overview Cards
Row(
children: [
Expanded(
child: _buildOverviewCard(
'Total Plays',
Icons.play_arrow,
'12,345',
Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildOverviewCard(
'Total Listening Time',
Icons.access_time,
'48h 32m',
Theme.of(context).colorScheme.secondary,
),
),
],
),
const SizedBox(height: 16),
// Top Tracks
_buildSectionHeader('Top Tracks'),
const SizedBox(height: 8),
_buildTopTracksList(),
const SizedBox(height: 24),
// Listening Stats
_buildSectionHeader('Listening Statistics'),
const SizedBox(height: 8),
_buildListeningStats(),
const SizedBox(height: 24),
// Genre Distribution
_buildSectionHeader('Genre Distribution'),
const SizedBox(height: 8),
_buildGenreChart(),
const SizedBox(height: 24),
// Time Distribution
_buildSectionHeader('Time Distribution'),
const SizedBox(height: 8),
_buildTimeChart(),
],
),
),
);
}
Widget _buildSectionHeader(String title) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.0),
),
child: Row(
children: [
Icon(
Icons.analytics,
color: Theme.of(context).colorScheme.primary,
size: 24,
),
const SizedBox(width: 12),
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildOverviewCard(String title, IconData icon, String value, Color color) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
],
),
],
),
);
}
Widget _buildTopTracksList() {
final topTracks = [
{'title': 'Bohemian Rhapsody', 'artist': 'Queen', 'plays': 1234, 'duration': '5:55'},
{'title': 'Stairway to Heaven', 'artist': 'Led Zeppelin', 'plays': 987, 'duration': '8:02'},
{'title': 'Hotel California', 'artist': 'Eagles', 'plays': 856, 'duration': '3:31'},
{'title': 'Sweet Child O\' Mine', 'artist': 'Guns N\' Roses', 'plays': 743, 'duration': '5:44'},
{'title': 'Don\'t Stop Believin\'', 'artist': 'Journey', 'plays': 654, 'duration': '4:12'},
];
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: topTracks.length,
itemBuilder: (context, index) {
final track = topTracks[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
track['title']?.toString() ?? '',
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(
track['artist']?.toString() ?? '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
trailing: Text(
'${track['plays'] ?? 0} plays',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
},
),
],
),
);
}
Widget _buildListeningStats() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Daily Average',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatItem('Songs per Day', '24'),
),
Expanded(
child: _buildStatItem('Hours per Day', '3.2'),
),
],
),
const SizedBox(height: 16),
Text(
'Weekly Average',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatItem('Songs per Week', '168'),
),
Expanded(
child: _buildStatItem('Hours per Week', '22.4'),
),
],
),
const SizedBox(height: 16),
Text(
'Monthly Average',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildStatItem('Songs per Month', '730'),
),
Expanded(
child: _buildStatItem('Hours per Month', '97.1'),
),
],
),
],
),
),
);
}
Widget _buildStatItem(String label, String value) {
return Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildGenreChart() {
final genres = {
'Rock': 35,
'Pop': 28,
'Electronic': 15,
'Classical': 12,
'Jazz': 8,
'Hip-Hop': 7,
'Country': 5,
};
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Genre Distribution',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: genres.entries.map((entry) {
return Chip(
label: Text(entry.key),
backgroundColor: _getGenreColor(entry.key),
labelStyle: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}).toList(),
),
),
],
),
),
);
}
Color _getGenreColor(String genre) {
switch (genre) {
case 'Rock':
return Colors.red;
case 'Pop':
return Colors.purple;
case 'Electronic':
return Colors.blue;
case 'Classical':
return Colors.brown;
case 'Jazz':
return Colors.orange;
case 'Hip-Hop':
return Colors.green;
case 'Country':
return Colors.amber;
default:
return Colors.grey;
}
}
Widget _buildTimeChart() {
final timeData = [
{'period': 'Morning', 'hours': 2.5, 'percentage': 15},
{'period': 'Afternoon', 'hours': 4.2, 'percentage': 25},
{'period': 'Evening', 'hours': 3.8, 'percentage': 22},
{'period': 'Night', 'hours': 7.5, 'percentage': 38},
];
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Daily Listening Pattern',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: Column(
children: timeData.map((data) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
data['period']?.toString() ?? '',
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'${data['percentage']}%',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
height: 8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(4.0),
),
child: FractionallySizedBox(
widthFactor: (data['percentage'] as int) / 100,
alignment: Alignment.centerLeft,
child: Container(
height: 4,
color: Colors.white,
),
),
),
],
),
);
}).toList(),
),
),
],
),
),
);
}
}
@@ -1,88 +0,0 @@
import 'package:flutter/material.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Authentication'),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.music_note,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 24),
const Text(
'SwingMusic',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Connect to your music library',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: const Text('Login'),
),
],
),
),
);
}
void _handleLogin() async {
setState(() {
_isLoading = true;
});
await Future.delayed(const Duration(seconds: 2));
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login successful!')),
);
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
@@ -1,468 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/auth_provider.dart';
import '../../core/constants/app_spacing.dart';
import '../../core/enums/auth_state.dart';
class EnhancedAuthScreen extends StatefulWidget {
const EnhancedAuthScreen({super.key});
@override
State<EnhancedAuthScreen> createState() => _EnhancedAuthScreenState();
}
class _EnhancedAuthScreenState extends State<EnhancedAuthScreen> {
final _formKey = GlobalKey<FormState>();
final _baseUrlController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _qrCodeController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
bool _isQrMode = false;
String? _errorMessage;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AuthProvider>(context, listen: false).initialize();
});
}
@override
void dispose() {
_baseUrlController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_qrCodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: SafeArea(
child: SingleChildScrollView(
padding: AppSpacing.paddingLG,
child: Column(
children: [
// Logo and Title
_buildHeader(context),
const SizedBox(height: 32),
// Auth Mode Toggle
_buildAuthModeToggle(context),
const SizedBox(height: 32),
// Auth Form
_buildAuthForm(context),
const SizedBox(height: 24),
// Error Message
if (_errorMessage != null)
_buildErrorMessage(context),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Column(
children: [
Icon(
Icons.music_note,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'SwingMusic',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'Connect to your music library',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildAuthModeToggle(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isQrMode = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _isQrMode
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person,
color: _isQrMode
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(width: 8),
Text(
'Username & Password',
style: TextStyle(
color: _isQrMode
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isQrMode = true),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _isQrMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code_scanner,
color: _isQrMode
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'QR Code',
style: TextStyle(
color: _isQrMode
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
],
),
);
}
Widget _buildAuthForm(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
return Form(
key: _formKey,
child: Column(
children: [
if (_isQrMode) ...[
// QR Code Input
TextFormField(
controller: _qrCodeController,
decoration: InputDecoration(
labelText: 'QR Code',
hintText: 'Enter or scan QR code',
prefixIcon: const Icon(Icons.qr_code_scanner),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
readOnly: true,
onTap: () => _scanQrCode(context),
),
] else ...[
// Server URL Input
TextFormField(
controller: _baseUrlController,
decoration: InputDecoration(
labelText: 'Server URL',
hintText: 'https://your-server.com',
prefixIcon: const Icon(Icons.link),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Server URL is required';
}
if (!_isValidUrl(value)) {
return 'Please enter a valid URL';
}
return null;
},
),
const SizedBox(height: 16),
// Username Input
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Username is required';
}
return null;
},
),
const SizedBox(height: 16),
// Password Input
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
onPressed: () => setState(() => _isPasswordVisible = !_isPasswordVisible),
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Password is required';
}
return null;
},
),
],
const SizedBox(height: 24),
// Login Button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : () => _handleLogin(context),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text(
_isQrMode ? 'Login with QR' : 'Login',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
},
);
}
Widget _buildErrorMessage(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 14,
),
),
),
IconButton(
onPressed: () => setState(() => _errorMessage = null),
icon: const Icon(Icons.close),
iconSize: 16,
color: Theme.of(context).colorScheme.error,
),
],
),
);
}
bool _isValidUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (e) {
return false;
}
}
Future<void> _handleLogin(BuildContext context) async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
if (_isQrMode) {
await authProvider.loginWithQrCode(_qrCodeController.text.trim());
} else {
await authProvider.loginWithUsernameAndPassword(
_baseUrlController.text.trim(),
_usernameController.text.trim(),
_passwordController.text,
);
}
if (mounted) {
Navigator.of(context).pushReplacementNamed('/home');
}
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
}
}
Future<void> _scanQrCode(BuildContext context) async {
try {
final qrCode = await Navigator.pushNamed<String>('/qr');
if (qrCode != null && mounted) {
_qrCodeController.text = qrCode!;
}
} catch (e) {
setState(() {
_errorMessage = 'Failed to scan QR code: $e';
});
}
}
}
@@ -1,889 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../data/models/track_model.dart';
class FolderScreen extends StatefulWidget {
const FolderScreen({super.key});
@override
State<FolderScreen> createState() => _FolderScreenState();
}
class _FolderScreenState extends State<FolderScreen> {
List<FolderItem> _folderItems = [];
List<FolderItem> _currentPath = [];
bool _isLoading = false;
final TextEditingController _searchController = TextEditingController();
String _currentFolderId = 'root';
@override
void initState() {
super.initState();
_loadFolderContents(_currentFolderId);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadFolderContents(String folderId) async {
setState(() {
_isLoading = true;
});
// Simulate API call
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_isLoading = false;
if (folderId == 'root') {
_currentPath = [];
_folderItems = [
FolderItem(
id: 'music',
name: 'Music',
type: FolderType.folder,
itemCount: 156,
path: '/music',
modifiedDate: DateTime.now().subtract(const Duration(days: 1)),
),
FolderItem(
id: 'downloads',
name: 'Downloads',
type: FolderType.folder,
itemCount: 42,
path: '/downloads',
modifiedDate: DateTime.now().subtract(const Duration(hours: 3)),
),
FolderItem(
id: 'rock',
name: 'Rock',
type: FolderType.folder,
itemCount: 89,
path: '/music/rock',
modifiedDate: DateTime.now().subtract(const Duration(days: 7)),
),
FolderItem(
id: 'pop',
name: 'Pop',
type: FolderType.folder,
itemCount: 67,
path: '/music/pop',
modifiedDate: DateTime.now().subtract(const Duration(days: 2)),
),
FolderItem(
id: 'jazz',
name: 'Jazz',
type: FolderType.folder,
itemCount: 34,
path: '/music/jazz',
modifiedDate: DateTime.now().subtract(const Duration(days: 14)),
),
FolderItem(
id: 'electronic',
name: 'Electronic',
type: FolderType.folder,
itemCount: 78,
path: '/music/electronic',
modifiedDate: DateTime.now().subtract(const Duration(days: 5)),
),
FolderItem(
id: 'classical',
name: 'Classical',
type: FolderType.folder,
itemCount: 45,
path: '/music/classical',
modifiedDate: DateTime.now().subtract(const Duration(days: 21)),
),
];
} else if (folderId == 'music') {
_currentPath = [
FolderItem(id: 'root', name: '..', type: FolderType.folder, path: '/'),
FolderItem(id: 'rock', name: 'Rock', type: FolderType.folder, itemCount: 89, path: '/music/rock'),
FolderItem(id: 'pop', name: 'Pop', type: FolderType.folder, itemCount: 67, path: '/music/pop'),
FolderItem(id: 'jazz', name: 'Jazz', type: FolderType.folder, itemCount: 34, path: '/music/jazz'),
FolderItem(id: 'electronic', name: 'Electronic', type: FolderType.folder, itemCount: 78, path: '/music/electronic'),
FolderItem(id: 'classical', name: 'Classical', type: FolderType.folder, itemCount: 45, path: '/music/classical'),
];
_folderItems = [
TrackFolderItem(
id: '1',
name: 'Bohemian Rhapsody.mp3',
artist: 'Queen',
album: 'A Night at the Opera',
duration: 334,
size: 5.2,
modifiedDate: DateTime.now().subtract(const Duration(days: 30)),
),
TrackFolderItem(
id: '2',
name: 'Stairway to Heaven.mp3',
artist: 'Led Zeppelin',
album: 'Led Zeppelin IV',
duration: 482,
size: 7.8,
modifiedDate: DateTime.now().subtract(const Duration(days: 25)),
),
TrackFolderItem(
id: '3',
name: 'Hotel California.mp3',
artist: 'Eagles',
album: 'Hotel California',
duration: 391,
size: 6.1,
modifiedDate: DateTime.now().subtract(const Duration(days: 20)),
),
TrackFolderItem(
id: '4',
name: 'Sweet Child O\' Mine.mp3',
artist: 'Queen',
album: 'A Night at the Opera',
duration: 348,
size: 5.4,
modifiedDate: DateTime.now().subtract(const Duration(days: 15)),
),
TrackFolderItem(
id: '5',
name: 'Another Brick in the Wall.mp3',
artist: 'Pink Floyd',
album: 'The Wall',
duration: 623,
size: 9.8,
modifiedDate: DateTime.now().subtract(const Duration(days: 10)),
),
];
} else {
// Handle other folders with sample data
_currentPath = [
FolderItem(id: 'root', name: '..', type: FolderType.folder, path: '/'),
];
_folderItems = [
TrackFolderItem(
id: '1',
name: 'Sample Track.mp3',
artist: 'Sample Artist',
album: 'Sample Album',
duration: 240,
size: 4.2,
modifiedDate: DateTime.now().subtract(const Duration(days: 5)),
),
];
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_getFolderTitle()),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
leading: _currentFolderId != 'root'
? IconButton(
onPressed: () => _navigateToFolder('root'),
icon: const Icon(Icons.arrow_back),
)
: null,
actions: [
PopupMenuButton<String>(
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'sort_name',
child: ListTile(
leading: const Icon(Icons.sort_by_alpha),
title: Text('Sort by Name'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'sort_date',
child: ListTile(
leading: const Icon(Icons.access_time),
title: Text('Sort by Date'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'sort_size',
child: ListTile(
leading: const Icon(Icons.storage),
title: Text('Sort by Size'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'view_list',
child: ListTile(
leading: const Icon(Icons.list),
title: Text('List View'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'view_grid',
child: ListTile(
leading: const Icon(Icons.grid_view),
title: Text('Grid View'),
contentPadding: EdgeInsets.zero,
),
),
],
),
],
),
body: Column(
children: [
// Breadcrumb Navigation
_buildBreadcrumbNavigation(),
// Search Bar
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search in current folder...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
},
icon: const Icon(Icons.clear),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_filterItems(value);
},
),
),
// Folder Contents
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _folderItems.isEmpty
? _buildEmptyState()
: _buildFolderContents(),
),
],
),
);
}
Widget _buildBreadcrumbNavigation() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
// Root/Home
InkWell(
onTap: () => _navigateToFolder('root'),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _currentFolderId == 'root'
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.home,
size: 20,
color: _currentFolderId == 'root'
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'Home',
style: TextStyle(
color: _currentFolderId == 'root'
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// Breadcrumb items
..._currentPath.map((item) {
return Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.chevron_right, size: 16),
const SizedBox(width: 4),
InkWell(
onTap: () => _navigateToFolder(item.id),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: item.id == _currentFolderId
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.name,
style: TextStyle(
color: item.id == _currentFolderId
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}).toList(),
],
),
),
);
}
Widget _buildFolderContents() {
return ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _folderItems.length,
itemBuilder: (context, index) {
final item = _folderItems[index];
if (item is TrackFolderItem) {
return TrackFolderTile(
track: item,
onTap: () => _playTrack(item),
onPlay: () => _playTrack(item),
);
} else {
return FolderTile(
folder: item,
onTap: () => _navigateToFolder(item.id),
onLongPress: () => _showFolderOptions(item),
);
}
},
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'This folder is empty',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Add music files to see them here',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
String _getFolderTitle() {
if (_currentFolderId == 'root') {
return 'Music Library';
} else {
final folder = _currentPath.firstWhere(
(item) => item.id == _currentFolderId,
orElse: () => FolderItem(id: '', name: '', type: FolderType.folder),
);
return folder.name;
}
}
void _filterItems(String query) {
setState(() {
if (query.isEmpty) {
_loadFolderContents(_currentFolderId);
} else {
// Filter items (in real app, this would call API)
_folderItems = _folderItems.where((item) {
final name = item.name.toLowerCase();
return name.contains(query.toLowerCase());
}).toList();
}
});
}
void _navigateToFolder(String folderId) {
setState(() {
_currentFolderId = folderId;
});
_loadFolderContents(folderId);
}
void _playTrack(TrackFolderItem track) {
final trackModel = TrackModel(
id: int.parse(track.id),
title: track.name,
album: track.album,
albumhash: track.album.hashCode.toString(),
artists: [],
albumartists: [],
artisthashes: [],
track: 0,
disc: 1,
duration: track.duration,
bitrate: 320,
filepath: track.id, // Use ID as filepath for demo
folder: '',
genres: [],
genrehashes: [],
copyright: '',
date: DateTime.now().millisecondsSinceEpoch ~/ 1000,
lastModified: DateTime.now().millisecondsSinceEpoch ~/ 1000,
trackhash: track.id,
image: '',
weakHash: '',
extra: {},
lastplayed: 0,
playcount: 0,
playduration: track.duration,
explicit: false,
favUserids: [],
isFavorite: false,
score: 0.0,
);
final audioProvider = Provider.of<AudioProvider>(context, listen: false);
audioProvider.setQueue([trackModel]);
audioProvider.loadTrack(trackModel);
audioProvider.play();
Navigator.pushNamed(context, '/player');
}
void _handleMenuAction(String action) {
switch (action) {
case 'sort_name':
setState(() {
_folderItems.sort((a, b) => a.name.compareTo(b.name));
});
break;
case 'sort_date':
setState(() {
_folderItems.sort((a, b) => b.modifiedDate!.compareTo(a.modifiedDate!));
});
break;
case 'sort_size':
setState(() {
_folderItems.sort((a, b) {
final sizeA = a is TrackFolderItem ? a.size : 0;
final sizeB = b is TrackFolderItem ? b.size : 0;
return sizeA.compareTo(sizeB);
});
});
break;
// View options would be handled here
}
}
void _showFolderOptions(FolderItem folder) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.play_arrow),
title: const Text('Play All'),
onTap: () {
Navigator.pop(context);
_playAllInFolder();
},
),
ListTile(
leading: const Icon(Icons.shuffle),
title: const Text('Shuffle All'),
onTap: () {
Navigator.pop(context);
_shuffleAllInFolder();
},
),
ListTile(
leading: const Icon(Icons.playlist_add),
title: const Text('Add to Playlist'),
onTap: () {
Navigator.pop(context);
_addToPlaylist();
},
),
const SizedBox(height: 8),
ListTile(
leading: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.error,
),
title: Text(
'Delete',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
onTap: () {
Navigator.pop(context);
_deleteFolder();
},
),
],
),
),
);
}
void _playAllInFolder() {
// Implementation to play all tracks in folder
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Playing all tracks in folder')),
);
}
void _shuffleAllInFolder() {
// Implementation to shuffle all tracks in folder
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Shuffling all tracks in folder')),
);
}
void _addToPlaylist() {
// Implementation to add folder contents to playlist
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Added to playlist')),
);
}
void _deleteFolder() {
// Implementation to delete folder
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Folder deleted')),
);
}
}
// Helper classes
class FolderItem {
final String id;
final String name;
final FolderType type;
final int? itemCount;
final String? path;
final DateTime? modifiedDate;
FolderItem({
required this.id,
required this.name,
required this.type,
this.itemCount,
this.path,
this.modifiedDate,
});
}
class TrackFolderItem extends FolderItem {
final String artist;
final String album;
final int duration;
final double size;
TrackFolderItem({
required String id,
required String name,
required this.artist,
required this.album,
required this.duration,
required this.size,
String? path,
DateTime? modifiedDate,
}) : super(
id: id,
name: name,
type: FolderType.track,
path: path,
modifiedDate: modifiedDate ?? DateTime.now(),
);
}
enum FolderType {
folder,
track,
}
// Custom widgets
class FolderTile extends StatelessWidget {
final FolderItem folder;
final VoidCallback onTap;
final VoidCallback? onLongPress;
const FolderTile({
super.key,
required this.folder,
required this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Folder/Track Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: folder.type == FolderType.folder
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.0),
),
child: Icon(
folder.type == FolderType.folder ? Icons.folder : Icons.music_note,
color: folder.type == FolderType.folder
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(width: 16),
// Folder/Track Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
folder.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
if (folder.itemCount != null)
Text(
'${folder.itemCount} items',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (folder.modifiedDate != null)
Text(
_formatDate(folder.modifiedDate!),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (folder is TrackFolderItem)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(folder as TrackFolderItem).artist,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
(folder as TrackFolderItem).album,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
],
),
),
),
);
}
}
class TrackFolderTile extends StatelessWidget {
final TrackFolderItem track;
final VoidCallback onTap;
final VoidCallback? onPlay;
const TrackFolderTile({
super.key,
required this.track,
required this.onTap,
this.onPlay,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Track Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.music_note,
color: Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(width: 16),
// Track Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
track.artist,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
track.album,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
_formatDuration(track.duration),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
Text(
'${track.size.toStringAsFixed(1)} MB',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
// Play Button
IconButton(
onPressed: onPlay,
icon: const Icon(Icons.play_arrow),
color: Theme.of(context).colorScheme.primary,
),
],
),
),
),
);
}
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else if (difference.inDays < 30) {
return '${(difference.inDays / 7).floor()} weeks ago';
} else {
return '${(difference.inDays / 30).floor()} months ago';
}
}
String _formatDuration(int seconds) {
final minutes = seconds ~/ 60;
final remainingSeconds = seconds % 60;
return '${minutes}:${remainingSeconds.toString().padLeft(2, '0')}';
}
@@ -1,407 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../shared/providers/library_provider.dart';
import '../../core/widgets/album_card.dart';
import '../../core/widgets/track_list_tile.dart';
import '../../data/models/track_model.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late LibraryProvider _libraryProvider;
late AudioProvider _audioProvider;
bool _isLoading = false;
@override
void initState() {
super.initState();
_libraryProvider = Provider.of<LibraryProvider>(context, listen: false);
_audioProvider = Provider.of<AudioProvider>(context, listen: false);
_loadHomeData();
}
Future<void> _loadHomeData() async {
setState(() {
_isLoading = true;
});
try {
// Load recent data from library
await Future.wait([
_libraryProvider.loadTracks(limit: 10),
_libraryProvider.loadAlbums(limit: 5),
_libraryProvider.loadArtists(limit: 5),
]);
} catch (e) {
// Handle error silently for now
}
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// App Bar
SliverAppBar(
floating: true,
snap: true,
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
title: Text(
'SwingMusic',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
actions: [
IconButton(
onPressed: () {
// Show notifications
},
icon: const Icon(Icons.notifications_outlined),
),
PopupMenuButton<String>(
icon: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(
Icons.person,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
onSelected: (value) {
if (value == 'settings') {
Navigator.pushNamed(context, '/settings');
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings),
SizedBox(width: 8),
Text('Settings'),
],
),
),
],
),
],
),
// Quick Actions Section
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quick Actions',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildQuickActionCard(
icon: Icons.search,
label: 'Search',
color: Theme.of(context).colorScheme.primary,
onTap: () {
Navigator.pushNamed(context, '/search');
},
),
),
const SizedBox(width: 12),
Expanded(
child: _buildQuickActionCard(
icon: Icons.library_music,
label: 'Library',
color: Theme.of(context).colorScheme.secondary,
onTap: () {
Navigator.pushNamed(context, '/library');
},
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildQuickActionCard(
icon: Icons.favorite,
label: 'Favorites',
color: Colors.red,
onTap: () {
Navigator.pushNamed(context, '/library');
},
),
),
const SizedBox(width: 12),
Expanded(
child: _buildQuickActionCard(
icon: Icons.download,
label: 'Downloads',
color: Colors.green,
onTap: () {
// Navigate to downloads
},
),
),
],
),
],
),
),
),
// Recently Played Section
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recently Played',
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/library');
},
child: const Text('See all'),
),
],
),
const SizedBox(height: 16),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_libraryProvider.tracks.isEmpty)
_buildEmptySection('No recently played tracks')
else
SizedBox(
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _libraryProvider.tracks.length.clamp(0, 10),
itemBuilder: (context, index) {
final track = _libraryProvider.tracks[index];
return Container(
width: 160,
margin: const EdgeInsets.only(right: 12),
child: TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
showAlbumArt: true,
),
);
},
),
),
],
),
),
),
// Recent Albums Section
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recent Albums',
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/library');
},
child: const Text('See all'),
),
],
),
const SizedBox(height: 16),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_libraryProvider.albums.isEmpty)
_buildEmptySection('No recent albums')
else
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _libraryProvider.albums.length.clamp(0, 4),
itemBuilder: (context, index) {
final album = _libraryProvider.albums[index];
return AlbumCard(
album: album,
onTap: () {
// Navigate to album details
},
);
},
),
],
),
),
),
// Top Artists Section
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Top Artists',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_libraryProvider.artists.isEmpty)
_buildEmptySection('No top artists')
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _libraryProvider.artists.length.clamp(0, 5),
itemBuilder: (context, index) {
final artist = _libraryProvider.artists[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
artist.name.isNotEmpty ? artist.name[0].toUpperCase() : '?',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
),
title: Text(artist.name),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Navigate to artist details
},
);
},
),
],
),
),
),
// Bottom padding
const SliverToBoxAdapter(
child: SizedBox(height: 100), // Space for mini player
),
],
),
);
}
Widget _buildQuickActionCard({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 28,
),
),
const SizedBox(height: 12),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildEmptySection(String message) {
return Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(
Icons.music_note_outlined,
size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 12),
Text(
message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
void _playTrack(TrackModel track) {
_audioProvider.setQueue([track]);
_audioProvider.loadTrack(track);
_audioProvider.play();
Navigator.pushNamed(context, '/player');
}
}
@@ -1,363 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../shared/providers/library_provider.dart';
import '../../core/widgets/album_card.dart';
import '../../core/widgets/track_list_tile.dart';
import '../../data/models/track_model.dart';
class LibraryScreen extends StatefulWidget {
const LibraryScreen({super.key});
@override
State<LibraryScreen> createState() => _LibraryScreenState();
}
class _LibraryScreenState extends State<LibraryScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
late LibraryProvider _libraryProvider;
late AudioProvider _audioProvider;
bool _isLoading = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 6, vsync: this);
_libraryProvider = Provider.of<LibraryProvider>(context, listen: false);
_audioProvider = Provider.of<AudioProvider>(context, listen: false);
_loadLibraryData();
}
Future<void> _loadLibraryData() async {
setState(() {
_isLoading = true;
});
try {
await Future.wait([
_libraryProvider.loadTracks(),
_libraryProvider.loadAlbums(),
_libraryProvider.loadArtists(),
_libraryProvider.loadFavorites(),
]);
} catch (e) {
// Handle error silently for now
}
setState(() {
_isLoading = false;
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your Library',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Search bar
TextField(
decoration: InputDecoration(
hintText: 'Search your library...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
],
),
),
// Tabs
TabBar(
controller: _tabController,
isScrollable: true,
tabs: const [
Tab(text: 'Recent'),
Tab(text: 'Albums'),
Tab(text: 'Tracks'),
Tab(text: 'Folders'),
Tab(text: 'Favorites'),
Tab(text: 'Playlists'),
],
),
// Tab content
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildRecentTab(),
_buildAlbumsTab(),
_buildTracksTab(),
_buildFoldersTab(),
_buildFavoritesTab(),
_buildPlaylistsTab(),
],
),
),
],
),
);
}
Widget _buildRecentTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recently Played',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (_libraryProvider.tracks.isEmpty)
_buildEmptyState('No recently played tracks')
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _libraryProvider.tracks.length,
itemBuilder: (context, index) {
final track = _libraryProvider.tracks[index];
return TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
isPlaying: _isCurrentTrack(track),
);
},
),
],
),
);
}
Widget _buildAlbumsTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Albums',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (_libraryProvider.albums.isEmpty)
_buildEmptyState('No albums found')
else
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _libraryProvider.albums.length,
itemBuilder: (context, index) {
final album = _libraryProvider.albums[index];
return AlbumCard(
album: album,
onTap: () {
// Navigate to album details
},
);
},
),
],
),
);
}
Widget _buildTracksTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'All Tracks',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (_libraryProvider.tracks.isEmpty)
_buildEmptyState('No tracks found')
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _libraryProvider.tracks.length,
itemBuilder: (context, index) {
final track = _libraryProvider.tracks[index];
return TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
isPlaying: _isCurrentTrack(track),
);
},
),
],
),
);
}
Widget _buildFavoritesTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Favorite Tracks',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (_libraryProvider.favoriteTracks.isEmpty)
_buildEmptyState('No favorite tracks yet')
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _libraryProvider.favoriteTracks.length,
itemBuilder: (context, index) {
final track = _libraryProvider.favoriteTracks[index];
return TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
isPlaying: _isCurrentTrack(track),
);
},
),
],
),
);
}
Widget _buildFoldersTab() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder,
size: 64,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(height: 16),
Text(
'Folder navigation coming soon',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildPlaylistsTab() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.playlist_play,
size: 64,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(height: 16),
Text(
'Playlist management coming soon',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildEmptyState(String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 64,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
);
}
void _playTrack(TrackModel track) {
_audioProvider.setQueue([track]);
_audioProvider.loadTrack(track);
_audioProvider.play();
Navigator.pushNamed(context, '/player');
}
bool _isCurrentTrack(TrackModel track) {
return _audioProvider.currentTrack?.trackhash == track.trackhash;
}
}
@@ -1,482 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../data/services/lyrics_service.dart';
import '../../core/constants/app_spacing.dart';
class LyricsScreen extends StatefulWidget {
const LyricsScreen({super.key});
@override
State<LyricsScreen> createState() => _LyricsScreenState();
}
class _LyricsScreenState extends State<LyricsScreen> {
bool _isLoading = false;
String? _lyrics;
String? _error;
final TextEditingController _lyricsController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
title: Text(
'Lyrics',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
body: Consumer<AudioProvider>(
builder: (context, audioProvider, child) {
final currentTrack = audioProvider.currentTrack;
if (currentTrack == null) {
return _buildEmptyState(context);
}
return Column(
children: [
// Track Info
Container(
padding: AppSpacing.paddingLG,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentTrack.displayTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
currentTrack.artistNames,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (currentTrack.displayAlbum.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
currentTrack.displayAlbum,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
const SizedBox(width: 16),
// Album Art
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: currentTrack.image.isNotEmpty
? Image.network(
currentTrack.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.8),
Theme.of(context).colorScheme.primary,
],
),
),
child: Icon(
Icons.music_note,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
),
);
},
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.8),
Theme.of(context).colorScheme.primary,
],
),
),
child: Icon(
Icons.album,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
),
),
),
),
],
),
),
const SizedBox(height: 24),
// Sync Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _loadLyrics(context, currentTrack?.trackhash ?? ''),
icon: Icon(
Icons.sync,
color: Theme.of(context).colorScheme.onPrimary,
),
label: 'Sync Lyrics',
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
),
const SizedBox(height: 24),
// Lyrics Content
Expanded(
child: Container(
padding: AppSpacing.paddingLG,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isLoading) ...[
const Center(
child: CircularProgressIndicator(),
),
] else if (_error != null) ...[
Container(
padding: AppSpacing.paddingMD,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_error!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
),
] else if (_lyrics != null && _lyrics!.isNotEmpty) ...[
// Edit Mode
if (_isEditMode) ...[
_buildEditMode(context),
] else ...[
// Display Mode
_buildLyricsDisplay(context),
],
] else ...[
Container(
padding: AppSpacing.paddingXL,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lyrics,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
size: 48,
),
const SizedBox(height: 16),
Text(
'No lyrics available',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
),
),
],
],
),
),
),
],
);
},
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Container(
padding: AppSpacing.paddingXL,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 64,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
'No lyrics available',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Play a track to see its lyrics',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildLyricsDisplay(BuildContext context) {
return Container(
padding: AppSpacing.paddingMD,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with Edit button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Lyrics',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
Row(
children: [
// Edit/Save buttons
if (_isEditMode) ...[
IconButton(
onPressed: () => _saveLyrics(context),
icon: Icon(
Icons.save,
color: Theme.of(context).colorScheme.primary,
size: 20,
),
),
IconButton(
onPressed: () => _cancelEdit(context),
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
size: 20,
),
),
] else ...[
IconButton(
onPressed: () => _enableEditMode(context),
icon: Icon(
Icons.edit,
color: Theme.of(context).colorScheme.primary,
size: 20,
),
),
],
],
// Sync button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _syncLyrics(context),
icon: Icon(
Icons.refresh,
color: Theme.of(context).colorScheme.onPrimary,
),
label: 'Sync',
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
),
],
),
),
// Lyrics text
Expanded(
child: SingleChildScrollView(
child: Text(
_lyrics!,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
height: 1.6,
color: Theme.of(context).colorScheme.onSurface,
leading: TextStyle(
height: 1.4,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
),
),
),
],
),
);
}
Widget _buildEditMode(BuildContext context) {
return Container(
padding: AppSpacing.paddingMD,
child: Row(
children: [
Expanded(
child: TextField(
controller: _lyricsController,
maxLines: null,
style: TextStyle(
height: 1.6,
color: Theme.of(context).colorScheme.onSurface,
),
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
),
),
),
IconButton(
onPressed: () => _saveLyrics(context),
icon: Icon(
Icons.save,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
);
}
bool get _isEditMode => _lyricsController.text != _lyrics;
void _enableEditMode() {
_lyricsController.text = _lyrics ?? '';
setState(() {});
}
void _cancelEdit() {
_lyricsController.text = _lyrics ?? '';
setState(() {});
}
void _saveLyrics(BuildContext context) {
final updatedLyrics = _lyricsController.text.trim();
// TODO: Save lyrics to API
setState(() {
_lyrics = updatedLyrics;
_isEditMode = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Lyrics saved successfully'),
backgroundColor: Colors.green,
),
);
}
Future<void> _loadLyrics(BuildContext context, String trackHash) async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final lyricsService = LyricsService();
final lyrics = await lyricsService.getLyrics(trackHash);
setState(() {
_isLoading = false;
_lyrics = lyrics;
});
} catch (e) {
setState(() {
_isLoading = false;
_error = 'Failed to load lyrics: $e';
});
}
}
void _syncLyrics(BuildContext context) {
final currentTrack = Provider.of<AudioProvider>(context, listen: false).currentTrack;
if (currentTrack != null) return;
_loadLyrics(context, currentTrack.trackhash);
}
}
@@ -1,374 +0,0 @@
import 'package:flutter/material.dart';
class OfflineScreen extends StatefulWidget {
const OfflineScreen({super.key});
@override
State<OfflineScreen> createState() => _OfflineScreenState();
}
class _OfflineScreenState extends State<OfflineScreen> {
bool _isOfflineMode = false;
final List<Map<String, dynamic>> _downloadedTracks = [];
int _totalDownloads = 0;
int _completedDownloads = 0;
double _totalDownloadSize = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Offline Mode'),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
actions: [
Switch(
value: _isOfflineMode,
onChanged: (value) {
setState(() {
_isOfflineMode = value;
});
},
activeThumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.onPrimary,
),
],
),
body: Column(
children: [
// Offline Mode Toggle Card
Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.cloud_off,
color: _isOfflineMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
size: 24,
),
const SizedBox(width: 12),
Text(
'Offline Mode',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
_isOfflineMode
? 'Download music for offline listening'
: 'Connect to server for online mode',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 16),
// Downloads Section
Expanded(
child: Container(
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1.0,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Downloads Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Downloads',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: _clearDownloads,
icon: const Icon(Icons.clear_all),
tooltip: 'Clear All',
),
],
),
const SizedBox(height: 16),
// Download Stats
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard('Total', _totalDownloads.toString(), Icons.download),
_buildStatCard('Completed', _completedDownloads.toString(), Icons.check_circle),
_buildStatCard('Size', _formatFileSize(_totalDownloadSize), Icons.storage),
],
),
),
const SizedBox(height: 16),
// Downloaded Tracks List
Expanded(
child: _downloadedTracks.isEmpty
? _buildEmptyState()
: _buildDownloadsList(),
),
],
),
),
),
],
),
);
}
Widget _buildStatCard(String title, String value, IconData icon) {
return Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8.0),
),
child: Column(
children: [
Icon(
icon,
color: Theme.of(context).colorScheme.onPrimaryContainer,
size: 24,
),
const SizedBox(height: 8),
Text(
title,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.download_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No downloads yet',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _simulateDownload,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.download),
const SizedBox(width: 8),
const Text('Download Sample Track'),
],
),
),
],
),
);
}
Widget _buildDownloadsList() {
return ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _downloadedTracks.length,
itemBuilder: (context, index) {
final track = _downloadedTracks[index];
return Card(
margin: const EdgeInsets.only(bottom: 8.0),
child: ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: track['isOffline'] == true
? const Icon(Icons.offline_pin, color: Colors.green)
: const Icon(Icons.music_note, color: Colors.blue),
),
),
title: Text(
track['title']?.toString() ?? 'Unknown Track',
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(
track['artist']?.toString() ?? 'Unknown Artist',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatFileSize(track['size']?.toDouble() ?? 0.0),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
PopupMenuButton<String>(
onSelected: (value) {
_handleTrackAction(track, value);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'play',
child: Row(
children: [
const Icon(Icons.play_arrow, size: 16),
const SizedBox(width: 8),
const Text('Play'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Icons.delete, size: 16, color: Colors.red),
const SizedBox(width: 8),
const Text('Delete'),
],
),
),
],
),
],
),
),
);
},
);
}
String _formatFileSize(double bytes) {
if (bytes < 1024) {
return '${bytes.toStringAsFixed(0)} B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
void _clearDownloads() {
setState(() {
_downloadedTracks.clear();
_totalDownloads = 0;
_completedDownloads = 0;
_totalDownloadSize = 0.0;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('All downloads cleared')),
);
}
void _simulateDownload() {
setState(() {
_totalDownloads++;
_completedDownloads++;
final trackSize = 5.2 * 1024 * 1024; // 5.2 MB
_totalDownloadSize += trackSize;
_downloadedTracks.add({
'id': 'track_${_totalDownloads}',
'title': 'Sample Track $_totalDownloads',
'artist': 'Sample Artist',
'album': 'Sample Album',
'duration': '3:45',
'size': trackSize,
'isOffline': true,
'downloadDate': DateTime.now().toIso8601String(),
});
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sample track downloaded')),
);
}
void _handleTrackAction(Map<String, dynamic> track, String action) {
switch (action) {
case 'play':
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Playing: ${track['title']}')),
);
break;
case 'delete':
setState(() {
_downloadedTracks.remove(track);
_totalDownloads--;
if (track['isOffline'] == true) {
_completedDownloads--;
_totalDownloadSize -= (track['size'] ?? 0.0);
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted: ${track['title']}')),
);
break;
}
}
}
@@ -1,427 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../core/constants/app_spacing.dart';
import '../../core/enums/playback_mode.dart';
class EnhancedPlayerScreen extends StatefulWidget {
const EnhancedPlayerScreen({super.key});
@override
State<EnhancedPlayerScreen> createState() => _EnhancedPlayerScreenState();
}
class _EnhancedPlayerScreenState extends State<EnhancedPlayerScreen> {
@override
Widget build(BuildContext context) {
return Consumer<AudioProvider>(
builder: (context, audioProvider, child) {
final currentTrack = audioProvider.currentTrack;
if (currentTrack == null) {
return _buildEmptyPlayer();
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface.withOpacity(0.95),
],
),
),
child: SafeArea(
child: Column(
children: [
// Top Bar
Padding(
padding: AppSpacing.horizontalMD,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
'Now Playing',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
onPressed: () => _showMoreOptions(context),
icon: Icon(
Icons.more_vert,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
// Album Artwork
Expanded(
flex: 3,
child: Padding(
padding: AppSpacing.horizontalLG,
child: _buildAlbumArtwork(context, currentTrack),
),
),
// Track Info & Controls
Expanded(
flex: 2,
child: Padding(
padding: AppSpacing.horizontalLG,
child: Column(
children: [
// Track Info
_buildTrackInfo(context, currentTrack),
const SizedBox(height: 24),
// Progress Bar
_buildProgressBar(context, audioProvider),
const SizedBox(height: 24),
// Playback Controls
_buildPlaybackControls(context, audioProvider),
const SizedBox(height: 16),
// Bottom Controls
_buildBottomControls(context, audioProvider),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildAlbumArtwork(BuildContext context, dynamic currentTrack) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: currentTrack.image.isNotEmpty
? Image.network(
currentTrack.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAlbumArt(context);
},
)
: _buildDefaultAlbumArt(context),
),
);
}
Widget _buildTrackInfo(BuildContext context, dynamic currentTrack) {
return Column(
children: [
Text(
currentTrack.displayTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
currentTrack.artistNames,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (currentTrack.displayAlbum.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
currentTrack.displayAlbum,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
);
}
Widget _buildProgressBar(BuildContext context, AudioProvider audioProvider) {
return Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
trackHeight: 4,
activeTrackColor: Theme.of(context).colorScheme.primary,
inactiveTrackColor: Theme.of(context).colorScheme.surfaceVariant,
),
child: Slider(
value: audioProvider.progress,
onChanged: (value) {
final newPosition = Duration(
milliseconds: (value * audioProvider.duration.inMilliseconds).round(),
);
audioProvider.seekTo(newPosition);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
audioProvider.positionFormatted,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
Text(
audioProvider.durationFormatted,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
],
),
),
],
);
}
Widget _buildPlaybackControls(BuildContext context, AudioProvider audioProvider) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Shuffle
IconButton(
onPressed: () => audioProvider.toggleShuffle(),
icon: Icon(
Icons.shuffle,
color: audioProvider.shuffleMode == ShuffleMode.on
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Previous
IconButton(
onPressed: () => audioProvider.playPrevious(),
icon: Icon(
Icons.skip_previous,
color: Theme.of(context).colorScheme.onSurface,
),
iconSize: 40,
),
// Play/Pause with buffering indicator
_buildPlayPauseButton(context, audioProvider),
// Next
IconButton(
onPressed: () => audioProvider.playNext(),
icon: Icon(
Icons.skip_next,
color: Theme.of(context).colorScheme.onSurface,
),
iconSize: 40,
),
// Repeat
IconButton(
onPressed: () => audioProvider.toggleRepeat(),
icon: _getRepeatIcon(audioProvider.repeatMode),
color: audioProvider.repeatMode != RepeatMode.off
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
],
);
}
Widget _buildPlayPauseButton(BuildContext context, AudioProvider audioProvider) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Stack(
alignment: Alignment.center,
children: [
IconButton(
onPressed: () {
if (audioProvider.isPlaying) {
audioProvider.pause();
} else {
audioProvider.play();
}
},
icon: Icon(
audioProvider.isPlaying ? Icons.pause : Icons.play_arrow,
color: Theme.of(context).colorScheme.onPrimary,
size: 48,
),
),
if (audioProvider.isBuffering)
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
),
child: const CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
],
),
);
}
Widget _buildBottomControls(BuildContext context, AudioProvider audioProvider) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Favorite
IconButton(
onPressed: () => _toggleFavorite(context, audioProvider),
icon: Icon(
audioProvider.currentTrack?.isFavorite == true
? Icons.favorite
: Icons.favorite_border,
color: audioProvider.currentTrack?.isFavorite == true
? Colors.red
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Queue
IconButton(
onPressed: () => _showQueue(context),
icon: Icon(
Icons.queue_music,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Volume
IconButton(
onPressed: () => _showVolumeSlider(context, audioProvider),
icon: Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
}
IconData _getRepeatIcon(RepeatMode mode) {
switch (mode) {
case RepeatMode.one:
return Icons.repeat_one;
case RepeatMode.all:
return Icons.repeat;
case RepeatMode.off:
default:
return Icons.repeat;
}
}
void _toggleFavorite(BuildContext context, AudioProvider audioProvider) {
// Toggle favorite functionality
}
void _showQueue(BuildContext context) {
// Show queue functionality
}
void _showVolumeSlider(BuildContext context, AudioProvider audioProvider) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: AppSpacing.paddingLG,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Volume',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Slider(
value: audioProvider.volume,
onChanged: (value) => audioProvider.setVolume(value),
min: 0.0,
max: 1.0,
),
],
),
),
);
}
void _showMoreOptions(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: AppSpacing.large,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.share),
title: const Text('Share'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.playlist_add),
title: const Text('Add to Playlist'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Track Info'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
@@ -1,489 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../core/constants/app_spacing.dart';
import '../../core/enums/playback_mode.dart';
class EnhancedPlayerScreen extends StatefulWidget {
const EnhancedPlayerScreen({super.key});
@override
State<EnhancedPlayerScreen> createState() => _EnhancedPlayerScreenState();
}
class _EnhancedPlayerScreenState extends State<EnhancedPlayerScreen> {
@override
Widget build(BuildContext context) {
return Consumer<AudioProvider>(
builder: (context, audioProvider, child) {
final currentTrack = audioProvider.currentTrack;
if (currentTrack == null) {
return _buildEmptyPlayer();
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface.withOpacity(0.95),
],
),
),
child: SafeArea(
child: Column(
children: [
// Top Bar
Padding(
padding: AppSpacing.horizontalMD,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
'Now Playing',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
IconButton(
onPressed: () => _showMoreOptions(context),
icon: Icon(
Icons.more_vert,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
// Album Artwork
Expanded(
flex: 3,
child: Padding(
padding: AppSpacing.horizontalLG,
child: _buildAlbumArtwork(context, currentTrack),
),
),
// Track Info & Controls
Expanded(
flex: 2,
child: Padding(
padding: AppSpacing.horizontalLG,
child: Column(
children: [
// Track Info
_buildTrackInfo(context, currentTrack),
const SizedBox(height: 24),
// Progress Bar
_buildProgressBar(context, audioProvider),
const SizedBox(height: 24),
// Playback Controls
_buildPlaybackControls(context, audioProvider),
const SizedBox(height: 16),
// Bottom Controls
_buildBottomControls(context, audioProvider),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildAlbumArtwork(BuildContext context, dynamic currentTrack) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: currentTrack.image.isNotEmpty
? Image.network(
currentTrack.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAlbumArt(context);
},
)
: _buildDefaultAlbumArt(context),
),
);
}
Widget _buildTrackInfo(BuildContext context, dynamic currentTrack) {
return Column(
children: [
Text(
currentTrack.displayTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
currentTrack.artistNames,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (currentTrack.displayAlbum.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
currentTrack.displayAlbum,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
);
}
Widget _buildProgressBar(BuildContext context, AudioProvider audioProvider) {
return Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
trackHeight: 4,
activeTrackColor: Theme.of(context).colorScheme.primary,
inactiveTrackColor: Theme.of(context).colorScheme.surfaceVariant,
),
child: Slider(
value: audioProvider.progress,
onChanged: (value) {
final newPosition = Duration(
milliseconds: (value * audioProvider.duration.inMilliseconds).round(),
);
audioProvider.seekTo(newPosition);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
audioProvider.positionFormatted,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
Text(
audioProvider.durationFormatted,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
],
),
),
],
);
}
Widget _buildPlaybackControls(BuildContext context, AudioProvider audioProvider) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Shuffle
IconButton(
onPressed: () => audioProvider.toggleShuffle(),
icon: Icon(
Icons.shuffle,
color: audioProvider.shuffleMode == ShuffleMode.on
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Previous
IconButton(
onPressed: () => audioProvider.playPrevious(),
icon: Icon(
Icons.skip_previous,
color: Theme.of(context).colorScheme.onSurface,
),
iconSize: 40,
),
// Play/Pause with buffering indicator
_buildPlayPauseButton(context, audioProvider),
// Next
IconButton(
onPressed: () => audioProvider.playNext(),
icon: Icon(
Icons.skip_next,
color: Theme.of(context).colorScheme.onSurface,
),
iconSize: 40,
),
// Repeat
IconButton(
onPressed: () => audioProvider.toggleRepeat(),
icon: _getRepeatIcon(audioProvider.repeatMode),
color: audioProvider.repeatMode != RepeatMode.off
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
],
);
}
Widget _buildPlayPauseButton(BuildContext context, AudioProvider audioProvider) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Stack(
alignment: Alignment.center,
children: [
IconButton(
onPressed: () {
if (audioProvider.isPlaying) {
audioProvider.pause();
} else {
audioProvider.play();
}
},
icon: Icon(
audioProvider.isPlaying ? Icons.pause : Icons.play_arrow,
color: Theme.of(context).colorScheme.onPrimary,
size: 48,
),
),
if (audioProvider.isBuffering)
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
),
child: const CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
],
),
);
}
Widget _buildBottomControls(BuildContext context, AudioProvider audioProvider) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Favorite
IconButton(
onPressed: () => _toggleFavorite(context, audioProvider),
icon: Icon(
audioProvider.currentTrack?.isFavorite == true
? Icons.favorite
: Icons.favorite_border,
color: audioProvider.currentTrack?.isFavorite == true
? Colors.red
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Queue
IconButton(
onPressed: () => _showQueue(context),
icon: Icon(
Icons.queue_music,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// Volume
IconButton(
onPressed: () => _showVolumeSlider(context, audioProvider),
icon: Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
// More options
IconButton(
onPressed: () => _showMoreOptions(context),
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
],
);
}
Widget _buildDefaultAlbumArt(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: Icon(
Icons.album,
size: 120,
color: Theme.of(context).colorScheme.onPrimary,
),
);
}
Widget _buildEmptyPlayer() {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 64,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
'No track playing',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'Select a track from your library to start playing',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
IconData _getRepeatIcon(RepeatMode mode) {
switch (mode) {
case RepeatMode.one:
return Icons.repeat_one;
case RepeatMode.all:
return Icons.repeat;
case RepeatMode.off:
return Icons.repeat;
}
}
void _toggleFavorite(BuildContext context, AudioProvider audioProvider) {
// Toggle favorite functionality
}
void _showQueue(BuildContext context) {
// Show queue functionality
}
void _showVolumeSlider(BuildContext context, AudioProvider audioProvider) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: AppSpacing.paddingLG,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Volume',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Slider(
value: audioProvider.volume,
onChanged: (value) => audioProvider.setVolume(value),
min: 0.0,
max: 1.0,
),
],
),
),
);
}
void _showMoreOptions(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: AppSpacing.paddingLG,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.share),
title: const Text('Share'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.playlist_add),
title: const Text('Add to Playlist'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Track Info'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
}
@@ -1,373 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../core/constants/app_spacing.dart';
class PlayerScreen extends StatefulWidget {
const PlayerScreen({super.key});
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
@override
void initState() {
super.initState();
// Initialize audio service if not already done
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AudioProvider>(context, listen: false).initialize();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<AudioProvider>(
builder: (context, audioProvider, child) {
final currentTrack = audioProvider.currentTrack;
if (currentTrack == null) {
return _buildEmptyPlayer();
}
return Column(
children: [
// App Bar
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.keyboard_arrow_down),
),
Text(
'Now Playing',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () {
// Show more options
},
icon: const Icon(Icons.more_vert),
),
],
),
),
),
// Album Art
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: currentTrack.image.isNotEmpty
? Image.network(
currentTrack.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAlbumArt(context);
},
)
: _buildDefaultAlbumArt(context),
),
),
),
),
),
// Track Info
Expanded(
flex: 1,
child: Padding(
padding: AppSpacing.horizontalXL,
child: Column(
children: [
Text(
currentTrack.displayTitle,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
currentTrack.artistNames,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (currentTrack.displayAlbum.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
currentTrack.displayAlbum,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
// Progress Bar
Padding(
padding: AppSpacing.horizontalXL,
child: Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
trackHeight: 4,
),
child: Slider(
value: audioProvider.progress,
onChanged: (value) {
final newPosition = Duration(
milliseconds: (value * audioProvider.duration.inMilliseconds).round(),
);
audioProvider.seekTo(newPosition);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
audioProvider.positionFormatted,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
audioProvider.durationFormatted,
style: Theme.of(context).textTheme.bodySmall,
),
// Volume Control
Expanded(
child: Row(
children: [
Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
Expanded(
child: Slider(
value: audioProvider.volume,
onChanged: (value) {
audioProvider.setVolume(value);
},
min: 0.0,
max: 1.0,
),
),
],
),
),
],
),
),
],
),
),
// Playback Controls
Expanded(
flex: 1,
child: Padding(
padding: AppSpacing.horizontalXL,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Shuffle
IconButton(
onPressed: () => audioProvider.toggleShuffle(),
icon: Icon(
Icons.shuffle,
color: audioProvider.isShuffleMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
// Previous
IconButton(
onPressed: () => audioProvider.playPrevious(),
icon: const Icon(Icons.skip_previous),
iconSize: 32,
),
// Play/Pause
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: IconButton(
onPressed: () {
if (audioProvider.isPlaying) {
audioProvider.pause();
} else {
audioProvider.play();
}
},
icon: audioProvider.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Icon(
audioProvider.isPlaying ? Icons.pause : Icons.play_arrow,
color: Theme.of(context).colorScheme.onPrimary,
size: 32,
),
),
),
// Next
IconButton(
onPressed: () => audioProvider.playNext(),
icon: const Icon(Icons.skip_next),
iconSize: 32,
),
// Repeat
IconButton(
onPressed: () => audioProvider.toggleRepeat(),
icon: Icon(
audioProvider.isRepeatMode ? Icons.repeat_one : Icons.repeat,
color: audioProvider.isRepeatMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
// Bottom Controls
Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
// Add to favorites
},
icon: const Icon(Icons.favorite_border),
),
IconButton(
onPressed: () {
// Show playlist
},
icon: const Icon(Icons.playlist_play),
),
IconButton(
onPressed: () {
// Share
},
icon: const Icon(Icons.share),
),
],
),
),
],
);
},
),
);
}
Widget _buildEmptyPlayer() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_note,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No track playing',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Select a track from your library to start playing',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildDefaultAlbumArt(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.7),
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
],
),
),
child: Icon(
Icons.album,
size: 120,
color: Theme.of(context).colorScheme.onPrimary,
),
);
}
}
@@ -1,437 +0,0 @@
import 'package:flutter/material.dart';
import '../../data/models/playlist_model.dart';
class PlaylistsScreen extends StatefulWidget {
const PlaylistsScreen({super.key});
@override
State<PlaylistsScreen> createState() => _PlaylistsScreenState();
}
class _PlaylistsScreenState extends State<PlaylistsScreen> {
List<PlaylistModel> _playlists = [];
bool _isLoading = false;
final TextEditingController _playlistNameController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadPlaylists();
}
@override
void dispose() {
_playlistNameController.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _loadPlaylists() async {
setState(() {
_isLoading = true;
});
// Simulate API call
await Future.delayed(const Duration(seconds: 1));
setState(() {
_isLoading = false;
// Sample playlists for demo
_playlists = [
PlaylistModel(
id: '1',
name: 'My Favorites',
description: 'My favorite tracks',
trackcount: 25,
isPublic: false,
createdDate: DateTime.now().subtract(const Duration(days: 30)),
lastModified: DateTime.now().subtract(const Duration(days: 5)),
),
PlaylistModel(
id: '2',
name: 'Workout Mix',
description: 'High energy tracks for workouts',
trackcount: 18,
isPublic: false,
createdDate: DateTime.now().subtract(const Duration(days: 15)),
lastModified: DateTime.now().subtract(const Duration(days: 2)),
),
PlaylistModel(
id: '3',
name: 'Chill Vibes',
description: 'Relaxing and focus music',
trackcount: 32,
isPublic: true,
createdDate: DateTime.now().subtract(const Duration(days: 7)),
lastModified: DateTime.now().subtract(const Duration(days: 1)),
),
PlaylistModel(
id: '4',
name: 'Road Trip Classics',
description: 'Classic hits for long drives',
trackcount: 45,
isPublic: false,
createdDate: DateTime.now().subtract(const Duration(days: 90)),
lastModified: DateTime.now().subtract(const Duration(days: 10)),
),
];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Playlists'),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
actions: [
IconButton(
onPressed: _showCreatePlaylistDialog,
icon: const Icon(Icons.add),
),
],
),
body: Column(
children: [
// Search Bar
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search playlists...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
_filterPlaylists('');
},
icon: const Icon(Icons.clear),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_filterPlaylists(value);
},
),
),
// Playlists Grid
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _playlists.isEmpty
? _buildEmptyState()
: _buildPlaylistsGrid(),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showCreatePlaylistDialog,
child: const Icon(Icons.add),
),
);
}
Widget _buildPlaylistsGrid() {
return GridView.builder(
padding: const EdgeInsets.all(16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _playlists.length,
itemBuilder: (context, index) {
final playlist = _playlists[index];
return Card(
child: InkWell(
onTap: () => _openPlaylist(playlist),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Playlist Cover
Container(
width: double.infinity,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withValues(alpha: 0.8),
Theme.of(context).colorScheme.secondary.withValues(alpha: 0.8),
],
),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
// Playlist Icon
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.playlist_play,
color: Colors.white,
size: 20,
),
),
),
// Public/Private Badge
if (playlist.isPublic)
Positioned(
bottom: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'PUBLIC',
style: const TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
const SizedBox(height: 12),
// Playlist Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
playlist.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
playlist.description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'${playlist.trackcount} tracks',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 4),
Text(
_formatDate(playlist.lastModified),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.playlist_add,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No playlists yet',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Create your first playlist to get started',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _showCreatePlaylistDialog,
icon: const Icon(Icons.add),
label: const Text('Create Playlist'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
),
);
}
void _filterPlaylists(String query) {
setState(() {
if (query.isEmpty) {
// Reset to all playlists
_loadPlaylists();
} else {
// Filter playlists (in real app, this would call API)
_playlists = _playlists.where((playlist) =>
playlist.name.toLowerCase().contains(query.toLowerCase())
).toList();
}
});
}
void _openPlaylist(PlaylistModel playlist) {
Navigator.pushNamed(context, '/playlist', arguments: playlist);
}
void _showCreatePlaylistDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Create Playlist'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _playlistNameController,
decoration: const InputDecoration(
labelText: 'Playlist Name',
border: OutlineInputBorder(),
),
autofocus: true,
),
const SizedBox(height: 16),
TextField(
decoration: const InputDecoration(
labelText: 'Description (Optional)',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _createPlaylist,
child: const Text('Create'),
),
],
),
);
}
void _createPlaylist() async {
final name = _playlistNameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a playlist name')),
);
return;
}
// Create playlist (in real app, this would call API)
final newPlaylist = PlaylistModel(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: name,
description: '',
trackcount: 0,
isPublic: false,
createdDate: DateTime.now(),
lastModified: DateTime.now(),
);
setState(() {
_playlists.insert(0, newPlaylist);
});
_playlistNameController.clear();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Playlist "$name" created successfully')),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else if (difference.inDays < 30) {
return '${(difference.inDays / 7).floor()} weeks ago';
} else {
return '${(difference.inDays / 30).floor()} months ago';
}
}
}
@@ -1,292 +0,0 @@
import 'package:flutter/material.dart';
class QRScreen extends StatefulWidget {
const QRScreen({super.key});
@override
State<QRScreen> createState() => _QRScreenState();
}
class _QRScreenState extends State<QRScreen> {
String _qrCode = '';
bool _isGenerating = false;
bool _isScanning = false;
final TextEditingController _qrController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('QR Code Pairing'),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
leading: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back),
),
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// QR Code Display
Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 2.0,
),
),
child: Column(
children: [
const Icon(
Icons.qr_code_scanner,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 16),
Text(
'QR Code Pairing',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Scan or generate a QR code to connect',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// QR Code Visual
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.0),
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 2.0,
),
),
child: _isGenerating
? const Center(
child: CircularProgressIndicator(),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 180,
height: 180,
color: Colors.black,
child: const Center(
child: Text(
'QR CODE',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 16),
Text(
_qrCode.isEmpty ? 'Generate QR Code' : _qrCode,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 24),
// Action Buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ElevatedButton(
onPressed: _isScanning ? null : _toggleScanning,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.qr_code_scanner),
const SizedBox(width: 8),
const Text('Scan QR Code'),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isScanning ? null : _generateQRCode,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.qr_code),
const SizedBox(width: 8),
const Text('Generate QR'),
],
),
),
),
],
),
const SizedBox(height: 16),
// Manual Entry
TextField(
controller: _qrController,
decoration: InputDecoration(
labelText: 'Or enter QR code manually',
hintText: 'Enter QR code',
prefixIcon: const Icon(Icons.keyboard),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
),
onChanged: (value) {
setState(() {
_qrCode = value;
});
},
),
const SizedBox(height: 24),
// Connect Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _qrCode.isEmpty ? null : _connectWithQR,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Connect'),
),
),
],
),
),
],
),
),
);
}
void _toggleScanning() {
setState(() {
_isScanning = !_isScanning;
});
if (_isScanning) {
_scanQRCode();
}
}
void _generateQRCode() {
setState(() {
_isGenerating = true;
});
// Generate a random QR code for demo
Future.delayed(const Duration(seconds: 2)).then((_) {
setState(() {
_isGenerating = false;
_qrCode = 'SWING-${DateTime.now().millisecondsSinceEpoch % 10000}';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('QR Code generated!')),
);
});
}
void _scanQRCode() async {
try {
// In a real app, this would use camera plugin
// For demo, we'll simulate scanning
await Future.delayed(const Duration(seconds: 3));
setState(() {
_isScanning = false;
_qrCode = 'DEMO-SCANNED-${DateTime.now().millisecondsSinceEpoch}';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('QR Code scanned!')),
);
} catch (e) {
setState(() {
_isScanning = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Scan failed: ${e.toString()}')),
);
}
}
void _connectWithQR() async {
if (_qrCode.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter or scan a QR code')),
);
return;
}
setState(() {
_isGenerating = true;
});
try {
// Simulate connection with QR code
await Future.delayed(const Duration(seconds: 2));
setState(() {
_isGenerating = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Connected with QR Code!')),
);
// Navigate to main app
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
} catch (e) {
setState(() {
_isGenerating = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Connection failed: ${e.toString()}')),
);
}
}
}
@@ -1,523 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/audio_provider.dart';
import '../../core/widgets/album_card.dart';
import '../../core/widgets/track_list_tile.dart';
import '../../data/models/track_model.dart';
import '../../data/models/album_model.dart';
import '../../data/models/artist_model.dart' as artist_model;
import '../../data/models/search_suggestion_model.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> with TickerProviderStateMixin {
late TabController _tabController;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
// Search results
final List<TrackModel> _trackResults = [];
final List<AlbumModel> _albumResults = [];
final List<artist_model.ArtistModel> _artistResults = [];
bool _isSearching = false;
String _currentQuery = '';
// Search filters
String _selectedFilter = 'all'; // 'all', 'tracks', 'albums', 'artists'
final List<String> _searchFilters = ['all', 'tracks', 'albums', 'artists'];
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_searchFocusNode.requestFocus();
}
@override
void dispose() {
_tabController.dispose();
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Search Header
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// Search Bar
TextField(
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: _onSearchChanged,
onSubmitted: _onSearchSubmitted,
decoration: InputDecoration(
hintText: 'Search tracks, albums, artists...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: _clearSearch,
icon: const Icon(Icons.clear),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
const SizedBox(height: 12),
// Filter Chips
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _searchFilters.map((filter) {
final isSelected = _selectedFilter == filter;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(filter.toUpperCase()),
selected: isSelected,
onSelected: (selected) {
setState(() {
_selectedFilter = filter;
});
_performSearch(_currentQuery);
},
backgroundColor: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
labelStyle: TextStyle(
color: isSelected
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}).toList(),
),
),
],
),
),
// Search Results
Expanded(
child: _isSearching
? const Center(child: CircularProgressIndicator())
: _currentQuery.isEmpty
? _buildSearchSuggestions()
: _buildSearchResults(),
),
],
),
);
}
Widget _buildSearchSuggestions() {
return DefaultTabController(
length: 4,
child: Column(
children: [
TabBar(
controller: _tabController,
isScrollable: true,
tabs: const [
Tab(text: 'Top'),
Tab(text: 'Tracks'),
Tab(text: 'Albums'),
Tab(text: 'Artists'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildTopSearches(),
_buildRecentSearches(),
_buildBrowseGenres(),
_buildBrowseFolders(),
],
),
),
],
),
);
}
Widget _buildTopSearches() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Top Searches',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Sample top searches
...['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical'].map((genre) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
Icons.trending_up,
color: Theme.of(context).colorScheme.primary,
),
title: Text(genre),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
_searchController.text = genre;
_performSearch(genre);
},
),
);
}),
],
),
);
}
Widget _buildRecentSearches() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recent Searches',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
// Sample recent searches
...['Beatles', 'Queen', 'Pink Floyd', 'Led Zeppelin'].map((search) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(
Icons.history,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
title: Text(search),
trailing: IconButton(
onPressed: () {
_searchController.text = search;
_performSearch(search);
},
icon: const Icon(Icons.arrow_forward_ios),
),
onTap: () {
_searchController.text = search;
_performSearch(search);
},
),
);
}),
],
),
);
}
Widget _buildBrowseGenres() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: ['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical', 'Hip Hop', 'Country', 'R&B'].length,
itemBuilder: (context, index) {
final genre = ['Rock', 'Pop', 'Jazz', 'Electronic', 'Classical', 'Hip Hop', 'Country', 'R&B'][index];
return Card(
child: InkWell(
onTap: () {
_searchController.text = genre;
_performSearch(genre);
},
borderRadius: BorderRadius.circular(8),
child: Center(
child: Text(
genre,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
),
);
},
),
);
}
Widget _buildBrowseFolders() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_outlined,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'Folder browsing coming soon',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
Widget _buildSearchResults() {
if (_selectedFilter == 'all') {
return DefaultTabController(
length: 3,
child: Column(
children: [
TabBar(
tabs: const [
Tab(text: 'Tracks'),
Tab(text: 'Albums'),
Tab(text: 'Artists'),
],
),
Expanded(
child: TabBarView(
children: [
_buildTrackResults(),
_buildAlbumResults(),
_buildArtistResults(),
],
),
),
],
),
);
} else if (_selectedFilter == 'tracks') {
return _buildTrackResults();
} else if (_selectedFilter == 'albums') {
return _buildAlbumResults();
} else {
return _buildArtistResults();
}
}
Widget _buildTrackResults() {
if (_trackResults.isEmpty) {
return _buildEmptyResults('No tracks found');
}
return ListView.builder(
itemCount: _trackResults.length,
itemBuilder: (context, index) {
final track = _trackResults[index];
return TrackListTile(
track: track,
onTap: () => _playTrack(track),
onPlay: () => _playTrack(track),
);
},
);
}
Widget _buildAlbumResults() {
if (_albumResults.isEmpty) {
return _buildEmptyResults('No albums found');
}
return GridView.builder(
padding: const EdgeInsets.all(16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _albumResults.length,
itemBuilder: (context, index) {
final album = _albumResults[index];
return AlbumCard(
album: album,
onTap: () {
// Navigate to album details
},
);
},
);
}
Widget _buildArtistResults() {
if (_artistResults.isEmpty) {
return _buildEmptyResults('No artists found');
}
return ListView.builder(
itemCount: _artistResults.length,
itemBuilder: (context, index) {
final artist = _artistResults[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
artist.name.isNotEmpty ? artist.name[0].toUpperCase() : '?',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
),
title: Text(artist.name),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Navigate to artist details
},
);
},
);
}
Widget _buildEmptyResults(String message) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
'Try different keywords or browse categories',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
void _onSearchChanged(String query) {
_currentQuery = query;
if (query.isEmpty) {
setState(() {
_isSearching = false;
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
} else {
// Debounce search
Future.delayed(const Duration(milliseconds: 500), () {
if (_searchController.text == query) {
_performSearch(query);
}
});
}
}
void _onSearchSubmitted(String query) {
_performSearch(query);
_searchFocusNode.unfocus();
}
void _clearSearch() {
_searchController.clear();
setState(() {
_currentQuery = '';
_isSearching = false;
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
}
Future<void> _performSearch(String query) async {
if (query.trim().isEmpty) return;
setState(() {
_isSearching = true;
});
try {
// Simulate API call
await Future.delayed(const Duration(milliseconds: 800));
// This would be actual API calls
setState(() {
_isSearching = false;
// For demo, just clear results
_trackResults.clear();
_albumResults.clear();
_artistResults.clear();
});
} catch (e) {
setState(() {
_isSearching = false;
});
// Handle error
}
}
void _playTrack(TrackModel track) {
final audioProvider = Provider.of<AudioProvider>(context, listen: false);
audioProvider.setQueue([track]);
audioProvider.loadTrack(track);
audioProvider.play();
Navigator.pushNamed(context, '/player');
}
}
@@ -1,845 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/auth_provider.dart';
import '../../shared/providers/enhanced_library_provider.dart';
import '../../core/constants/app_spacing.dart';
class EnhancedSettingsScreen extends StatefulWidget {
const EnhancedSettingsScreen({super.key});
@override
State<EnhancedSettingsScreen> createState() => _EnhancedSettingsScreenState();
}
class _EnhancedSettingsScreenState extends State<EnhancedSettingsScreen> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0,
title: Text(
'Settings',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
body: SingleChildScrollView(
padding: AppSpacing.paddingLG,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Connection Settings
_buildSection(
context,
'Connection',
Icons.cloud,
[
_buildServerUrlTile(context),
_buildAuthStatusTile(context),
_buildConnectionTestTile(context),
],
),
const SizedBox(height: 24),
// Audio Settings
_buildSection(
context,
'Audio',
Icons.music_note,
[
_buildAudioQualityTile(context),
_buildCrossfadeTile(context),
_buildGaplessTile(context),
_buildVolumeTile(context),
],
),
const SizedBox(height: 24),
// Download Settings
_buildSection(
context,
'Downloads',
Icons.download,
[
_buildDownloadQualityTile(context),
_buildDownloadLocationTile(context),
_buildWifiOnlyTile(context),
_buildMaxDownloadSizeTile(context),
],
),
const SizedBox(height: 24),
// Theme Settings
_buildSection(
context,
'Appearance',
Icons.palette,
[
_buildThemeTile(context),
_buildAccentColorTile(context),
],
),
const SizedBox(height: 24),
// Cache Settings
_buildSection(
context,
'Storage',
Icons.storage,
[
_buildCacheSizeTile(context),
_buildClearCacheTile(context),
],
),
const SizedBox(height: 24),
// About
_buildSection(
context,
'About',
Icons.info,
[
_buildVersionTile(context),
_buildBuildNumberTile(context),
_buildDeveloperTile(context),
],
),
],
),
),
);
}
Widget _buildSection(
BuildContext context,
String title,
IconData icon,
List<Widget> children,
) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
icon,
color: Theme.of(context).colorScheme.primary,
size: 20,
),
const SizedBox(width: 12),
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
...children,
],
),
);
}
Widget _buildServerUrlTile(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
return ListTile(
leading: Icon(
Icons.link,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Server URL',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
authProvider.baseUrl ?? 'Not set',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: IconButton(
onPressed: () => _showServerUrlDialog(context, authProvider),
icon: Icon(
Icons.edit,
color: Theme.of(context).colorScheme.primary,
),
),
);
},
);
}
Widget _buildAuthStatusTile(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
return ListTile(
leading: Icon(
authProvider.isLoggedIn ? Icons.check_circle : Icons.error,
color: authProvider.isLoggedIn
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.error,
),
title: Text(
'Authentication Status',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
authProvider.isLoggedIn ? 'Connected' : 'Not connected',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: authProvider.isLoggedIn
? Theme.of(context).colorScheme.onSurface.withOpacity(0.7)
: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: authProvider.isLoggedIn
? IconButton(
onPressed: () => _showLogoutDialog(context, authProvider),
icon: Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
)
: null,
);
},
);
}
Widget _buildConnectionTestTile(BuildContext context) {
return ListTile(
leading: Icon(
Icons.wifi,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Test Connection',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Check server connectivity',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: IconButton(
onPressed: () => _testConnection(context),
icon: Icon(
Icons.play_arrow,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
Widget _buildAudioQualityTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.high_quality,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Audio Quality',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Higher quality uses more storage',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['audioQuality'] ?? '320kbps',
items: const ['128kbps', '320kbps', '512kbps', 'flac'],
onChanged: (value) => libraryProvider.updateUserPreferences({
'audioQuality': value,
}),
),
);
},
);
}
Widget _buildCrossfadeTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.blur_on,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Crossfade',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Smooth transitions between tracks',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: Switch(
value: libraryProvider.userPreferences['crossfade'] ?? false,
onChanged: (value) => libraryProvider.updateUserPreferences({
'crossfade': value,
}),
),
);
},
);
}
Widget _buildGaplessTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.skip_next,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Gapless Playback',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Remove silence between tracks',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: Switch(
value: libraryProvider.userPreferences['gapless'] ?? false,
onChanged: (value) => libraryProvider.updateUserPreferences({
'gapless': value,
}),
),
);
},
);
}
Widget _buildVolumeTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.volume_up,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Default Volume',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Set default volume level',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<double>(
value: (libraryProvider.userPreferences['defaultVolume'] ?? 1.0).toDouble(),
items: [0.25, 0.5, 0.75, 1.0],
onChanged: (value) => libraryProvider.updateUserPreferences({
'defaultVolume': value,
}),
),
);
},
);
}
Widget _buildDownloadQualityTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.download,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Download Quality',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Choose audio quality for downloads',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['downloadQuality'] ?? '320kbps',
items: const ['128kbps', '320kbps', '512kbps', 'flac'],
onChanged: (value) => libraryProvider.updateUserPreferences({
'downloadQuality': value,
}),
),
);
},
);
}
Widget _buildDownloadLocationTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.folder,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Download Location',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Where to save downloaded files',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['downloadLocation'] ?? 'Music',
items: const ['Music', 'Downloads', 'Custom'],
onChanged: (value) => libraryProvider.updateUserPreferences({
'downloadLocation': value,
}),
),
);
},
);
}
Widget _buildWifiOnlyTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.wifi,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Wi-Fi Only',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Download only when connected to Wi-Fi',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: Switch(
value: libraryProvider.userPreferences['wifiOnly'] ?? false,
onChanged: (value) => libraryProvider.updateUserPreferences({
'wifiOnly': value,
}),
),
);
},
);
}
Widget _buildMaxDownloadSizeTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.sd_storage,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Max Download Size',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Maximum size for automatic downloads',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['maxDownloadSize'] ?? '100MB',
items: const ['50MB', '100MB', '500MB', '1GB'],
onChanged: (value) => libraryProvider.updateUserPreferences({
'maxDownloadSize': value,
}),
),
);
},
);
}
Widget _buildThemeTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.palette,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Theme',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Choose app appearance',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<ThemeMode>(
value: _getThemeMode(libraryProvider.userPreferences['theme']),
items: const [
DropdownMenuItem(value: ThemeMode.system, child: Text('System')),
DropdownMenuItem(value: ThemeMode.light, child: Text('Light')),
DropdownMenuItem(value: ThemeMode.dark, child: Text('Dark')),
],
onChanged: (ThemeMode? value) => libraryProvider.updateUserPreferences({
'theme': value?.name,
}),
),
);
},
);
}
Widget _buildAccentColorTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.color_lens,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Accent Color',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Customize accent colors',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['accentColor'] ?? 'Blue',
items: const [
DropdownMenuItem(value: 'Blue', child: Text('Blue')),
DropdownMenuItem(value: 'Green', child: Text('Green')),
DropdownMenuItem(value: 'Purple', child: Text('Purple')),
DropdownMenuItem(value: 'Orange', child: Text('Orange')),
DropdownMenuItem(value: 'Red', child: Text('Red')),
],
onChanged: (String? value) => libraryProvider.updateUserPreferences({
'accentColor': value,
}),
),
);
},
);
}
Widget _buildCacheSizeTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.cached,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Cache Size',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Maximum cache size',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: DropdownButton<String>(
value: libraryProvider.userPreferences['cacheSize'] ?? '500MB',
items: const ['100MB', '500MB', '1GB', '2GB', '5GB'],
onChanged: (String? value) => libraryProvider.updateUserPreferences({
'cacheSize': value,
}),
),
);
},
);
}
Widget _buildClearCacheTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.clear_all,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Clear Cache',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'Free up storage space',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
trailing: IconButton(
onPressed: () => _showClearCacheDialog(context),
icon: Icon(
Icons.delete_sweep,
color: Theme.of(context).colorScheme.error,
),
),
);
},
);
}
Widget _buildVersionTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.info,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Version',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
libraryProvider.statistics['version'] ?? 'Unknown',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
);
},
);
}
Widget _buildBuildNumberTile(BuildContext context) {
return Consumer<EnhancedLibraryProvider>(
builder: (context, libraryProvider, child) {
return ListTile(
leading: Icon(
Icons.build,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Build Number',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
libraryProvider.statistics['buildNumber'] ?? 'Unknown',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
);
},
);
}
Widget _buildDeveloperTile(BuildContext context) {
return ListTile(
leading: Icon(
Icons.code,
color: Theme.of(context).colorScheme.onSurface,
),
title: Text(
'Developer',
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
'View developer options',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
trailing: IconButton(
onPressed: () => _showDeveloperOptions(context),
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
ThemeMode _getThemeMode(String? themeString) {
switch (themeString) {
case 'system':
return ThemeMode.system;
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
void _showServerUrlDialog(BuildContext context, AuthProvider authProvider) {
final controller = TextEditingController(text: authProvider.baseUrl ?? '');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Server URL'),
content: TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Server URL',
hintText: 'https://your-server.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
authProvider.updateBaseUrl(controller.text.trim());
Navigator.of(context).pop();
},
child: Text('Save'),
),
],
),
);
}
void _showLogoutDialog(BuildContext context, AuthProvider authProvider) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Logout'),
content: Text('Are you sure you want to logout?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
authProvider.logout();
Navigator.of(context).pop();
},
child: Text('Logout'),
),
],
),
);
}
void _testConnection(BuildContext context) {
// TODO: Implement connection test
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Connection test not implemented yet')),
);
}
void _showClearCacheDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Clear Cache'),
content: Text('Are you sure you want to clear all cached data?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cache cleared successfully')),
);
},
child: Text('Clear'),
),
],
),
);
}
void _showDeveloperOptions(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Developer Options'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('Debug Mode'),
trailing: Switch(
value: false,
onChanged: (value) {
// TODO: Implement debug mode
},
),
),
ListTile(
title: Text('Enable Logging'),
trailing: Switch(
value: false,
onChanged: (value) {
// TODO: Implement logging
},
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close'),
),
],
),
);
}
}

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