init: repo
15
ChiaraAndroid/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
3
ChiaraAndroid/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
1
ChiaraAndroid/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
Chiara
|
6
ChiaraAndroid/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
18
ChiaraAndroid/.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-06-10T03:24:59.011863144Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=3040223527000PT" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
19
ChiaraAndroid/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
41
ChiaraAndroid/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
ChiaraAndroid/.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
</component>
|
||||
</project>
|
10
ChiaraAndroid/.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
9
ChiaraAndroid/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
6
ChiaraAndroid/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
1
ChiaraAndroid/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
152
ChiaraAndroid/app/build.gradle.kts
Normal file
@@ -0,0 +1,152 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.kotlin.ksp)
|
||||
alias(libs.plugins.openapi.generator)
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
|
||||
val openapiOutputDir: String = layout.buildDirectory.dir("generated/api").get().asFile.path
|
||||
|
||||
android {
|
||||
namespace = "top.rrricardo.chiara"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "top.rrricardo.chiara"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets["main"].kotlin {
|
||||
srcDir("$openapiOutputDir/src/main/kotlin")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Default
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
// ViewModel
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
// ViewModel utilities for Compose
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
// LiveData
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
// Lifecycles only (without ViewModel or LiveData)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
// Lifecycle utilities for Compose
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
|
||||
// Saved state module for ViewModel
|
||||
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
||||
|
||||
// Annotation processor
|
||||
kapt(libs.androidx.lifecycle.compiler)
|
||||
|
||||
// Navigation Controller
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// Palette
|
||||
implementation(libs.androidx.palette)
|
||||
|
||||
// Media Exo Player
|
||||
implementation(libs.media3.exoplayer)
|
||||
implementation(libs.media3.session)
|
||||
implementation(libs.media3.ui)
|
||||
implementation(libs.media3.hls)
|
||||
|
||||
// Hilt
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
kapt(libs.hilt.android.compiler)
|
||||
|
||||
// Constraint Layout
|
||||
implementation(libs.constraintlayout.compose)
|
||||
|
||||
// Coil
|
||||
implementation(libs.coil)
|
||||
|
||||
// openapi
|
||||
implementation(libs.okhttp3)
|
||||
implementation(libs.retrofit.retrofit)
|
||||
implementation(libs.retrofit.converter.scalars)
|
||||
implementation(libs.retrofit.serialization.converter)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
openApiGenerate {
|
||||
generatorName.set("kotlin")
|
||||
inputSpec.set("$rootDir/app/src/main/openapi/chiara.json")
|
||||
outputDir.set(openapiOutputDir)
|
||||
apiPackage.set("top.rrricardo.chiara.openapi.api")
|
||||
modelPackage.set("top.rrricardo.chiara.openapi.model")
|
||||
packageName.set("top.rrricardo.chiara.openapi.client")
|
||||
generateApiTests.set(false)
|
||||
generateModelTests.set(false)
|
||||
configOptions.set(
|
||||
mapOf(
|
||||
"dataLibrary" to "java8"
|
||||
)
|
||||
)
|
||||
additionalProperties.set(
|
||||
mapOf(
|
||||
"library" to "jvm-retrofit2",
|
||||
"serializationLibrary" to "kotlinx_serialization",
|
||||
"useCoroutines" to "true",
|
||||
)
|
||||
)
|
||||
}
|
21
ChiaraAndroid/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@@ -0,0 +1,24 @@
|
||||
package top.rrricardo.chiara
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("top.rrricardo.chiara", appContext.packageName)
|
||||
}
|
||||
}
|
40
ChiaraAndroid/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_login_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Chiara"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Chiara">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".service.impl.MusicService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
BIN
ChiaraAndroid/app/src/main/ic_login-playstore.png
Normal file
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,44 @@
|
||||
package top.rrricardo.chiara
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import top.rrricardo.chiara.service.impl.MusicService
|
||||
import top.rrricardo.chiara.ui.ChiaraApp
|
||||
import top.rrricardo.chiara.ui.theme.ChiaraTheme
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
ChiaraTheme {
|
||||
ChiaraApp(mainViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
mainViewModel.destroyMediaController()
|
||||
|
||||
stopService(Intent(this, MusicService::class.java))
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package top.rrricardo.chiara
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application() {
|
||||
|
||||
}
|
||||
|
||||
object Configuration {
|
||||
const val SERVER_ADDRESS = "http://chiara.jackfiled.icu"
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package top.rrricardo.chiara.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import top.rrricardo.chiara.Configuration
|
||||
import top.rrricardo.chiara.openapi.client.infrastructure.ApiClient
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import top.rrricardo.chiara.service.impl.MusicControllerImpl
|
||||
import java.time.Duration
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApiClient() = ApiClient(
|
||||
baseUrl = Configuration.SERVER_ADDRESS,
|
||||
okHttpClientBuilder = OkHttpClient.Builder().readTimeout(Duration.ofSeconds(30))
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMusicController(@ApplicationContext context: Context): MusicController =
|
||||
MusicControllerImpl(context)
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package top.rrricardo.chiara.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ServiceComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ServiceScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ServiceComponent::class)
|
||||
object ServiceModule {
|
||||
@ServiceScoped
|
||||
@Provides
|
||||
fun provideAudioAttributes() = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
|
||||
@ServiceScoped
|
||||
@Provides
|
||||
fun provideExoPlayer(
|
||||
@ApplicationContext context: Context,
|
||||
autoAttributes: AudioAttributes
|
||||
) = ExoPlayer.Builder(context).build().apply {
|
||||
setAudioAttributes(autoAttributes, true)
|
||||
setHandleAudioBecomingNoisy(true)
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package top.rrricardo.chiara.extension
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
fun Long.toTime(): String {
|
||||
val stringBuffer = StringBuffer()
|
||||
|
||||
val minutes = (this / 60000).toInt()
|
||||
val seconds = (this / 1000 % 60).toInt()
|
||||
|
||||
stringBuffer
|
||||
.append(String.format(Locale.CHINA, "%02d", minutes))
|
||||
.append(":")
|
||||
.append(String.format(Locale.CHINA, "%02d", seconds))
|
||||
|
||||
return stringBuffer.toString()
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
import top.rrricardo.chiara.openapi.model.AlbumResponse
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
|
||||
data class AlbumPageState(
|
||||
val loading: Boolean = false,
|
||||
val selectedSong: SongResponse? = null,
|
||||
val album: AlbumResponse? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
@@ -0,0 +1,15 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
import top.rrricardo.chiara.openapi.model.AlbumResponse
|
||||
import top.rrricardo.chiara.openapi.model.ShowSeasonResponse
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
|
||||
data class HomePageState(
|
||||
val loading: Boolean = false,
|
||||
val isMusicPage: Boolean = true,
|
||||
val selectedAlbum : AlbumResponse? = null,
|
||||
val errorMessage: String? = null,
|
||||
val albums: List<AlbumResponse> = emptyList(),
|
||||
val selectedSeason : ShowSeasonResponse? = null,
|
||||
val seasons: List<ShowSeasonResponse> = emptyList(),
|
||||
)
|
@@ -0,0 +1,11 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
|
||||
data class MusicControllerState(
|
||||
val playerState: PlayerState= PlayerState.STOPPED,
|
||||
val currentSong: SongResponse? = null,
|
||||
val currentPosition: Long = 0L,
|
||||
val totalDuration: Long = 0L,
|
||||
val repeatState : RepeatState = RepeatState.Sequence
|
||||
)
|
@@ -0,0 +1,7 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
enum class PlayerState {
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
STOPPED
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
enum class RepeatState {
|
||||
Sequence,
|
||||
RepeatOne,
|
||||
Shuffle
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
import top.rrricardo.chiara.openapi.model.ShowSeasonResponse
|
||||
|
||||
data class ShowPageState(
|
||||
val loading: Boolean = false,
|
||||
val showSeason: ShowSeasonResponse? = null
|
||||
)
|
@@ -0,0 +1,11 @@
|
||||
package top.rrricardo.chiara.model
|
||||
|
||||
import top.rrricardo.chiara.openapi.model.EpisodeResponse
|
||||
import top.rrricardo.chiara.openapi.model.ShowSeasonResponse
|
||||
|
||||
data class VideoPageState(
|
||||
val loading: Boolean = false,
|
||||
val playing: Boolean = false,
|
||||
val showSeasonResponse: ShowSeasonResponse? = null,
|
||||
val episodeResponse: EpisodeResponse? = null
|
||||
)
|
@@ -0,0 +1,103 @@
|
||||
package top.rrricardo.chiara.service
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import top.rrricardo.chiara.model.PlayerState
|
||||
import top.rrricardo.chiara.model.RepeatState
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
|
||||
interface MusicController {
|
||||
var mediaControllerCallback: (
|
||||
(
|
||||
playerState: PlayerState,
|
||||
currentMusic: SongResponse?,
|
||||
currentPosition: Long,
|
||||
totalDuration: Long,
|
||||
repeatState: RepeatState
|
||||
) -> Unit
|
||||
)?
|
||||
|
||||
var mediaControllerReadyCallback: ((Player) -> Unit)?
|
||||
|
||||
var mediaControllerErrorCallback: ((PlaybackException) -> Unit)?
|
||||
|
||||
var player: Player?
|
||||
|
||||
/**
|
||||
* 获得当前播放器的播放列表
|
||||
*/
|
||||
fun getPlayList(): List<SongResponse>
|
||||
|
||||
/**
|
||||
* 添加一首曲目到播放列表中
|
||||
*/
|
||||
fun addSong(song: SongResponse)
|
||||
|
||||
/**
|
||||
* 添加一系列曲目到播放列表中
|
||||
*/
|
||||
fun addSongRange(songs: Iterable<SongResponse>)
|
||||
|
||||
/**
|
||||
* 清空播放列表
|
||||
*/
|
||||
fun clearPlayList()
|
||||
|
||||
/**
|
||||
* 删除播放列表中的指定曲目
|
||||
*/
|
||||
fun remove(songId: Int)
|
||||
|
||||
/**
|
||||
* 播放播放列表中的指定曲目
|
||||
*/
|
||||
fun play(songId: Int)
|
||||
|
||||
/**
|
||||
* 恢复播放
|
||||
*/
|
||||
fun resume()
|
||||
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
fun pause()
|
||||
|
||||
/**
|
||||
* 获得当前播放歌曲的位置
|
||||
*/
|
||||
fun getCurrentPosition(): Long
|
||||
|
||||
fun destroy()
|
||||
|
||||
/**
|
||||
* 跳转到下一首曲目
|
||||
*/
|
||||
fun skipToNextSong()
|
||||
|
||||
/**
|
||||
* 跳转到上一首曲目
|
||||
*/
|
||||
fun skipToPreviousSong()
|
||||
|
||||
/**
|
||||
* 获得当前正在播放的曲目
|
||||
*/
|
||||
fun getCurrentSong(): SongResponse?
|
||||
|
||||
/**
|
||||
* 跳转到指定位置
|
||||
*/
|
||||
fun seekTo(position: Long)
|
||||
|
||||
fun setSequenceRepeat()
|
||||
|
||||
fun setRepeatOne()
|
||||
|
||||
fun setShuffleRepeat()
|
||||
|
||||
fun playVideo(mediaItem: MediaItem)
|
||||
|
||||
fun stopVideo()
|
||||
}
|
@@ -0,0 +1,229 @@
|
||||
package top.rrricardo.chiara.service.impl
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import top.rrricardo.chiara.model.PlayerState
|
||||
import top.rrricardo.chiara.model.RepeatState
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
|
||||
class MusicControllerImpl(context: Context) : MusicController {
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>
|
||||
|
||||
private val mediaController: MediaController?
|
||||
get() = if (mediaControllerFuture.isDone) mediaControllerFuture.get() else null
|
||||
|
||||
private val playList = HashMap<Int, SongResponse>()
|
||||
private val mediaIdMap = HashMap<Int, Int>()
|
||||
private val songUrlMap = HashMap<String, SongResponse>()
|
||||
|
||||
init {
|
||||
val sessionToken =
|
||||
SessionToken(context, ComponentName(context, MusicService::class.java))
|
||||
|
||||
mediaControllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener({
|
||||
controllerListener()
|
||||
mediaController?.let {
|
||||
mediaControllerReadyCallback?.invoke(it)
|
||||
player = it
|
||||
}
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
override var mediaControllerCallback: (
|
||||
(
|
||||
playerState: PlayerState,
|
||||
currentMusic: SongResponse?,
|
||||
currentPosition: Long,
|
||||
totalDuration: Long,
|
||||
repeatState: RepeatState
|
||||
)
|
||||
-> Unit)? = null
|
||||
|
||||
override var mediaControllerReadyCallback: ((Player) -> Unit)? = null
|
||||
|
||||
override var mediaControllerErrorCallback: ((PlaybackException) -> Unit)? = null
|
||||
|
||||
override var player: Player? = mediaController
|
||||
|
||||
override fun clearPlayList() {
|
||||
mediaController?.stop()
|
||||
playList.clear()
|
||||
}
|
||||
|
||||
override fun getPlayList(): List<SongResponse> = playList.values.toList()
|
||||
|
||||
override fun addSong(song: SongResponse) {
|
||||
playList[song.id] = song
|
||||
|
||||
refreshMediaItems()
|
||||
}
|
||||
|
||||
override fun addSongRange(songs: Iterable<SongResponse>) {
|
||||
songs.map {
|
||||
playList[it.id] = it
|
||||
}
|
||||
|
||||
refreshMediaItems()
|
||||
}
|
||||
|
||||
override fun remove(songId: Int) {
|
||||
playList.remove(songId)
|
||||
}
|
||||
|
||||
override fun play(songId: Int) {
|
||||
mediaController?.apply {
|
||||
mediaIdMap[songId]?.let {
|
||||
seekToDefaultPosition(it)
|
||||
prepare()
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
mediaController?.play()
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
mediaController?.pause()
|
||||
}
|
||||
|
||||
override fun getCurrentSong(): SongResponse? {
|
||||
mediaController?.currentMediaItem?.let {
|
||||
return songUrlMap[it.mediaId]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getCurrentPosition(): Long = mediaController?.currentPosition ?: 0L
|
||||
|
||||
override fun seekTo(position: Long) {
|
||||
mediaController?.seekTo(position)
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
MediaController.releaseFuture(mediaControllerFuture)
|
||||
mediaControllerCallback = null
|
||||
}
|
||||
|
||||
override fun skipToNextSong() {
|
||||
mediaController?.seekToNext()
|
||||
}
|
||||
|
||||
override fun skipToPreviousSong() {
|
||||
mediaController?.seekToPrevious()
|
||||
}
|
||||
|
||||
override fun setSequenceRepeat() {
|
||||
mediaController?.shuffleModeEnabled = false
|
||||
mediaController?.repeatMode = Player.REPEAT_MODE_ALL
|
||||
}
|
||||
|
||||
override fun setRepeatOne() {
|
||||
mediaController?.shuffleModeEnabled = false
|
||||
mediaController?.repeatMode = Player.REPEAT_MODE_ONE
|
||||
}
|
||||
|
||||
override fun setShuffleRepeat() {
|
||||
mediaController?.repeatMode = Player.REPEAT_MODE_ALL
|
||||
mediaController?.shuffleModeEnabled = true
|
||||
}
|
||||
|
||||
override fun playVideo(mediaItem: MediaItem) {
|
||||
mediaController?.setMediaItem(mediaItem)
|
||||
mediaController?.apply {
|
||||
prepare()
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopVideo() {
|
||||
mediaController?.apply {
|
||||
stop()
|
||||
clearMediaItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun controllerListener() {
|
||||
mediaController?.addListener(object : Player.Listener {
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
super.onEvents(player, events)
|
||||
|
||||
with(player) {
|
||||
val repeatState = if (shuffleModeEnabled) {
|
||||
RepeatState.Shuffle
|
||||
} else {
|
||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
||||
RepeatState.RepeatOne
|
||||
} else {
|
||||
RepeatState.Sequence
|
||||
}
|
||||
}
|
||||
|
||||
mediaControllerCallback?.invoke(
|
||||
playbackState.toPlayerState(isPlaying),
|
||||
currentMediaItem?.let {
|
||||
songUrlMap[it.mediaId]
|
||||
},
|
||||
currentPosition.coerceAtLeast(0L),
|
||||
duration.coerceAtLeast(0L),
|
||||
repeatState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
super.onPlayerError(error)
|
||||
mediaControllerErrorCallback?.invoke(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun Int.toPlayerState(isPlaying: Boolean) =
|
||||
when (this) {
|
||||
Player.STATE_IDLE -> PlayerState.STOPPED
|
||||
Player.STATE_ENDED -> PlayerState.STOPPED
|
||||
else -> if (isPlaying) {
|
||||
PlayerState.PLAYING
|
||||
} else {
|
||||
PlayerState.PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshMediaItems() {
|
||||
mediaIdMap.clear()
|
||||
songUrlMap.clear()
|
||||
|
||||
val mediaItems = playList.values.mapIndexed { i, song ->
|
||||
mediaIdMap[song.id] = i
|
||||
songUrlMap[song.url] = song
|
||||
|
||||
MediaItem.Builder()
|
||||
.setUri(song.url)
|
||||
.setMediaId(song.url)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(song.title)
|
||||
.setSubtitle(song.arist)
|
||||
.setArtist(song.arist)
|
||||
.setArtworkUri(Uri.parse(song.coverImageUrl))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
mediaController?.setMediaItems(mediaItems)
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package top.rrricardo.chiara.service.impl
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MusicService : MediaSessionService() {
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
@Inject
|
||||
lateinit var exoPlayer: ExoPlayer
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
mediaSession = MediaSession.Builder(this, exoPlayer)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
return mediaSession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
package top.rrricardo.chiara.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import top.rrricardo.chiara.ui.page.AlbumPage
|
||||
import top.rrricardo.chiara.ui.page.HomePage
|
||||
import top.rrricardo.chiara.ui.page.PlayListPage
|
||||
import top.rrricardo.chiara.ui.page.ShowPage
|
||||
import top.rrricardo.chiara.ui.page.SongPage
|
||||
import top.rrricardo.chiara.ui.page.SplashPage
|
||||
import top.rrricardo.chiara.ui.page.VideoPage
|
||||
import top.rrricardo.chiara.ui.viewmodel.AlbumPageViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.HomePageViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.ShowPageViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.VideoPageViewModel
|
||||
|
||||
@Composable
|
||||
fun ChiaraApp(mainViewModel: MainViewModel) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = Navigation.SPLASH_SCREEN) {
|
||||
composable(route = Navigation.SPLASH_SCREEN) {
|
||||
SplashPage(mainViewModel = mainViewModel, navController = navController)
|
||||
}
|
||||
|
||||
composable(route = Navigation.HOME) {
|
||||
val homePageViewModel: HomePageViewModel = hiltViewModel()
|
||||
val isInitialized = rememberSaveable(stateSaver = autoSaver()) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
homePageViewModel.fetchRemoteData()
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
HomePage(
|
||||
mainViewModel = mainViewModel,
|
||||
homePageViewModel = homePageViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
"${Navigation.ALBUM_SCREEN}/{albumId}",
|
||||
arguments = listOf(navArgument("albumId") {
|
||||
type = NavType.IntType
|
||||
})
|
||||
) { backStackEntry ->
|
||||
val albumId = backStackEntry.arguments?.getInt("albumId")
|
||||
val albumPageViewModel: AlbumPageViewModel = hiltViewModel()
|
||||
val isInitialized = rememberSaveable(stateSaver = autoSaver()) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
albumId?.let {
|
||||
albumPageViewModel.fetchAlbum(it)
|
||||
}
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
AlbumPage(
|
||||
mainViewModel = mainViewModel,
|
||||
albumPageViewModel = albumPageViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = Navigation.SONG_SCREEN) {
|
||||
SongPage(mainViewModel = mainViewModel, navController = navController)
|
||||
}
|
||||
|
||||
composable(route = Navigation.PLAYLIST_SCREEN) {
|
||||
PlayListPage(mainViewModel = mainViewModel, navController = navController)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "${Navigation.SHOW_SCREEN}/{showId}", arguments = listOf(
|
||||
navArgument("showId") {
|
||||
type = NavType.IntType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val seasonId = backStackEntry.arguments?.getInt("showId")
|
||||
val showPageViewModel: ShowPageViewModel = hiltViewModel()
|
||||
val isInitialized = rememberSaveable(stateSaver = autoSaver()) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
seasonId?.let {
|
||||
showPageViewModel.fetchShowSeason(it)
|
||||
}
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
ShowPage(showPageViewModel = showPageViewModel, navController = navController)
|
||||
}
|
||||
|
||||
composable(route = "${Navigation.VIDEO_SCREEN}?videoId={videoId}&showId={showId}",
|
||||
arguments = listOf(
|
||||
navArgument("videoId") {
|
||||
type = NavType.IntType
|
||||
},
|
||||
navArgument("showId") {
|
||||
type = NavType.IntType
|
||||
}
|
||||
)) { backStackEntry ->
|
||||
val videoId = backStackEntry.arguments?.getInt("videoId")
|
||||
val showId = backStackEntry.arguments?.getInt("showId")
|
||||
val videoPageViewModel: VideoPageViewModel = hiltViewModel()
|
||||
val isInitialized = rememberSaveable(stateSaver = autoSaver()) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
videoId?.let {videoId ->
|
||||
videoPageViewModel.fetchAndPlay(videoId)
|
||||
|
||||
showId?.let {
|
||||
videoPageViewModel.fetchShow(it, videoId)
|
||||
}
|
||||
}
|
||||
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
VideoPage(videoPageViewModel = videoPageViewModel, navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Navigation {
|
||||
const val SPLASH_SCREEN = "splashScreen"
|
||||
const val HOME = "home"
|
||||
const val ALBUM_SCREEN = "albumScreen"
|
||||
const val SONG_SCREEN = "songScreen"
|
||||
const val PLAYLIST_SCREEN = "playlistScreen"
|
||||
const val SHOW_SCREEN = "showScreen"
|
||||
const val VIDEO_SCREEN = "videoScreen"
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
package top.rrricardo.chiara.ui.component
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathOperation
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import top.rrricardo.chiara.R
|
||||
|
||||
val roundedShape = object : Shape {
|
||||
override fun createOutline(
|
||||
size: Size,
|
||||
layoutDirection: LayoutDirection,
|
||||
density: Density
|
||||
): Outline {
|
||||
val p1 = Path().apply {
|
||||
addOval(Rect(4f, 3f, size.width - 1, size.height - 1))
|
||||
}
|
||||
val thickness = size.height / 2.10f
|
||||
val p2 = Path().apply {
|
||||
addOval(
|
||||
Rect(
|
||||
thickness,
|
||||
thickness,
|
||||
size.width - thickness,
|
||||
size.height - thickness
|
||||
)
|
||||
)
|
||||
}
|
||||
val p3 = Path()
|
||||
p3.op(p1, p2, PathOperation.Difference)
|
||||
|
||||
return Outline.Generic(p3)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimatedVinyl(
|
||||
modifier: Modifier = Modifier,
|
||||
isSongPlaying: Boolean = false,
|
||||
painter: Painter
|
||||
) {
|
||||
var currentRotation by remember {
|
||||
mutableFloatStateOf(0f)
|
||||
}
|
||||
|
||||
val rotation = remember {
|
||||
Animatable(currentRotation)
|
||||
}
|
||||
|
||||
LaunchedEffect(isSongPlaying) {
|
||||
if (isSongPlaying) {
|
||||
rotation.animateTo(
|
||||
targetValue = currentRotation + 360f, animationSpec = infiniteRepeatable(
|
||||
animation = tween(3000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
) {
|
||||
currentRotation = value
|
||||
}
|
||||
} else {
|
||||
if (currentRotation > 0f) {
|
||||
rotation.animateTo(
|
||||
targetValue = currentRotation + 50f, animationSpec = tween(
|
||||
1250, easing = LinearEasing
|
||||
)
|
||||
) {
|
||||
currentRotation = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vinyl(modifier = modifier, painter = painter, rotationDegrees = rotation.value)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Vinyl(
|
||||
modifier: Modifier = Modifier,
|
||||
rotationDegrees: Float = 0f,
|
||||
painter: Painter
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(roundedShape)
|
||||
) {
|
||||
// Vinyl background
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.rotate(rotationDegrees),
|
||||
painter = painterResource(id = R.drawable.vinyl_background),
|
||||
contentDescription = "Vinyl background"
|
||||
)
|
||||
|
||||
// Vinyl song cover
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(0.5f)
|
||||
.rotate(rotationDegrees)
|
||||
.aspectRatio(1f)
|
||||
.align(Alignment.Center)
|
||||
.clip(roundedShape),
|
||||
painter = painter,
|
||||
contentDescription = "Song Cover"
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
package top.rrricardo.chiara.ui.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.rrricardo.chiara.ui.theme.ChiaraTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChiaraAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
showNavigationIcon: Boolean = false,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
actions: @Composable (RowScope.() -> Unit) = {}
|
||||
) {
|
||||
val appBarColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(appBarColor)
|
||||
.windowInsetsTopHeight(WindowInsets.statusBars)
|
||||
)
|
||||
TopAppBar(title = {
|
||||
Row {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}, navigationIcon = {
|
||||
CompositionLocalProvider(LocalContentColor provides LocalContentColor.current.copy(alpha = 0.4f)) {
|
||||
if (showNavigationIcon) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
onNavigateUp()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}, actions = actions, modifier = modifier.background(appBarColor)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ChiaraAppBarPreview() {
|
||||
ChiaraTheme {
|
||||
ChiaraAppBar(
|
||||
title = "Genshin Impact - Jade Moon Upon a Sea of Clouds",
|
||||
showNavigationIcon = true
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,268 @@
|
||||
package top.rrricardo.chiara.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.List
|
||||
import androidx.compose.material.icons.rounded.List
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import top.rrricardo.chiara.R
|
||||
import top.rrricardo.chiara.model.PlayerState
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import top.rrricardo.chiara.ui.Navigation
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun HomeBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
mainViewModel: MainViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
var offsetX by remember {
|
||||
mutableFloatStateOf(0f)
|
||||
}
|
||||
val musicControllerState = mainViewModel.musicControllerState
|
||||
val musicController = mainViewModel.musicController
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
modifier = modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragEnd = {
|
||||
when {
|
||||
offsetX > 0 -> {
|
||||
musicController.skipToPreviousSong()
|
||||
}
|
||||
|
||||
offsetX < 0 -> {
|
||||
musicController.skipToNextSong()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX = dragAmount.x
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(
|
||||
if (!isSystemInDarkTheme()) {
|
||||
Color.LightGray
|
||||
} else {
|
||||
Color.DarkGray
|
||||
}
|
||||
),
|
||||
) {
|
||||
if (musicControllerState.currentSong != null) {
|
||||
HomeBottomBarItem(
|
||||
song = musicControllerState.currentSong,
|
||||
musicController = musicController,
|
||||
playerState = musicControllerState.playerState,
|
||||
navController = navController
|
||||
)
|
||||
} else {
|
||||
NonPlayingHomeBar(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeBottomBarItem(
|
||||
song: SongResponse,
|
||||
musicController: MusicController,
|
||||
playerState: PlayerState,
|
||||
navController: NavController
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(64.dp)
|
||||
.clickable(onClick = {
|
||||
navController.navigate(Navigation.SONG_SCREEN)
|
||||
})
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(song.coverImageUrl)
|
||||
.crossfade(true).build()
|
||||
),
|
||||
contentDescription = song.title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.offset(16.dp)
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 8.dp, horizontal = 32.dp),
|
||||
) {
|
||||
Text(
|
||||
song.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
song.arist,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
alpha = 0.60f
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
val painter = rememberAsyncImagePainter(
|
||||
if (playerState == PlayerState.PLAYING) {
|
||||
R.drawable.ic_round_pause
|
||||
} else {
|
||||
R.drawable.ic_round_play_arrow
|
||||
}
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = "Music",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(48.dp)
|
||||
.clickable(
|
||||
interactionSource = remember {
|
||||
MutableInteractionSource()
|
||||
},
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 24.dp
|
||||
)
|
||||
) {
|
||||
if (playerState == PlayerState.PLAYING) {
|
||||
musicController.pause()
|
||||
} else {
|
||||
musicController.resume()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.List,
|
||||
contentDescription = "播放列表",
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(36.dp)
|
||||
.clickable {
|
||||
navController.navigate(Navigation.PLAYLIST_SCREEN)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NonPlayingHomeBar(navController: NavController) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(64.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_image),
|
||||
contentDescription = "Music",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(48.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 8.dp, horizontal = 32.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "未在播放",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.List,
|
||||
contentDescription = "播放列表",
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(36.dp)
|
||||
.clickable {
|
||||
navController.navigate(Navigation.PLAYLIST_SCREEN)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,154 @@
|
||||
package top.rrricardo.chiara.ui.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import top.rrricardo.chiara.Configuration
|
||||
import top.rrricardo.chiara.R
|
||||
import top.rrricardo.chiara.ui.theme.ChiaraTheme
|
||||
|
||||
@Composable
|
||||
fun MusicItem(
|
||||
title: String,
|
||||
artist: String,
|
||||
coverImageUrl: String,
|
||||
playing: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val backgroundColor = if (playing) {
|
||||
MaterialTheme.colorScheme.surface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.background
|
||||
}
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val (
|
||||
dividerRef, titleRef, artistRef, imageRef
|
||||
) = createRefs()
|
||||
|
||||
HorizontalDivider(Modifier.constrainAs(dividerRef) {
|
||||
top.linkTo(parent.top)
|
||||
centerHorizontallyTo(parent)
|
||||
width = Dimension.fillToConstraints
|
||||
})
|
||||
|
||||
val coverImage = if (coverImageUrl == Configuration.SERVER_ADDRESS) {
|
||||
painterResource(id = R.drawable.ic_round_album)
|
||||
} else {
|
||||
rememberAsyncImagePainter(ImageRequest
|
||||
.Builder(LocalContext.current).data(coverImageUrl)
|
||||
.crossfade(true).build())
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = coverImage,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.constrainAs(imageRef) {
|
||||
end.linkTo(parent.end, 16.dp)
|
||||
top.linkTo(parent.top, 16.dp)
|
||||
bottom.linkTo(parent.bottom, 16.dp)
|
||||
}
|
||||
)
|
||||
|
||||
val textColor = if (playing) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
}
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = textColor,
|
||||
modifier = Modifier.constrainAs(titleRef) {
|
||||
linkTo(
|
||||
start = parent.start,
|
||||
end = imageRef.start,
|
||||
startMargin = 24.dp,
|
||||
endMargin = 16.dp,
|
||||
bias = 0f
|
||||
)
|
||||
top.linkTo(parent.top, 16.dp)
|
||||
start.linkTo(parent.start, 16.dp)
|
||||
width = Dimension.preferredWrapContent
|
||||
}
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides LocalContentColor.current.copy(alpha = 0.4f)) {
|
||||
Text(
|
||||
text = artist,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.constrainAs(artistRef) {
|
||||
linkTo(
|
||||
start = parent.start,
|
||||
end = imageRef.start,
|
||||
startMargin = 24.dp,
|
||||
endMargin = 16.dp,
|
||||
bias = 0f
|
||||
)
|
||||
top.linkTo(titleRef.bottom, 6.dp)
|
||||
start.linkTo(parent.start, 16.dp)
|
||||
width = Dimension.preferredWrapContent
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MusicItemPreview() {
|
||||
ChiaraTheme {
|
||||
Column {
|
||||
MusicItem(
|
||||
title = "Genshin Impact - Jade Moon Upon a Sea of Clouds",
|
||||
artist = "Yu-Peng Chen, HOYO-MiX",
|
||||
coverImageUrl = "http://10.29.155.159:5078/api/file/b1c6a383-f860-4391-82ff-9507851603cc"
|
||||
) {
|
||||
|
||||
}
|
||||
MusicItem(
|
||||
title = "Genshin Impact - Jade Moon Upon a Sea of Clouds",
|
||||
artist = "Yu-Peng Chen, HOYO-MiX",
|
||||
coverImageUrl = "http://10.29.155.159:5078/api/file/b1c6a383-f860-4391-82ff-9507851603cc",
|
||||
playing = true
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
package top.rrricardo.chiara.ui.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.rrricardo.chiara.openapi.model.EpisodeResponse
|
||||
import top.rrricardo.chiara.openapi.model.ShowSeasonResponse
|
||||
|
||||
@Composable
|
||||
fun SeasonItem(season: ShowSeasonResponse, onClick: () -> Unit) {
|
||||
Column(modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)) {
|
||||
HorizontalDivider()
|
||||
|
||||
Text(
|
||||
text = season.title,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeItem(episode: EpisodeResponse, onClick: () -> Unit) {
|
||||
Column(modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)) {
|
||||
HorizontalDivider()
|
||||
|
||||
Text(
|
||||
text = episode.title,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides LocalContentColor.current.copy(alpha = 0.4f)) {
|
||||
Text(
|
||||
text = episode.episodeNumber,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SeasonItemPreview() {
|
||||
Column {
|
||||
SeasonItem(
|
||||
season = ShowSeasonResponse(
|
||||
id = 1,
|
||||
title = "Tonikaku Kawaii S2",
|
||||
episodes = emptyList()
|
||||
)
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
EpisodeItem(
|
||||
episode = EpisodeResponse(
|
||||
id = 1,
|
||||
title = "Tonikaku Kawaii S2 E1",
|
||||
episodeNumber = "1",
|
||||
showSeasonId = 1
|
||||
)
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import top.rrricardo.chiara.ui.component.ChiaraAppBar
|
||||
import top.rrricardo.chiara.ui.component.HomeBottomBar
|
||||
import top.rrricardo.chiara.ui.component.MusicItem
|
||||
import top.rrricardo.chiara.ui.viewmodel.AlbumPageViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun AlbumPage(
|
||||
mainViewModel: MainViewModel,
|
||||
albumPageViewModel: AlbumPageViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
ChiaraAppBar(
|
||||
title = albumPageViewModel.albumPageState.album?.title ?: "音乐播放器",
|
||||
showNavigationIcon = true,
|
||||
onNavigateUp = {
|
||||
navController.navigateUp()
|
||||
}
|
||||
)
|
||||
|
||||
with(albumPageViewModel.albumPageState) {
|
||||
when {
|
||||
loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!loading -> {
|
||||
album?.songs?.let { songs ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(songs) {
|
||||
MusicItem(
|
||||
title = it.title,
|
||||
artist = it.arist,
|
||||
coverImageUrl = it.coverImageUrl,
|
||||
playing = albumPageViewModel.albumPageState.selectedSong?.id == it.id
|
||||
) {
|
||||
albumPageViewModel.selectSong(it)
|
||||
albumPageViewModel.playSong()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
HomeBottomBar(mainViewModel = mainViewModel, navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,225 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import top.rrricardo.chiara.R
|
||||
import top.rrricardo.chiara.ui.Navigation
|
||||
import top.rrricardo.chiara.ui.component.ChiaraAppBar
|
||||
import top.rrricardo.chiara.ui.component.HomeBottomBar
|
||||
import top.rrricardo.chiara.ui.component.MusicItem
|
||||
import top.rrricardo.chiara.ui.component.SeasonItem
|
||||
import top.rrricardo.chiara.ui.viewmodel.HomePageViewModel
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun HomePage(
|
||||
mainViewModel: MainViewModel,
|
||||
homePageViewModel: HomePageViewModel,
|
||||
navController: NavHostController
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
ChiaraAppBar(title = "Chiara播放器", actions = {
|
||||
Icon(imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = "Refresh",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
homePageViewModel.fetchRemoteData()
|
||||
}
|
||||
)
|
||||
})
|
||||
}, bottomBar = {
|
||||
BottomAppBar(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
homePageViewModel.changeToMusicPage()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_round_library_music),
|
||||
contentDescription = "Music library",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
homePageViewModel.changeToVideoPage()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_round_video_library),
|
||||
contentDescription = "Video library",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
with(homePageViewModel.homePageState) {
|
||||
when {
|
||||
loading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
top = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!loading && isMusicPage -> {
|
||||
MusicList(
|
||||
mainViewModel = mainViewModel,
|
||||
homePageViewModel = homePageViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
!loading && !isMusicPage -> {
|
||||
VideoList(
|
||||
homePageViewModel = homePageViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MusicList(
|
||||
mainViewModel: MainViewModel,
|
||||
homePageViewModel: HomePageViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(homePageViewModel.homePageState.albums) {
|
||||
MusicItem(
|
||||
title = it.title,
|
||||
artist = it.arist,
|
||||
coverImageUrl = it.coverImageUrl
|
||||
) {
|
||||
homePageViewModel.selectAlbum(it)
|
||||
navController.navigate(
|
||||
"${Navigation.ALBUM_SCREEN}/${it.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HomeBottomBar(mainViewModel = mainViewModel, navController = navController)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoList(homePageViewModel: HomePageViewModel, navController: NavController) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(homePageViewModel.homePageState.seasons) {
|
||||
SeasonItem(season = it) {
|
||||
navController.navigate("${Navigation.SHOW_SCREEN}/${it.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun NavigationBarPreview() {
|
||||
Scaffold(bottomBar = {
|
||||
BottomAppBar(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_round_library_music),
|
||||
contentDescription = "Music library",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_round_video_library),
|
||||
contentDescription = "Video library",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Box(modifier = Modifier.padding(it))
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import top.rrricardo.chiara.ui.component.ChiaraAppBar
|
||||
import top.rrricardo.chiara.ui.component.HomeBottomBar
|
||||
import top.rrricardo.chiara.ui.component.MusicItem
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun PlayListPage(
|
||||
modifier: Modifier = Modifier,
|
||||
mainViewModel: MainViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
Scaffold(modifier = modifier, topBar = {
|
||||
ChiaraAppBar(
|
||||
title = "播放列表",
|
||||
showNavigationIcon = true,
|
||||
onNavigateUp = {
|
||||
navController.navigateUp()
|
||||
}
|
||||
)
|
||||
}, floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
mainViewModel.musicController.clearPlayList()
|
||||
mainViewModel.refreshPlayList()
|
||||
}) {
|
||||
Icon(imageVector = Icons.Rounded.Delete, contentDescription = "Delete")
|
||||
}
|
||||
}, bottomBar = {
|
||||
HomeBottomBar(mainViewModel = mainViewModel, navController = navController)
|
||||
}) { padding ->
|
||||
Surface(
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
mainViewModel.refreshPlayList()
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
with(mainViewModel) {
|
||||
if (playList.isEmpty()) {
|
||||
NoSongHint()
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.onBackground)
|
||||
) {
|
||||
items(playList) {
|
||||
MusicItem(
|
||||
title = it.title,
|
||||
artist = it.arist,
|
||||
coverImageUrl = it.coverImageUrl,
|
||||
playing = it.id == mainViewModel.musicControllerState.currentSong?.id
|
||||
) {
|
||||
mainViewModel.musicController.play(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoSongHint() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "还没有任何歌曲呢",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun NoSongHitPreview() {
|
||||
NoSongHint()
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import top.rrricardo.chiara.ui.Navigation
|
||||
import top.rrricardo.chiara.ui.component.ChiaraAppBar
|
||||
import top.rrricardo.chiara.ui.component.EpisodeItem
|
||||
import top.rrricardo.chiara.ui.viewmodel.ShowPageViewModel
|
||||
|
||||
@Composable
|
||||
fun ShowPage(
|
||||
showPageViewModel: ShowPageViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
ChiaraAppBar(title = showPageViewModel.showPageState.showSeason?.title ?: "Video")
|
||||
|
||||
with(showPageViewModel.showPageState) {
|
||||
when {
|
||||
loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
!loading -> {
|
||||
showSeason?.episodes?.let {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(it) {
|
||||
EpisodeItem(episode = it) {
|
||||
navController.navigate(
|
||||
"${Navigation.VIDEO_SCREEN}?videoId=${it.id}&showId=${it.showSeasonId}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,338 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.rounded.List
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import top.rrricardo.chiara.R
|
||||
import top.rrricardo.chiara.extension.toTime
|
||||
import top.rrricardo.chiara.model.PlayerState
|
||||
import top.rrricardo.chiara.model.RepeatState
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
import top.rrricardo.chiara.ui.Navigation
|
||||
import top.rrricardo.chiara.ui.component.AnimatedVinyl
|
||||
import top.rrricardo.chiara.ui.theme.ChiaraTheme
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun SongPage(
|
||||
mainViewModel: MainViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
mainViewModel.musicControllerState.currentSong?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
SongScreenContent(
|
||||
song = it,
|
||||
mainViewModel = mainViewModel,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SongScreenContent(
|
||||
song: SongResponse,
|
||||
mainViewModel: MainViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
val musicControllerState = mainViewModel.musicControllerState
|
||||
|
||||
val dominantColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val gradientColors = if (isSystemInDarkTheme()) {
|
||||
listOf(
|
||||
dominantColor, MaterialTheme.colorScheme.background
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.background
|
||||
)
|
||||
}
|
||||
|
||||
val sliderColors = if (isSystemInDarkTheme()) {
|
||||
SliderDefaults.colors(
|
||||
thumbColor = MaterialTheme.colorScheme.onBackground,
|
||||
activeTrackColor = MaterialTheme.colorScheme.onBackground,
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.1f
|
||||
),
|
||||
)
|
||||
} else SliderDefaults.colors(
|
||||
thumbColor = dominantColor,
|
||||
activeTrackColor = dominantColor,
|
||||
inactiveTrackColor = dominantColor.copy(
|
||||
alpha = 0.1f
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Surface {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = gradientColors,
|
||||
endY = LocalConfiguration.current.screenHeightDp.toFloat() * LocalDensity.current.density
|
||||
)
|
||||
)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
) {
|
||||
Column {
|
||||
IconButton(
|
||||
onClick = {
|
||||
navController.navigateUp()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
|
||||
contentDescription = "Close",
|
||||
colorFilter = ColorFilter.tint(LocalContentColor.current)
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 32.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.weight(1f, fill = false)
|
||||
.aspectRatio(1f)
|
||||
|
||||
) {
|
||||
val imagePainter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(song.coverImageUrl)
|
||||
.crossfade(true).build()
|
||||
)
|
||||
|
||||
AnimatedVinyl(
|
||||
painter = imagePainter,
|
||||
isSongPlaying = musicControllerState.playerState == PlayerState.PLAYING
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = song.title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(song.arist,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.graphicsLayer {
|
||||
alpha = 0.60f
|
||||
})
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp)
|
||||
) {
|
||||
|
||||
Slider(
|
||||
value = musicControllerState.currentPosition.toFloat(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
valueRange = 0f..musicControllerState.totalDuration.toFloat(),
|
||||
colors = sliderColors,
|
||||
onValueChange = {
|
||||
mainViewModel.musicController.seekTo(it.toLong())
|
||||
},
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides LocalContentColor.current.copy(
|
||||
alpha = 0.4f
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
musicControllerState.currentPosition.toTime(),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
musicControllerState.totalDuration.toTime(),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowLeft,
|
||||
contentDescription = "Skip Previous",
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = {
|
||||
mainViewModel.musicController.skipToPreviousSong()
|
||||
})
|
||||
.padding(12.dp)
|
||||
.size(32.dp)
|
||||
)
|
||||
val playPauseIcon =
|
||||
if (musicControllerState.playerState == PlayerState.PLAYING) {
|
||||
R.drawable.ic_round_pause
|
||||
} else {
|
||||
R.drawable.ic_round_play_arrow
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(playPauseIcon),
|
||||
contentDescription = "Play",
|
||||
tint = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onBackground)
|
||||
.clickable(onClick = {
|
||||
if (musicControllerState.playerState == PlayerState.PLAYING) {
|
||||
mainViewModel.musicController.pause()
|
||||
} else {
|
||||
mainViewModel.musicController.resume()
|
||||
}
|
||||
})
|
||||
.size(64.dp)
|
||||
.padding(8.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
contentDescription = "Skip Next",
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = {
|
||||
mainViewModel.musicController.skipToNextSong()
|
||||
})
|
||||
.padding(12.dp)
|
||||
.size(32.dp)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 20.dp)
|
||||
) {
|
||||
val repeatIcon = when (musicControllerState.repeatState) {
|
||||
RepeatState.Sequence -> R.drawable.ic_round_repeat
|
||||
RepeatState.RepeatOne -> R.drawable.ic_round_repeat_one
|
||||
RepeatState.Shuffle -> R.drawable.ic_round_shuffle
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = repeatIcon),
|
||||
contentDescription = "RepeatState",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable(onClick = {
|
||||
when (musicControllerState.repeatState) {
|
||||
RepeatState.Sequence -> {
|
||||
mainViewModel.musicController.setRepeatOne()
|
||||
}
|
||||
|
||||
RepeatState.RepeatOne -> {
|
||||
mainViewModel.musicController.setShuffleRepeat()
|
||||
}
|
||||
|
||||
RepeatState.Shuffle -> {
|
||||
mainViewModel.musicController.setSequenceRepeat()
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding(12.dp)
|
||||
.size(32.dp)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.List,
|
||||
contentDescription = "PlayList",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.size(32.dp)
|
||||
.clickable {
|
||||
navController.navigate(Navigation.PLAYLIST_SCREEN)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ColorPreview() {
|
||||
ChiaraTheme {
|
||||
Text(text = "测试测试测试测试测试测试测试", color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import top.rrricardo.chiara.R
|
||||
import top.rrricardo.chiara.ui.Navigation
|
||||
import top.rrricardo.chiara.ui.viewmodel.MainViewModel
|
||||
|
||||
@Composable
|
||||
fun SplashPage(mainViewModel: MainViewModel, navController: NavController) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
mainViewModel.musicController.mediaControllerReadyCallback = {
|
||||
navController.navigate(Navigation.HOME) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplashScreenContent()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SplashScreenContent() {
|
||||
val scale = remember {
|
||||
Animatable(0f)
|
||||
}
|
||||
|
||||
// AnimationEffect
|
||||
LaunchedEffect(key1 = true) {
|
||||
scale.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 1000,
|
||||
easing = {
|
||||
OvershootInterpolator(4f).getInterpolation(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Image
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_login_foreground),
|
||||
contentDescription = "Logo",
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
.scale(scale.value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SplashScreenPreview() {
|
||||
SplashScreenContent()
|
||||
}
|
@@ -0,0 +1,171 @@
|
||||
package top.rrricardo.chiara.ui.page
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.navigation.NavController
|
||||
import top.rrricardo.chiara.openapi.model.EpisodeResponse
|
||||
import top.rrricardo.chiara.openapi.model.ShowSeasonResponse
|
||||
import top.rrricardo.chiara.ui.component.ChiaraAppBar
|
||||
import top.rrricardo.chiara.ui.component.EpisodeItem
|
||||
import top.rrricardo.chiara.ui.viewmodel.VideoPageViewModel
|
||||
|
||||
@Composable
|
||||
fun VideoPage(videoPageViewModel: VideoPageViewModel, navController: NavController) {
|
||||
Scaffold(topBar = {
|
||||
with(videoPageViewModel.videoPageState) {
|
||||
ChiaraAppBar(title = episodeResponse?.title ?: "视频",
|
||||
showNavigationIcon = true, onNavigateUp = {
|
||||
videoPageViewModel.stopTransaction()
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}, snackbarHost = {
|
||||
SnackbarHost(hostState = videoPageViewModel.snackBarHostState)
|
||||
}) { padding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
BackHandler(enabled = true) {
|
||||
videoPageViewModel.stopTransaction()
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
with(videoPageViewModel.videoPageState) {
|
||||
when {
|
||||
loading -> Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
top = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
!loading -> {
|
||||
PlayerSurface(
|
||||
modifier = Modifier
|
||||
.height(300.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
it.player = videoPageViewModel.getPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showSeasonResponse?.let { showSeasonResponse ->
|
||||
episodeResponse?.let {
|
||||
NextEpisodeList(
|
||||
showSeasonResponse = showSeasonResponse,
|
||||
episodeResponse = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayerSurface(modifier: Modifier = Modifier, onPlayerViewReady: (PlayerView) -> Unit) {
|
||||
AndroidView(factory = {
|
||||
PlayerView(it).apply {
|
||||
useController = true
|
||||
onPlayerViewReady(this)
|
||||
}
|
||||
}, modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NextEpisodeList(
|
||||
modifier: Modifier = Modifier,
|
||||
showSeasonResponse: ShowSeasonResponse,
|
||||
episodeResponse: EpisodeResponse
|
||||
) {
|
||||
Column(modifier.fillMaxSize()) {
|
||||
Text(text = "接下来:")
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
items(showSeasonResponse.episodes.filter {
|
||||
it.id != episodeResponse.id
|
||||
}) {
|
||||
EpisodeItem(episode = it) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun NextEpisodeListPreview() {
|
||||
NextEpisodeList(
|
||||
showSeasonResponse = ShowSeasonResponse(
|
||||
id = 0,
|
||||
title = "TestTestTest",
|
||||
episodes = listOf(
|
||||
EpisodeResponse(
|
||||
id = 1,
|
||||
title = "E01",
|
||||
episodeNumber = "1",
|
||||
showSeasonId = 0
|
||||
),
|
||||
EpisodeResponse(
|
||||
id = 2,
|
||||
title = "E02",
|
||||
episodeNumber = "2",
|
||||
showSeasonId = 0
|
||||
)
|
||||
)
|
||||
), episodeResponse = EpisodeResponse(
|
||||
id = 1,
|
||||
title = "E01",
|
||||
episodeNumber = "1",
|
||||
showSeasonId = 0
|
||||
)
|
||||
)
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package top.rrricardo.chiara.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
@@ -0,0 +1,59 @@
|
||||
package top.rrricardo.chiara.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFFFFCCBC),
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40,
|
||||
surface = Color(0xFFB3E5FC),
|
||||
background = Color(0xFFE3F2FD),
|
||||
|
||||
/* Other default colors to override
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChiaraTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package top.rrricardo.chiara.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
@@ -0,0 +1,77 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import top.rrricardo.chiara.Configuration
|
||||
import top.rrricardo.chiara.model.AlbumPageState
|
||||
import top.rrricardo.chiara.openapi.api.AlbumApi
|
||||
import top.rrricardo.chiara.openapi.client.infrastructure.ApiClient
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AlbumPageViewModel @Inject constructor(
|
||||
apiClient: ApiClient,
|
||||
private val musicController: MusicController
|
||||
) : ViewModel() {
|
||||
var albumPageState by mutableStateOf(AlbumPageState())
|
||||
private set
|
||||
|
||||
private val albumApi = apiClient.createService(AlbumApi::class.java)
|
||||
|
||||
fun fetchAlbum(albumId: Int) {
|
||||
albumPageState = albumPageState.copy(
|
||||
loading = true
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
val albumResponse = albumApi.apiAlbumIdGet(albumId)
|
||||
var album = albumResponse.body()
|
||||
|
||||
if (albumResponse.isSuccessful && album != null) {
|
||||
album = album.copy(
|
||||
coverImageUrl = Configuration.SERVER_ADDRESS + album.coverImageUrl,
|
||||
songs = album.songs.map {
|
||||
it.copy(
|
||||
coverImageUrl = Configuration.SERVER_ADDRESS + it.coverImageUrl,
|
||||
url = Configuration.SERVER_ADDRESS + it.url
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
albumPageState = albumPageState.copy(
|
||||
loading = false,
|
||||
album = album
|
||||
)
|
||||
} else {
|
||||
albumPageState = albumPageState.copy(
|
||||
loading = false,
|
||||
errorMessage = "网络错误"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSong(song: SongResponse) {
|
||||
albumPageState = albumPageState.copy(
|
||||
selectedSong = song
|
||||
)
|
||||
}
|
||||
|
||||
fun playSong() {
|
||||
albumPageState.album?.let {
|
||||
musicController.addSongRange(it.songs)
|
||||
}
|
||||
|
||||
albumPageState.selectedSong?.let {
|
||||
musicController.play(it.id)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import top.rrricardo.chiara.Configuration
|
||||
import top.rrricardo.chiara.model.HomePageState
|
||||
import top.rrricardo.chiara.openapi.api.AlbumApi
|
||||
import top.rrricardo.chiara.openapi.api.SeasonApi
|
||||
import top.rrricardo.chiara.openapi.client.infrastructure.ApiClient
|
||||
import top.rrricardo.chiara.openapi.model.AlbumResponse
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomePageViewModel @Inject constructor(
|
||||
private val apiClient: ApiClient
|
||||
) : ViewModel() {
|
||||
|
||||
var homePageState by mutableStateOf(HomePageState())
|
||||
private set
|
||||
|
||||
|
||||
fun changeToMusicPage() {
|
||||
homePageState = homePageState.copy(
|
||||
isMusicPage = true
|
||||
)
|
||||
}
|
||||
|
||||
fun changeToVideoPage() {
|
||||
homePageState = homePageState.copy(
|
||||
isMusicPage = false
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchRemoteData() {
|
||||
if (homePageState.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
homePageState = homePageState.copy(
|
||||
loading = true,
|
||||
selectedAlbum = null,
|
||||
selectedSeason = null,
|
||||
)
|
||||
|
||||
val albumApi = apiClient.createService(AlbumApi::class.java)
|
||||
val seasonApi = apiClient.createService(SeasonApi::class.java)
|
||||
viewModelScope.launch(CoroutineExceptionHandler { _, _ ->
|
||||
run {
|
||||
homePageState = homePageState.copy(
|
||||
loading = false,
|
||||
errorMessage = "网络错误"
|
||||
)
|
||||
}
|
||||
}) {
|
||||
val albumResponse = albumApi.apiAlbumGet()
|
||||
val albums = albumResponse.body()
|
||||
|
||||
homePageState = if (albumResponse.isSuccessful && albums != null) {
|
||||
homePageState.copy(
|
||||
loading = false,
|
||||
albums = albums.map {
|
||||
it.copy(
|
||||
coverImageUrl = Configuration.SERVER_ADDRESS + it.coverImageUrl
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
homePageState.copy(
|
||||
loading = false,
|
||||
errorMessage = "网络错误"
|
||||
)
|
||||
}
|
||||
|
||||
val seasonResponse = seasonApi.apiSeasonGet()
|
||||
val seasons = seasonResponse.body()
|
||||
|
||||
homePageState = if (seasonResponse.isSuccessful && seasons != null) {
|
||||
homePageState.copy(
|
||||
loading = false,
|
||||
seasons = seasons
|
||||
)
|
||||
} else {
|
||||
homePageState.copy(
|
||||
loading = false,
|
||||
errorMessage = "网络错误"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAlbum(selectedAlbum: AlbumResponse) {
|
||||
if (selectedAlbum == homePageState.selectedAlbum) {
|
||||
return
|
||||
}
|
||||
|
||||
homePageState = homePageState.copy(
|
||||
selectedAlbum = selectedAlbum
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import top.rrricardo.chiara.model.MusicControllerState
|
||||
import top.rrricardo.chiara.model.PlayerState
|
||||
import top.rrricardo.chiara.openapi.model.SongResponse
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
val musicController: MusicController
|
||||
) : ViewModel() {
|
||||
var musicControllerState by mutableStateOf(MusicControllerState())
|
||||
private set
|
||||
|
||||
var playList: List<SongResponse> by mutableStateOf(emptyList())
|
||||
private set
|
||||
|
||||
init {
|
||||
musicController.mediaControllerCallback = { playerState,
|
||||
currentMusic,
|
||||
currentPosition,
|
||||
totalDuration,
|
||||
repeatState ->
|
||||
musicControllerState = musicControllerState.copy(
|
||||
playerState = playerState,
|
||||
currentSong = currentMusic,
|
||||
currentPosition = currentPosition,
|
||||
totalDuration = totalDuration,
|
||||
repeatState = repeatState
|
||||
)
|
||||
|
||||
if (playerState == PlayerState.PLAYING) {
|
||||
viewModelScope.launch {
|
||||
while (true) {
|
||||
// 每秒钟更新一次播放器的状态
|
||||
delay(1.seconds)
|
||||
musicControllerState = musicControllerState.copy(
|
||||
currentPosition = musicController.getCurrentPosition()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyMediaController() = musicController.destroy()
|
||||
|
||||
fun refreshPlayList() {
|
||||
playList = musicController.getPlayList()
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import top.rrricardo.chiara.model.ShowPageState
|
||||
import top.rrricardo.chiara.openapi.api.SeasonApi
|
||||
import top.rrricardo.chiara.openapi.client.infrastructure.ApiClient
|
||||
import top.rrricardo.chiara.openapi.model.EpisodeResponse
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ShowPageViewModel @Inject constructor(
|
||||
apiClient: ApiClient
|
||||
) : ViewModel() {
|
||||
private val showSeasonApi = apiClient.createService(SeasonApi::class.java)
|
||||
|
||||
var showPageState by mutableStateOf(ShowPageState())
|
||||
private set
|
||||
|
||||
fun fetchShowSeason(id: Int) {
|
||||
showPageState = showPageState.copy(
|
||||
loading = true
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
val response = showSeasonApi.apiSeasonIdGet(id)
|
||||
val showSeason = response.body()
|
||||
|
||||
showPageState = if (response.isSuccessful && showSeason != null) {
|
||||
showPageState.copy(
|
||||
loading = false,
|
||||
showSeason = showSeason
|
||||
)
|
||||
} else {
|
||||
showPageState.copy(
|
||||
loading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectEpisode(selectedEpisode: EpisodeResponse) {
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.palette.graphics.Palette
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SongViewModel @Inject constructor(
|
||||
val musicController: MusicController
|
||||
) : ViewModel() {
|
||||
fun calculateColorPalette(drawable: Bitmap, onFinish: (Color) -> Unit) {
|
||||
Palette.from(drawable).generate { palette ->
|
||||
palette?.dominantSwatch?.rgb?.let {
|
||||
onFinish(Color(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
package top.rrricardo.chiara.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import top.rrricardo.chiara.Configuration
|
||||
import top.rrricardo.chiara.model.VideoPageState
|
||||
import top.rrricardo.chiara.openapi.api.HlsApi
|
||||
import top.rrricardo.chiara.openapi.api.SeasonApi
|
||||
import top.rrricardo.chiara.openapi.client.infrastructure.ApiClient
|
||||
import top.rrricardo.chiara.service.MusicController
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class VideoPageViewModel @Inject constructor(
|
||||
val musicController: MusicController,
|
||||
apiClient: ApiClient
|
||||
) : ViewModel() {
|
||||
private val hlsApi = apiClient.createService(HlsApi::class.java)
|
||||
private val showSeasonApi = apiClient.createService(SeasonApi::class.java)
|
||||
|
||||
var videoPageState by mutableStateOf(VideoPageState())
|
||||
private set
|
||||
|
||||
val snackBarHostState = SnackbarHostState()
|
||||
|
||||
fun fetchAndPlay(id: Int) {
|
||||
videoPageState = videoPageState.copy(
|
||||
loading = true
|
||||
)
|
||||
musicController.mediaControllerErrorCallback = {
|
||||
showPlayerException(it)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val response = hlsApi.apiHlsStartEpisodeIdPost(id)
|
||||
val playList = response.body()
|
||||
|
||||
if (response.isSuccessful && playList != null) {
|
||||
videoPageState = videoPageState.copy(
|
||||
loading = false,
|
||||
playing = true
|
||||
)
|
||||
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setMediaId(Configuration.SERVER_ADDRESS + playList.playList)
|
||||
.setUri(Configuration.SERVER_ADDRESS + playList.playList)
|
||||
.build()
|
||||
Log.i("Video Player", Configuration.SERVER_ADDRESS + playList.playList)
|
||||
musicController.playVideo(mediaItem)
|
||||
} else {
|
||||
videoPageState = videoPageState.copy(
|
||||
loading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchShow(showId: Int, videoId: Int) {
|
||||
viewModelScope.launch {
|
||||
val response = showSeasonApi.apiSeasonIdGet(showId)
|
||||
val showSeasonResponse = response.body()
|
||||
|
||||
if (response.isSuccessful && showSeasonResponse != null) {
|
||||
videoPageState = videoPageState.copy(
|
||||
showSeasonResponse = showSeasonResponse,
|
||||
episodeResponse = showSeasonResponse.episodes.find {
|
||||
it.id == videoId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTransaction() {
|
||||
musicController.mediaControllerErrorCallback = null
|
||||
|
||||
if (videoPageState.playing) {
|
||||
musicController.stopVideo()
|
||||
viewModelScope.launch {
|
||||
hlsApi.apiHlsStopPost()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlayer() = musicController.player
|
||||
|
||||
private fun showPlayerException(exception: PlaybackException) {
|
||||
viewModelScope.launch {
|
||||
snackBarHostState.showSnackbar("设备不支持此类型视频!")
|
||||
}
|
||||
}
|
||||
}
|
864
ChiaraAndroid/app/src/main/openapi/chiara.json
Normal file
@@ -0,0 +1,864 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "Chiara",
|
||||
"version": "1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/Album": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Album"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Album/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Album"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/File/{key}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"File"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Hls/{file}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Hls"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "file",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Hls/start/{episodeId}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Hls"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "episodeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VideoPlayingResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VideoPlayingResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/VideoPlayingResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Hls/stop": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Hls"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/MediaRepository": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"MediaRepository"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"MediaRepository"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateMediaRepositoryRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateMediaRepositoryRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateMediaRepositoryRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/MediaRepository/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"MediaRepository"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"MediaRepository"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaRepositoryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Season": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Season"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Season/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Season"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Song": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Song"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Song/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Song"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"style": "simple",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"AlbumResponse": {
|
||||
"required": [
|
||||
"arist",
|
||||
"coverImageUrl",
|
||||
"id",
|
||||
"songs",
|
||||
"title"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"title": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"arist": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"coverImageUrl": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"songs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SongResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"CreateMediaRepositoryRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
"path"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EpisodeResponse": {
|
||||
"required": [
|
||||
"episodeNumber",
|
||||
"id",
|
||||
"showSeasonId",
|
||||
"title"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"title": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"episodeNumber": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"showSeasonId": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MediaRepositoryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"albumResponses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumResponse"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"showSeasonResponses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ShowSeasonResponse"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ProblemDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"instance": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": { }
|
||||
},
|
||||
"ShowSeasonResponse": {
|
||||
"required": [
|
||||
"episodes",
|
||||
"id",
|
||||
"title"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"title": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"episodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EpisodeResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SongResponse": {
|
||||
"required": [
|
||||
"arist",
|
||||
"coverImageUrl",
|
||||
"id",
|
||||
"title",
|
||||
"url"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"title": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"arist": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"coverImageUrl": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VideoPlayingResponse": {
|
||||
"required": [
|
||||
"playList"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"playList": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
ChiaraAndroid/app/src/main/res/drawable/ic_image.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="89"
|
||||
android:viewportHeight="177.23065">
|
||||
<group android:scaleX="0.40655723"
|
||||
android:scaleY="0.8096"
|
||||
android:translateX="26.408203"
|
||||
android:translateY="16.872358">
|
||||
<group android:translateY="132.92299">
|
||||
<path android:pathData="M44.390625,-0Q35.453125,-0,27.25,-3.578125Q19.0625,-7.15625,13,-13.21875Q6.953125,-19.28125,3.4375,-27.6875Q-0.0625,-36.109375,0.078125,-45.265625Q0.21875,-54.0625,3.765625,-62.03125Q7.3125,-70,13.21875,-75.828125Q19.125,-81.671875,27.390625,-85.140625Q35.671875,-88.609375,44.90625,-88.609375Q53.84375,-88.609375,61.953125,-85.03125Q70.078125,-81.453125,76.015625,-75.390625Q81.96875,-69.34375,85.40625,-60.921875Q88.84375,-52.5,88.6875,-43.34375Q88.546875,-36.328125,86.25,-29.75Q83.96875,-23.1875,79.828125,-17.828125Q75.703125,-12.484375,70.3125,-8.453125Q64.921875,-4.4375,58.1875,-2.21875Q51.46875,-0,44.390625,-0ZM28.65625,-71.859375L28.65625,-17.203125L29.09375,-17.203125L68.96875,-43.796875Q69.5,-44.3125,68.96875,-44.3125Q40.390625,-63.4375,28.65625,-70.890625Q28.875,-71.109375,28.984375,-71.328125Q29.09375,-71.5625,28.984375,-71.703125Q28.875,-71.859375,28.65625,-71.859375Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
5
ChiaraAndroid/app/src/main/res/drawable/ic_music.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z"/>
|
||||
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM17,7h-2v5.37c0,1.27 -0.9,2.44 -2.16,2.6 -1.69,0.23 -3.11,-1.25 -2.8,-2.95 0.2,-1.1 1.18,-1.95 2.3,-2.02 0.63,-0.04 1.2,0.16 1.66,0.51L14,6c0,-0.55 0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1zM3,6c-0.55,0 -1,0.45 -1,1v13c0,1.1 0.9,2 2,2h13c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L5,20c-0.55,0 -1,-0.45 -1,-1L4,7c0,-0.55 -0.45,-1 -1,-1z"/>
|
||||
|
||||
</vector>
|
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,19c1.1,0 2,-0.9 2,-2L10,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2v10c0,1.1 0.9,2 2,2zM14,7v10c0,1.1 0.9,2 2,2s2,-0.9 2,-2L18,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2z"/>
|
||||
</vector>
|
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z"/>
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,7h10v1.79c0,0.45 0.54,0.67 0.85,0.35l2.79,-2.79c0.2,-0.2 0.2,-0.51 0,-0.71l-2.79,-2.79c-0.31,-0.31 -0.85,-0.09 -0.85,0.36L17,5L6,5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,7zM17,17L7,17v-1.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-2.79,2.79c-0.2,0.2 -0.2,0.51 0,0.71l2.79,2.79c0.31,0.31 0.85,0.09 0.85,-0.36L7,19h11c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3z"/>
|
||||
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,7h10v1.79c0,0.45 0.54,0.67 0.85,0.35l2.79,-2.79c0.2,-0.2 0.2,-0.51 0,-0.71l-2.79,-2.79c-0.31,-0.31 -0.85,-0.09 -0.85,0.36L17,5L6,5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,7zM17,17L7,17v-1.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-2.79,2.79c-0.2,0.2 -0.2,0.51 0,0.71l2.79,2.79c0.31,0.31 0.85,0.09 0.85,-0.36L7,19h11c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3zM13,14.25L13,9.81c0,-0.45 -0.36,-0.81 -0.81,-0.81 -0.13,0 -0.25,0.03 -0.36,0.09l-1.49,0.74c-0.21,0.1 -0.34,0.32 -0.34,0.55 0,0.34 0.28,0.62 0.62,0.62h0.88v3.25c0,0.41 0.34,0.75 0.75,0.75s0.75,-0.34 0.75,-0.75z"/>
|
||||
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.59,9.17L6.12,4.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41l4.46,4.46 1.42,-1.4zM15.35,4.85l1.19,1.19L4.7,17.88c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L17.96,7.46l1.19,1.19c0.31,0.31 0.85,0.09 0.85,-0.36L20,4.5c0,-0.28 -0.22,-0.5 -0.5,-0.5h-3.79c-0.45,0 -0.67,0.54 -0.36,0.85zM14.83,13.41l-1.41,1.41 3.13,3.13 -1.2,1.2c-0.31,0.31 -0.09,0.85 0.36,0.85h3.79c0.28,0 0.5,-0.22 0.5,-0.5v-3.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-1.19,1.19 -3.13,-3.14z"/>
|
||||
|
||||
</vector>
|
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,6c-0.55,0 -1,0.45 -1,1v13c0,1.1 0.9,2 2,2h13c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L5,20c-0.55,0 -1,-0.45 -1,-1L4,7c0,-0.55 -0.45,-1 -1,-1zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l5.47,4.1c0.27,0.2 0.27,0.6 0,0.8L12,14.5z"/>
|
||||
|
||||
</vector>
|
BIN
ChiaraAndroid/app/src/main/res/drawable/vinyl_background.png
Normal file
After Width: | Height: | Size: 549 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_login_background"/>
|
||||
<foreground android:drawable="@drawable/ic_login_foreground"/>
|
||||
</adaptive-icon>
|
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_login_background"/>
|
||||
<foreground android:drawable="@drawable/ic_login_foreground"/>
|
||||
</adaptive-icon>
|
BIN
ChiaraAndroid/app/src/main/res/mipmap-hdpi/ic_login.webp
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-hdpi/ic_login_round.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-mdpi/ic_login.webp
Normal file
After Width: | Height: | Size: 784 B |
BIN
ChiaraAndroid/app/src/main/res/mipmap-mdpi/ic_login_round.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-xhdpi/ic_login.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-xhdpi/ic_login_round.webp
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-xxhdpi/ic_login.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-xxhdpi/ic_login_round.webp
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
ChiaraAndroid/app/src/main/res/mipmap-xxxhdpi/ic_login.webp
Normal file
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 8.4 KiB |
10
ChiaraAndroid/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_login_background">#1672FF</color>
|
||||
</resources>
|
3
ChiaraAndroid/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Chiara</string>
|
||||
</resources>
|
5
ChiaraAndroid/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Chiara" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
13
ChiaraAndroid/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
19
ChiaraAndroid/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
@@ -0,0 +1,17 @@
|
||||
package top.rrricardo.chiara
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
9
ChiaraAndroid/build.gradle.kts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.hilt.android) apply false
|
||||
alias(libs.plugins.kotlin.ksp) apply false
|
||||
alias(libs.plugins.openapi.generator) apply false
|
||||
}
|
23
ChiaraAndroid/gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
74
ChiaraAndroid/gradle/libs.versions.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
[versions]
|
||||
agp = "8.4.0"
|
||||
hiltAndroid = "2.51.1"
|
||||
hiltAndroidCompiler = "2.51.1"
|
||||
kotlin = "1.9.0"
|
||||
ksp = "1.9.0-1.0.13"
|
||||
coreKtx = "1.12.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeCompose = "2.7.0"
|
||||
lifecycleLivedataKtx = "2.7.0"
|
||||
lifecycleViewmodelSavedstate = "2.7.0"
|
||||
lifecycleRuntimeKtx = "2.7.0"
|
||||
lifecycleViewmodelCompose = "2.7.0"
|
||||
lifecycleViewmodelKtx = "2.7.0"
|
||||
navigationCompose = "2.7.7"
|
||||
activityCompose = "1.8.0"
|
||||
composeBom = "2023.08.00"
|
||||
palette = "1.0.0"
|
||||
constraintlayout-compose = "1.0.1"
|
||||
coil = "2.5.0"
|
||||
media3 = "1.3.1"
|
||||
okhttp3 = "4.12.0"
|
||||
retrofit = "2.10.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.1.0" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-compiler", version.ref = "lifecycleRuntimeCompose" }
|
||||
androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycleViewmodelSavedstate" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
||||
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-palette = { group = "androidx.palette", name = "palette", version.ref = "palette" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version = "1.2.1" }
|
||||
constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayout-compose" }
|
||||
coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||
media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
|
||||
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||
media3-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
|
||||
okhttp3 = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" }
|
||||
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" }
|
||||
retrofit-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" }
|
||||
retrofit-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
|
||||
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
openapi-generator = { id = "org.openapi.generator", version = "7.5.0" }
|
||||
|
||||
|
||||
|
BIN
ChiaraAndroid/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
ChiaraAndroid/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
#Sat May 18 22:35:36 CST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
185
ChiaraAndroid/gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
89
ChiaraAndroid/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
33
ChiaraAndroid/settings.gradle.kts
Normal file
@@ -0,0 +1,33 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven { setUrl("https://maven.aliyun.com/repository/central") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/jcenter") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/google") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/public") }
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
maven { setUrl("https://maven.aliyun.com/repository/central") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/jcenter") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/google") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
|
||||
maven { setUrl("https://maven.aliyun.com/repository/public") }
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Chiara"
|
||||
include(":app")
|