Repository: sskEvan/NCMusicDesktop
Branch: master
Commit: 378011898276
Files: 107
Total size: 292.2 KB
Directory structure:
gitextract_hkxa2m6e/
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── artifacts/
│ │ └── NCMusicDesktop_jvm_1_0_SNAPSHOT.xml
│ ├── gradle.xml
│ ├── kotlinc.xml
│ ├── misc.xml
│ ├── uiDesigner.xml
│ └── vcs.xml
├── README.md
├── build.gradle.kts
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── launcher/
│ └── icon.icns
├── proguard-rules.pro
├── settings.gradle.kts
└── src/
└── jvmMain/
├── kotlin/
│ ├── Main.kt
│ ├── base/
│ │ ├── AppConfig.kt
│ │ ├── BaseViewModel.kt
│ │ ├── MusicPlayController.kt
│ │ ├── UserManager.kt
│ │ └── player/
│ │ ├── IPlayer.kt
│ │ ├── IPlayerListener.kt
│ │ ├── NCPlayer.kt
│ │ ├── PlayMode.kt
│ │ └── PlayerStatus.kt
│ ├── http/
│ │ ├── RetrofitClient.kt
│ │ ├── api/
│ │ │ └── NCApi.kt
│ │ └── interceptor/
│ │ └── CookieInterceptor.kt
│ ├── model/
│ │ ├── BasePagingBean.kt
│ │ ├── BaseResult.kt
│ │ ├── CommentResult.kt
│ │ ├── LoginResult.kt
│ │ ├── LyricResult.kt
│ │ ├── NewSongResult.kt
│ │ ├── PlayListResult.kt
│ │ ├── PrivateContentResult.kt
│ │ ├── RecommendMVResult.kt
│ │ └── SongDetailResult.kt
│ ├── router/
│ │ ├── NavGraph.kt
│ │ └── RouterUrls.kt
│ ├── ui/
│ │ ├── common/
│ │ │ ├── CommonImage.kt
│ │ │ ├── CommonTabLayout.kt
│ │ │ ├── CpnActionMore.kt
│ │ │ ├── ExpandableText.kt
│ │ │ ├── ListToGridItems.kt
│ │ │ ├── LoadingComponent.kt
│ │ │ ├── ModifierExt.kt
│ │ │ ├── NoSuccessComponent.kt
│ │ │ ├── PaingFooterNumBar.kt
│ │ │ ├── SeekBar.kt
│ │ │ ├── TableLayout.kt
│ │ │ ├── ViewStateComponent.kt
│ │ │ ├── ViewStateLazyGridPagingComponent.kt
│ │ │ └── toast/
│ │ │ └── Toast.kt
│ │ ├── discovery/
│ │ │ ├── DiscoveryPage.kt
│ │ │ └── cpn/
│ │ │ ├── CpnHighQualityPlayListEntrance.kt
│ │ │ ├── CpnNewSongEntrance.kt
│ │ │ ├── CpnPersonalRecommend.kt
│ │ │ ├── CpnPlayListItem.kt
│ │ │ ├── CpnPlayListTabSelectedBar.kt
│ │ │ ├── CpnPrivateContentEntrance.kt
│ │ │ ├── CpnRecommandPlayListEntrance.kt
│ │ │ ├── CpnRecommendPlayList.kt
│ │ │ └── CpnRecommentMVEntrance.kt
│ │ ├── login/
│ │ │ └── QrcodeLoginDialog.kt
│ │ ├── main/
│ │ │ ├── MainPage.kt
│ │ │ └── cpn/
│ │ │ ├── CpnMainLeftMenu.kt
│ │ │ ├── CpnMainMusicPlayDrawer.kt
│ │ │ ├── CpnMainRightContainer.kt
│ │ │ ├── CpnMusicPlayBottomBar.kt
│ │ │ ├── CpnPlaformDecoratedButtons.kt
│ │ │ └── CpnThemePopup.kt
│ │ ├── play/
│ │ │ ├── CpnCurrentPlayListSheet.kt
│ │ │ ├── CpnLyric.kt
│ │ │ ├── CpnMusicPlay.kt
│ │ │ └── CpnSongInfo.kt
│ │ ├── playlist/
│ │ │ ├── PlayListDetailPage.kt
│ │ │ └── cpn/
│ │ │ ├── CpnPlayListCommentList.kt
│ │ │ ├── CpnPlayListSubscribers.kt
│ │ │ └── CpnTrackList.kt
│ │ ├── setting/
│ │ │ └── SettingPage.kt
│ │ ├── theme/
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── color/
│ │ │ ├── AppColors.kt
│ │ │ └── palette/
│ │ │ ├── dark/
│ │ │ │ └── DartColorPalette.kt
│ │ │ └── light/
│ │ │ ├── BlueColorPalette.kt
│ │ │ ├── DefaultColorPalette.kt
│ │ │ ├── GreenColorPalette.kt
│ │ │ ├── OriginColorPalette.kt
│ │ │ ├── PurpleColorPalette.kt
│ │ │ └── YellowColorPalette.kt
│ │ └── todo/
│ │ ├── TestPage.kt
│ │ └── TodoPage.kt
│ └── util/
│ ├── DataStoreUtils.kt
│ ├── DensityExt.kt
│ ├── EnvUtil.kt
│ ├── LyricUtil.kt
│ ├── QrcodeUtil.kt
│ ├── StringUtil.kt
│ ├── TimeUtil.kt
│ └── createDataStore.kt
└── resources/
└── image/
├── ic_empty.xml
├── ic_load_error.xml
└── ic_network_error.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
================================================
FILE: .idea/artifacts/NCMusicDesktop_jvm_1_0_SNAPSHOT.xml
================================================
$PROJECT_DIR$/build/libs
================================================
FILE: .idea/gradle.xml
================================================
================================================
FILE: .idea/kotlinc.xml
================================================
================================================
FILE: .idea/misc.xml
================================================
================================================
FILE: .idea/uiDesigner.xml
================================================
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: README.md
================================================
# NCMusicDesktop
小明非常喜欢网易云,去年刚用Jetpack Compose写了个仿网易云app [NCMusic](https://github.com/sskEvan/NCMusic) ,最近发现compose-jb正式版已经发布到了v1.3.1,
又玩了一下Compose Desktop,决定搞了个桌面版的NCMusicDesktop,数据源还是来自[Binaryify](https://github.com/Binaryify)大佬的[NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)~
由于以前没有开发桌面应用的经验,索性想按照Android jetpack的套路来开发,然而Navigation、Lifecycle 、ViewModel、LiveData等等这些在compose-jb中,暂时通通没有~
不要慌,一番查找在掘金上看到一篇文章[《推销 Compose 跨平台 Navigation:PreCompose》](https://juejin.cn/post/7122056172084920334),
讲了[Precompose](https://github.com/Tlaster/PreCompose)这个跨平台Navigation框架的使用, 它基本复刻了Jetpack Navigation、Lifecycle、ViewModel这些组件,
使用方式也基本保持一致,美滋滋!当然LiveData已经被废弃了,推荐使用Flow代替~至于网络请求,Retrofit照用不误,又一次美滋滋~
### 怎么用Android老套路来写Desktop应用
- 老规矩,先定义一波BaseResult、BaseViewModel、ViewStateComponent(页面状态切换组件)
```
代码: 略
```
- Model层
```
class LyricResult(
val transUser: LyricContributorBean?,
val lyricUser: LyricContributorBean?,
val lrc: LrcBean?,
val tlyric: LrcBean?
) : BaseResult()
```
- ViewModel层
```
class CpnLyricViewModel : BaseViewModel() {
fun getLyric(id: Long) = launchFlow {
NCRetrofitClient.getNCApi().getLyric(id)
}
}
interface NCApi {
@GET("/lyric")
suspend fun getLyric(@Query("id") id: Long): LyricResult
}
```
- View层
```
@Composable
fun CpnLyric() {
ViewStateComponent(
key = "CpnLyric-${id}",
loadDataBlock = {viewModel.getLyric(id)}
) {
LyricList(it)
}
}
```
### 怎么播放音乐
至于在Compose Desktop上怎么播放音乐呢,毕竟没有Android的MediaPlayer,在github上找了找,发现[succlz123](https://github.com/succlz123)大佬开源的Compose Multiplatform项目
[AcFun-Client-Multiplatform](https://github.com/succlz123/AcFun-Client-Multiplatform),里面有视频播放的功能,是基于[vlcj](https://github.com/caprica/vlcj)来实现的,看了下vlcj的api,使用AudioPlayerComponent播放音乐不是问题
### 关于嵌套滑动
开发过程中,有些交互感觉需要涉及到嵌套滑动,在Jetpack Compose中,使用NestedScrollConnection来处理嵌套滑动到场景,于是乎,写了一堆✨✨代码后,
发现NestedScrollConnection在Compose Desktop中完全不起作用,后面找了下github的issue,发现有哥们也遇到了哈哈哈,然而官方21年的回复是暂时没有计划,
到现在还是没有解决,凉飕飕~


### 第三方框架
- [PreCompose](https://github.com/Tlaster/PreCompose)
- [zxing](https://github.com/zxing/zxing)
- [compose-imageloader-desktop](https://github.com/succlz123/compose-desktop-imageloader)
- [vlcj](https://github.com/caprica/vlcj)
### 运行效果图






================================================
FILE: build.gradle.kts
================================================
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
group = "com.ssk.NCMusicDesktop"
version = "1.0-SNAPSHOT"
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://jitpack.io")
}
tasks.withType {
kotlinOptions.jvmTarget = "11"
}
kotlin {
jvm {
jvmToolchain(11)
withJava()
}
sourceSets {
val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation("com.google.zxing:javase:3.3.3")
implementation("moe.tlaster:precompose:1.3.14")
implementation("androidx.datastore:datastore-preferences-core:1.1.0-dev01")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("io.github.succlz123:compose-imageloader-desktop:0.0.2")
implementation("uk.co.caprica:vlcj:4.7.3")
}
}
val jvmTest by getting
}
}
compose.desktop {
application {
javaHome = "/Users/anmin83/Library/Java/JavaVirtualMachines/corretto-17.0.6/Contents/Home"
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Exe)
packageName = "NCMusicDesktop"
packageVersion = "1.0.0"
// includeAllModules = true
modules("java.instrument", "java.sql", "jdk.unsupported")
macOS {
// a version for all macOS distributables
packageVersion = "1.0.0"
// a version only for the dmg package
dmgPackageVersion = "1.0.0"
// a version only for the pkg package
pkgPackageVersion = "1.0.0"
// 显示在菜单栏、“关于”菜单项、停靠栏等中的应用程序名称
dockName = "NCMusicDesktop"
// a build version for all macOS distributables
packageBuildVersion = "1.0.0"
// a build version only for the dmg package
dmgPackageBuildVersion = "1.0.0"
// a build version only for the pkg package
pkgPackageBuildVersion = "1.0.0"
// 设置图标
iconFile.set(project.file("launcher/icon.icns"))
}
windows {
// a version for all Windows distributables
packageVersion = "1.0.0"
// a version only for the msi package
msiPackageVersion = "1.0.0"
// a version only for the exe package
exePackageVersion = "1.0.0"
// 设置图标
iconFile.set(project.file("launcher/icon.ico"))
}
}
buildTypes.release.proguard {
obfuscate.set(false)
configurationFiles.from(project.file("proguard-rules.pro"))
}
}
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
kotlin.code.style=official
kotlin.version=1.7.20
agp.version=7.3.0
compose.version=1.3.1
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${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 "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# 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 ;; #(
MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@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
================================================
FILE: proguard-rules.pro
================================================
# DataStore 混淆
-dontwarn androidx.datastore.**
# Retrofit2 混淆
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
# OkHttp3
-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**
-dontwarn okhttp3.internal.**
# Retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
# Gson
-keep class com.google.gson.stream.** { *; }
-keepattributes EnclosingMethod
================================================
FILE: settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
kotlin("multiplatform").version(extra["kotlin.version"] as String)
id("org.jetbrains.compose").version(extra["compose.version"] as String)
}
}
rootProject.name = "NCMusicDesktop"
================================================
FILE: src/jvmMain/kotlin/Main.kt
================================================
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import base.AppConfig
import moe.tlaster.precompose.PreComposeWindow
import moe.tlaster.precompose.navigation.rememberNavigator
import org.succlz123.lib.imageloader.core.ImageLoader
import router.NCNavigatorManager
import ui.common.theme.AppTheme
import ui.common.theme.themeTypeState
import ui.main.MainPage
import ui.main.cpn.CpnWindowsPlaformDecoratedButtons
import util.EnvUtil
import java.awt.Dimension
import java.io.File
fun main() = application {
initImageLoader()
val windowState = rememberWindowState(size = DpSize(AppConfig.windowMinWidth, AppConfig.windowMinHeight))
PreComposeWindow(
state = windowState,
onCloseRequest = ::exitApplication,
undecorated = EnvUtil.isWindows(),
title = ""
) {
window.minimumSize = Dimension(AppConfig.windowMinWidth.value.toInt(), AppConfig.windowMinHeight.value.toInt())
window.rootPane.apply {
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
}
App()
CpnWindowsPlaformDecoratedButtons(windowState)
}
}
@Composable
@Preview
private fun App() {
AppTheme(themeTypeState.value) {
NCNavigatorManager.navigator = rememberNavigator()
MainPage()
}
}
private fun initImageLoader() {
ImageLoader.configuration(rootDirectory = File(AppConfig.cacheRootDir))
}
================================================
FILE: src/jvmMain/kotlin/base/AppConfig.kt
================================================
package base
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
import java.io.File
object AppConfig {
val topBarHeight = 50.dp
val windowMinWidth = 1000.dp
val windowMinHeight = 680.dp
var fullScreen = false
// 应用缓存目录
val cacheRootDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "NCMusicDesktop"
}
================================================
FILE: src/jvmMain/kotlin/base/BaseViewModel.kt
================================================
package base
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import model.BaseResult
import moe.tlaster.precompose.viewmodel.ViewModel
import moe.tlaster.precompose.viewmodel.viewModelScope
typealias ViewStateMutableStateFlow = MutableStateFlow>
open class BaseViewModel : ViewModel() {
protected fun launchFlow(
handleSuccessBlock: ((T) -> Unit)? = null,
handleFailBlock: ((code: Int?, message: String?) -> Unit)? = null,
judgeEmpty: ((T) -> Boolean)? = null,
call: suspend () -> T
): ViewStateMutableStateFlow {
val flow = MutableStateFlow>(ViewState.Loading)
viewModelScope.launch {
runCatching {
call()
}.onSuccess { result ->
if (result.resultOk()) {
if (result.isEmpty() || judgeEmpty?.invoke(result) == true) {
flow.emit(ViewState.Empty)
} else {
handleSuccessBlock?.invoke(result)
flow.emit(ViewState.Success(result))
}
} else {
handleFailBlock?.invoke(result.code ?: -1, result.message ?: "请求出错")
flow.emit(ViewState.Fail(result.code?.toString() ?: "-1", result.message ?: "请求出错"))
}
}.onFailure { e ->
flow.emit(ViewState.Error(e))
}
}
return flow
}
protected fun launch(
handleSuccessBlock: ((T) -> Unit)? = null,
handleFailBlock: ((code: Int?, message: String?) -> Unit)? = null,
call: suspend () -> T
) : Job {
return viewModelScope.launch {
runCatching {
call()
}.onSuccess { result ->
if (result.resultOk()) {
handleSuccessBlock?.invoke(result)
} else {
handleFailBlock?.invoke(result.code ?: -1, result.message ?: "请求出错")
}
}.onFailure { e ->
handleFailBlock?.invoke(-1000, "请求出错")
e.printStackTrace()
}
}
}
}
sealed class ViewState {
object Loading : ViewState()
data class Success(val data: T) : ViewState()
object Empty : ViewState()
data class Fail(val errorCode: String, val errorMsg: String) : ViewState()
data class Error(val exception: Throwable) : ViewState()
}
================================================
FILE: src/jvmMain/kotlin/base/MusicPlayController.kt
================================================
package base
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import base.player.*
import model.SongBean
import ui.common.toast.ToastManager
import util.StringUtil
import java.util.*
object MusicPlayController : IPlayerListener {
// 是否显示音乐播放抽屉组件
var showMusicPlayDrawer by mutableStateOf(false)
// 是否显示播当前播放列表
var showCurPlayListSheet by mutableStateOf(false)
// 原始歌曲列表
var originSongList = mutableStateListOf()
// 当前播放模式下的实际歌曲列表
var realSongList = mutableStateListOf()
var curSongBean by mutableStateOf(null)
// 当前播放的歌曲在原始歌曲列表中的索引
var curOriginIndex by mutableStateOf(-1)
private set
// 当前播放的歌曲在当前播放模式下的实际歌曲列表中的索引
var curRealIndex by mutableStateOf(-1)
private set
// 当前播放进度
var progress by mutableStateOf(0f)
// 当前歌曲播放位置时间文本
var curPositionStr by mutableStateOf("00:00")
// 当前歌曲总时长文本
var totalDuringStr by mutableStateOf("00:00")
// 是否播放中
private var playing by mutableStateOf(false)
// 是否允许拖动进度条
var enableSeeking by mutableStateOf(false)
private set
// 播放模式
var playMode by mutableStateOf(PlayMode.LOOP)
private set
// 当前播放歌曲总时长
private var totalDuring = 0
// 是否拖动进度条中
private var seeking = false
// 当前播放状态
private var playerStatus: PlayerStatus = PlayerStatus.IDLE
val mediaPlayer: IPlayer by lazy { NCPlayer().apply {
addListener(this@MusicPlayController)
} }
/**
* 播放音乐列表
*/
fun playMusicList(songBeans: List, originIndex: Int) {
originSongList.clear()
originSongList.addAll(songBeans)
println("MusicPlayController playMusicList curOriginIndex=${originIndex}")
generateRealSongList(originIndex)
innerPlay(originSongList[originIndex])
}
/**
* 生成当前播放模式下的歌曲列表
*/
private fun generateRealSongList(originIndex: Int) {
when (playMode) {
PlayMode.RANDOM -> {
val randomList = mutableListOf()
randomList.addAll(originSongList)
randomList.shuffle()
realSongList.clear()
realSongList.addAll(randomList)
val realIndex = realSongList.indexOfFirst { it.id == originSongList[originIndex].id }
if (realIndex != originIndex) {
Collections.swap(realSongList, realIndex, originIndex)
}
curOriginIndex = originIndex
curRealIndex = originIndex
}
else -> {
realSongList.clear()
realSongList.addAll(originSongList)
curOriginIndex = originIndex
curRealIndex = originIndex
}
}
// originSongList.forEachIndexed { index, item ->
// println("songList $index --> ${item.name}")
// }
// println("---------------------------------------")
//
// realSongList.forEachIndexed { index, item ->
// println("pagerSongList $index --> ${item.name}")
// }
}
private fun innerPlay(songBean: SongBean) {
curSongBean = songBean
mediaPlayer.setDataSource(songBean)
mediaPlayer.start()
}
/**
* 根据原始歌曲列表索引播放音乐
*/
// fun playByOriginIndex(originIndex: Int) {
// if (originSongList.size > originIndex) {
// curOriginIndex = originIndex
// curRealIndex = realSongList.indexOfFirst { it.id == originSongList[originIndex].id }
// innerPlay(originSongList[curOriginIndex])
// }
// }
/**
* 根据实际播放模式中的歌曲列表索引播放音乐
*/
fun playByRealIndex(realIndex: Int) {
if (originSongList.getOrNull(realIndex) != null) {
curRealIndex = realIndex
curOriginIndex = originSongList.indexOfFirst { it.id == realSongList[realIndex].id }
innerPlay(realSongList[curRealIndex])
}
}
override fun onStatusChanged(status: PlayerStatus) {
playerStatus = status
playing = status == PlayerStatus.STARTED
enableSeeking = status == PlayerStatus.STARTED || status == PlayerStatus.PAUSED
when (status) {
PlayerStatus.COMPLETED -> {
autoPlayNext()
}
is PlayerStatus.ERROR -> {
println("PlayerStatus.ERROR->${status.errorMsg}")
if (status.errorCode != PlayerErrorCode.ERROR_ENV_INVALID) {
autoPlayNext()
} else {
ToastManager.showToast(status.errorMsg, ToastManager.LENGTH_LONG)
}
}
PlayerStatus.STOPPED -> {
totalDuringStr = "00:00"
curPositionStr = "00:00"
this.progress = 0f
}
else -> {}
}
}
private fun autoPlayNext() {
if(playMode == PlayMode.SINGLE) {
resume()
}else {
val newIndex = getNextRealIndex()
playByRealIndex(newIndex)
}
}
fun pause() {
if (playerStatus == PlayerStatus.STARTED) {
mediaPlayer.pause()
}
}
fun resume() {
if (playerStatus == PlayerStatus.PAUSED) {
mediaPlayer.resume()
}
}
fun isPlaying(): Boolean {
return playing
}
/**
* 获取当前播放模式下的上一首歌曲索引
*/
fun getPreRealIndex() = if (curRealIndex == 0) realSongList.size - 1 else curRealIndex - 1
/**
* 获取当前播放模式下的下一首歌曲索引
*/
fun getNextRealIndex() = if (curRealIndex == realSongList.size - 1) 0 else curRealIndex + 1
fun changePlayMode(playMode: PlayMode) {
this.playMode = playMode
generateRealSongList(curOriginIndex)
}
fun seekTo(progress: Float) {
this.progress = progress
if (totalDuring != 0) {
mediaPlayer.seekTo(progress)
}
seeking = false
}
fun seeking(progress: Float) {
seeking = true
this.progress = progress
if (totalDuring != 0) {
this.curPositionStr = StringUtil.formatMilliseconds((progress * totalDuring / 100).toInt())
}
}
override fun onProgress(totalDuring: Int, currentPosition: Int, percentage: Float) {
if (!seeking) {
this.totalDuring = totalDuring
totalDuringStr = StringUtil.formatMilliseconds(totalDuring)
curPositionStr = StringUtil.formatMilliseconds(currentPosition)
progress = percentage
}
}
}
================================================
FILE: src/jvmMain/kotlin/base/UserManager.kt
================================================
package base
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import com.google.gson.Gson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import model.LoginResult
import util.DataStoreUtils
object UserManager {
const val KEY_LOCAL_LOGIN_RESULT = "KEY_LOCAL_LOGIN_RESULT"
fun getLoginResultFlow(): Flow {
return DataStoreUtils.getData(KEY_LOCAL_LOGIN_RESULT, "")
.map {
var loginResult: LoginResult? = null
try {
loginResult = Gson().fromJson(it, LoginResult::class.java)
} catch (e: Exception) {
e.printStackTrace()
}
loginResult
}
}
fun getLoginResult(): LoginResult? {
return try {
val json = DataStoreUtils.readStringData(KEY_LOCAL_LOGIN_RESULT, "")
Gson().fromJson(json, LoginResult::class.java)
} catch (e: Exception) {
null
}
}
suspend fun saveLoginResult(json: String) {
DataStoreUtils.putData(KEY_LOCAL_LOGIN_RESULT, json)
}
}
================================================
FILE: src/jvmMain/kotlin/base/player/IPlayer.kt
================================================
package base.player
import model.SongBean
/**
* Created by ssk on 2022/4/23.
*/
interface IPlayer {
fun setDataSource(songBean: SongBean)
fun start()
fun pause()
fun resume()
fun stop()
fun seekTo(position: Float)
fun envAvailable() = false
fun addListener(listener: IPlayerListener)
fun removeListener(listener: IPlayerListener)
}
================================================
FILE: src/jvmMain/kotlin/base/player/IPlayerListener.kt
================================================
package base.player
/**
* Created by ssk on 2022/4/23.
*/
interface IPlayerListener {
fun onStatusChanged(status: PlayerStatus)
fun onProgress(totalDuring: Int, currentPosition: Int, percentage: Float)
}
================================================
FILE: src/jvmMain/kotlin/base/player/NCPlayer.kt
================================================
package base.player
import http.NCRetrofitClient
import kotlinx.coroutines.*
import model.SongBean
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
import uk.co.caprica.vlcj.player.component.AudioPlayerComponent
/**
* 音乐播放器,基于vlcj
*/
class NCPlayer : IPlayer {
private var mStatus: PlayerStatus = PlayerStatus.IDLE
private var mCurSongBean: SongBean? = null
private val mListeners = ArrayList()
private var mJob: Job? = null
private var mMediaPlayer: MediaPlayer? = null
private var mDuring: Int = 0
private var mCurTime: Int = 0
init {
if (envAvailable()) {
mMediaPlayer = AudioPlayerComponent().mediaPlayer().apply {
this.events().addMediaPlayerEventListener(object : MediaPlayerEventAdapter() {
override fun mediaPlayerReady(mediaPlayer: MediaPlayer) {
super.mediaPlayerReady(mediaPlayer)
println("----${mCurSongBean?.name}----mediaPlayerReady")
innerStartPlay()
}
override fun finished(mediaPlayer: MediaPlayer) {
super.finished(mediaPlayer)
setStatus(PlayerStatus.COMPLETED)
println("----${mCurSongBean?.name}----finished")
}
override fun timeChanged(mediaPlayer: MediaPlayer, newTime: Long) {
super.timeChanged(mediaPlayer, newTime)
mCurTime = newTime.toInt()
updateProgress()
}
override fun opening(mediaPlayer: MediaPlayer?) {
super.opening(mediaPlayer)
println("----${mCurSongBean?.name}----opening")
}
override fun playing(mediaPlayer: MediaPlayer?) {
super.playing(mediaPlayer)
println("----${mCurSongBean?.name}----playing")
}
override fun paused(mediaPlayer: MediaPlayer?) {
super.paused(mediaPlayer)
println("----${mCurSongBean?.name}----paused")
}
override fun stopped(mediaPlayer: MediaPlayer?) {
super.stopped(mediaPlayer)
println("----${mCurSongBean?.name}----stopped")
}
override fun error(mediaPlayer: MediaPlayer?) {
super.error(mediaPlayer)
println("----${mCurSongBean?.name}----error")
setStatus(PlayerStatus.ERROR(PlayerErrorCode.ERROR_PLAY, "播放失败"))
}
})
}
}
}
private fun innerStartPlay() {
if (envAvailable()) {
println("----${mCurSongBean?.name}----innerStartPlay()")
mMediaPlayer?.controls()?.start()
setStatus(PlayerStatus.STARTED)
}
}
override fun setDataSource(songBean: SongBean) {
mCurSongBean = songBean
println("----${mCurSongBean?.name}----setDataSource()")
}
override fun start() {
if (envAvailable()) {
println("----${mCurSongBean?.name}----start()")
if (mStatus == PlayerStatus.STARTED
) {
pause()
}
mCurSongBean?.let {
mDuring = it.dt
mCurTime = 0
updateProgress()
getSongUrlAndPlay(it.id)
}
}
}
private fun getSongUrlAndPlay(songId: Long) {
mJob?.cancel()
mJob = GlobalScope.launch(context = Dispatchers.IO) {
try {
val url = NCRetrofitClient.getNCApi().getSongUrl(songId).data.firstOrNull()?.url
?: "https://music.163.com/song/media/outer/url?id=$songId.mp3"
mMediaPlayer?.media()?.play(url)
} catch (e: Exception) {
if (e !is CancellationException) {
println("getSongUrlAndPlay e = $e")
e.printStackTrace()
mListeners.forEach {
it.onStatusChanged(PlayerStatus.ERROR(PlayerErrorCode.ERROR_GET_URL, "获取歌曲播放链接失败"))
}
}
}
}
}
fun updateProgress() {
if (mDuring != 0) {
mListeners.forEach {
it.onProgress(mDuring, mCurTime, mCurTime.toFloat() * 100 / mDuring)
}
}
}
override fun pause() {
if (envAvailable()) {
if (mStatus == PlayerStatus.STARTED) {
println("----${mCurSongBean?.name}----pause()")
mMediaPlayer?.controls()?.pause()
setStatus(PlayerStatus.PAUSED)
}
}
}
override fun resume() {
println("----${mCurSongBean?.name}----resume()")
innerStartPlay()
}
override fun stop() {
if (envAvailable()) {
println("----${mCurSongBean?.name}----stop()")
mMediaPlayer?.controls()?.stop()
mDuring = 0
setStatus(PlayerStatus.STOPPED)
setStatus(PlayerStatus.IDLE)
}
}
override fun seekTo(position: Float) {
if (envAvailable()) {
println("----${mCurSongBean?.name}----seekTo->${position}")
mMediaPlayer?.controls()?.setPosition(position / 100)
}
}
private fun setStatus(status: PlayerStatus) {
mStatus = status
mListeners.forEach {
it.onStatusChanged(mStatus)
}
}
override fun envAvailable(): Boolean {
if (NativeDiscovery().discover()) {
return true
} else {
setStatus(PlayerStatus.ERROR(PlayerErrorCode.ERROR_ENV_INVALID, "NCMusicDesktop播放音乐需依赖VLC组件,当前设备未检测到VLC组件,请前往 https://www.videolan.org/ 下载并安装。"))
return false
}
}
override fun addListener(listener: IPlayerListener) {
mListeners.add(listener)
}
override fun removeListener(listener: IPlayerListener) {
mListeners.remove(listener)
}
}
================================================
FILE: src/jvmMain/kotlin/base/player/PlayMode.kt
================================================
package base.player
/**
* Created by ssk on 2022/4/23.
*/
enum class PlayMode {
// 单曲循环
SINGLE,
// 随机
RANDOM,
// 列表循环
LOOP,
}
================================================
FILE: src/jvmMain/kotlin/base/player/PlayerStatus.kt
================================================
package base.player
/**
* Created by ssk on 2022/4/23.
*/
sealed class PlayerStatus() {
object IDLE: PlayerStatus()
object PREPARED: PlayerStatus()
object STARTED: PlayerStatus()
object PAUSED: PlayerStatus()
object STOPPED: PlayerStatus()
object COMPLETED: PlayerStatus()
class ERROR(val errorCode: Int, val errorMsg: String): PlayerStatus()
}
object PlayerErrorCode {
// 环境错误,没有安装VLC组件
const val ERROR_ENV_INVALID = 1
// 获取歌曲播放URL错误
const val ERROR_GET_URL = 1
// 播放错误
const val ERROR_PLAY = 2
}
================================================
FILE: src/jvmMain/kotlin/http/RetrofitClient.kt
================================================
package http
import http.api.NCApi
import http.interceptor.CookieIntercept
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NCRetrofitClient {
private var ncApi: NCApi? = null
fun getNCApi(): NCApi {
if (ncApi == null) {
ncApi = RetrofitClient.getApi(NCApi::class.java)
}
return ncApi!!
}
}
object RetrofitClient {
const val BASE_URL = "https://ncmusic.sskevan.cn"
private const val CONNECT_TIMEOUT = 30L
private const val READ_TIMEOUT = 10L
fun getApi(retrofit: Class): T = createRetrofit().create(retrofit)
private fun createRetrofit(url: String = BASE_URL): Retrofit {
// okHttpClientBuilder
val okHttpClientBuilder = OkHttpClient().newBuilder().apply {
connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
addInterceptor(CookieIntercept())
}
return RetrofitBuild(
url = url,
client = okHttpClientBuilder.build(),
gsonFactory = GsonConverterFactory.create()
).retrofit
}
}
class RetrofitBuild(
url: String, client: OkHttpClient,
gsonFactory: GsonConverterFactory
) {
val retrofit: Retrofit = Retrofit.Builder().apply {
baseUrl(url)
client(client)
addConverterFactory(gsonFactory)
}.build()
}
================================================
FILE: src/jvmMain/kotlin/http/api/NCApi.kt
================================================
package http.api
import model.*
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import java.util.*
interface NCApi {
/**
* 获取推荐歌单
*/
@GET("personalized")
suspend fun getRecommendPlayList(@Query("limit") limit: Int = 20): RecommendPlayListResult
/**
* 获取独家放送
*/
@GET("personalized/privatecontent")
suspend fun getPrivateContent(): PrivateContentResult
/**
* 新歌速递
*/
@GET("/top/song")
suspend fun getNewSong(): NewSongResult
/**
* 新歌速递
*/
@GET("/personalized/mv")
suspend fun getRecommendMV(): RecommendMVResult
/**
* 精品歌单
*/
@GET("/top/playlist/highquality")
suspend fun getHighQualityPlayList(@Query("limit") limit: Int = 20, @Query("cat") cat: String?): PlayListResult
/**
* 热门歌单分类
*/
@GET("/playlist/hot")
suspend fun getHotPlayListCategories(): HotPlayListTabResult
/**
* 歌单分类
*/
@GET("/playlist/catlist")
suspend fun getPlayListCategories(): PlayListTabResult
/**
* 歌单列表
*/
@GET("/top/playlist")
suspend fun getPlayList(
@Query("limit") limit: Int = 20,
@Query("tag") tag: String,
@Query("offset") offset: Int
): PlayListResult
/**
* 获取歌单详情
*/
@GET("playlist/detail")
suspend fun getPlaylistDetail(@Query("id") id: Long): PlaylistDetailResult
/**
* 获取歌曲详情
*/
@GET("song/detail")
suspend fun getSongDetail(@Query("ids") ids: String): SongDetailResult
/**
* 获取评论列表
*/
@GET("comment/{commentType}")
suspend fun getCommentList(
@Path("commentType") commentType: String,
@Query("id") id: Long,
@Query("limit") limit: Int = 20,
@Query("offset") offset: Int,
// @Query("before") before: Long, // 分页参数,取上一页最后一项的 time 获取下一页数据(获取超过 5000 条评论的时候需要用到)
): CommentResult
/**
* 获取二维码登录key
*/
@GET("/login/qr/key")
suspend fun getLoginQrcodeKey(@Query("timeStamp") timeStamp: Long = Date().time): QrcodeKeyResult
/**
* 获取二维码登录链接
*/
@GET("/login/qr/create")
suspend fun getLoginQrcodeValue(
@Query("key") key: String,
@Query("timeStamp") timeStamp: Long = Date().time
): QrcodeValueResult
/**
* 验证二维码登录授权结果
*/
@GET("/login/qr/check")
suspend fun checkQrcodeAuthStatus(
@Query("key") key: String,
@Query("timeStamp") timeStamp: Long = Date().time
): QrcodeAuthResult
/**
* 获取用户信息
*/
@GET("/user/account")
suspend fun getAccountInfo(
@Query("cookie") cookie: String,
): AccountInfoResult
/**
* 获取用户歌单
*/
@GET("user/playlist")
suspend fun getUserPlayList(@Query("uid") uid: String): UserPlaylistResult
/**
* 获取歌曲url
*/
@GET("/song/url")
suspend fun getSongUrl(
@Query("id") id: Long,
@Query("br") br: Int = 128000
): SongUrlBean
/**
* 获取歌词
*/
@GET("/lyric")
suspend fun getLyric(@Query("id") id: Long): LyricResult
}
================================================
FILE: src/jvmMain/kotlin/http/interceptor/CookieInterceptor.kt
================================================
package http.interceptor
import androidx.compose.runtime.collectAsState
import base.UserManager
import okhttp3.Interceptor
import okhttp3.Response
/**
* Created by ssk on 2022/4/24.
*/
class CookieIntercept : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val mLoginResult = UserManager.getLoginResult()
if (mLoginResult != null) {
val request = chain.request()
val url = if(request.url().toString().contains("?")) {
request.url().toString() + "&cookie=" + mLoginResult.cookie
}else {
request.url().toString() + "?cookie=" + mLoginResult.cookie
}
val builder = request.newBuilder()
builder.get().url(url)
val newRequest = builder.build()
return chain.proceed(newRequest);
}
return chain.proceed(chain.request())
}
}
================================================
FILE: src/jvmMain/kotlin/model/BasePagingBean.kt
================================================
package model
interface IBasePagingBean {
fun getTotalCount(): Int
}
================================================
FILE: src/jvmMain/kotlin/model/BaseResult.kt
================================================
package model
import androidx.annotation.Keep
import java.io.Serializable
/**
* Created by ssk on 2022/4/17.
*/
@Keep
open class BaseResult(val code: Int? = 0, val message: String? = null) : Serializable {
open fun resultOk(): Boolean {
return code == 200
}
open fun isEmpty() = false
}
================================================
FILE: src/jvmMain/kotlin/model/CommentResult.kt
================================================
package model
import androidx.annotation.Keep
@Keep
data class CommentResult(
val isMusician: Boolean = false,
val userId: Long = 0,
val total: Int = 0,
val more: Boolean = false,
val comments: List = emptyList(),
val topComments: List = emptyList(),
val hotComments: List = emptyList(),
) : BaseResult() {
override fun isEmpty() = comments.isEmpty()
}
@Keep
data class CommentBean(
val user: CommentUser,
val content: String = "",
val time: Long = 0,
var likedCount: Int = 0,
val showFloorComment: FloorComment? = null,
val tag: Tag? = null,
val commentId: Long = 0L,
val beReplied: List? = null,
var liked: Boolean = false,
) {
@Keep
data class Tag(
val datas: List? = null,
)
@Keep
data class TagData(
val text: String = "",
)
}
@Keep
data class FloorComment(
val replyCount: Long = 0,
val showReplyCount: Boolean = false,
)
@Keep
data class CommentUser(
val nickname: String = "",
val userId: Long = 0,
val avatarUrl: String? = null,
)
@Keep
data class BeReplied(
val user: CommentUser,
val content: String? = null,
val status: Int = 0,
val beRepliedCommentId: Long = 0,
)
@Keep
data class NewCommentResult(
val data : CommentData
) : BaseResult()
@Keep
data class CommentData(
val totalCount: Int = 0,
val hasMore: Boolean = false,
var cursor: String,
val comments: List = emptyList(),
)
@Keep
data class FloorCommentResult(
val data : FloorCommentData
) : BaseResult()
@Keep
data class FloorCommentData(
val totalCount: Int = 0,
val ownerComment: CommentBean,
val comments: List = emptyList(),
)
================================================
FILE: src/jvmMain/kotlin/model/LoginResult.kt
================================================
package model
import androidx.annotation.Keep
@Keep
data class QrcodeKeyResult(val data: QrcodeKeyBean): BaseResult()
@Keep
data class QrcodeValueResult(val data: QrcodeValueBean): BaseResult()
@Keep
data class QrcodeKeyBean(val unikey: String)
@Keep
data class QrcodeValueBean(val qrurl: String, val qrimg: String?)
@Keep
data class QrcodeAuthResult(val cookie: String): BaseResult() {
override fun resultOk(): Boolean {
return code == 803
}
}
@Keep
data class AccountInfoResult(val account: AccountBean, val profile: ProfileBean): BaseResult()
@Keep
data class LoginResult(
val account: AccountBean,
val profile: ProfileBean,
val cookie: String
)
@Keep
data class AccountBean(
val id: Long,
val userName: String,
val type: Int,
val status: Int,
val whitelistAuthority: Int,
val createTime: Long,
val tokenVersion: Int,
val ban: Int,
val baoyueVersion: Int,
val donateVersion: Int,
val vipType: Int,
val viptypeVersion: Double,
val anonimousUser: Boolean
)
@Keep
data class ProfileBean(
val followed: Boolean,
val userId: Int,
val defaultAvatar: Boolean,
val avatarUrl: String?,
val nickname: String,
val birthday: Long,
val province: Int,
val accountStatus: Int,
val vipType: Int,
val gender: Int,
val djStatus: Int,
val mutual: Boolean,
val authStatus: Int,
val backgroundImgId: Long,
val userType: Int,
val city: Int,
val backgroundUrl: String?,
val followeds: Int,
val follows: Int,
val eventCount: Int,
val playlistCount: Int,
val playlistBeSubscribedCount: Int
)
================================================
FILE: src/jvmMain/kotlin/model/LyricResult.kt
================================================
package model
import androidx.annotation.Keep
import model.BaseResult
/**
* Created by ssk on 2022/5/11.
*/
@Keep
class LyricResult(
val transUser: LyricContributorBean?,
val lyricUser: LyricContributorBean?,
val lrc: LrcBean?,
val tlyric: LrcBean?
) : BaseResult() {
override fun isEmpty() = transUser == null && lyricUser == null && lrc == null && tlyric == null
}
@Keep
data class LyricContributorBean(
val id: Long,
val status: Int,
val demand: Int,
val userid: Long,
val nickname: String,
val uptime: Long
)
/**
* version : 11
* lyric : [00:00.42]遠く離れてるほどに 近くに感じてる
* [00:07.87]寂しさも強さへと 変換(かわ)ってく
* [00:13.74]…君を想ったなら
* [00:34.98]街も 人も 夢も 変えていく時間に
* [00:41.54]ただ 逆らっていた
* [00:48.35]言葉を重ねても 理解(わか)り合えないこと
* [00:56.18]まだ 知らなかったね
* [01:01.76]
* [01:03.20]君だけを抱きしめたくて失くした夢 君は
* [01:10.55]「諦メナイデ」と云った
* [01:16.48]
* [01:17.63]遠く離れてるほどに 近くに感じてる
* [01:24.38]寂しさも強さへと 変換(かわ)ってく
* [01:29.66]…君を想ったなら
* [01:32.75]切なく胸を刺す それは夢の欠片(かけら)
* [01:39.06]ありのまま出逢えてた その奇跡
* [01:44.42]もう一度信じて
* [01:47.85]
* [01:54.90]君がいない日々に ずっと 立ち止まった
* [02:02.73]でも 歩き出してる
* [02:09.58]君と分かち合った どの偶然にも意味が
* [02:17.50]そう 必ずあった
* [02:23.18]
* [02:24.53]それぞれの夢を叶えて まためぐり逢う時
* [02:31.85]偶然は運命になる
* [02:37.46]
* [02:38.91]破れた約束さえも 誓いに変えたなら
* [02:45.55]あの場所で 出逢う時 あの頃の
* [02:50.97]二人に戻(なれ)るかな?
* [02:54.05]"優しさ" に似ている 懐かしい面影
* [03:00.50]瞳(め)を閉じて見えるから
* [03:04.23]手を触れず在(あ)ることを知るから
* [03:09.01]
* [03:34.32]明日(あす)に はぐれて 答えが
* [03:37.79]何も見えなくても
* [03:41.10]君に逢う そのために重ねてく
* [03:46.43]"今日" という真実
* [03:50.27]
* [03:50.95]遠く離れてるほどに 近くに感じてる
* [03:57.77]寂しさも強さへと 変換(かわ)ってく
* [04:03.03]…君を想ったなら
* [04:06.02]切なく胸を刺す それは夢の欠片(かけら)
* [04:12.53]ありのまま出逢えてた その奇跡
* [04:17.75]もう一度信じて
*/
@Keep
data class LrcBean(
val version: Int,
val lyric: String
)
================================================
FILE: src/jvmMain/kotlin/model/NewSongResult.kt
================================================
package model
/**
* 新歌速递
*/
data class NewSongResult(
var data: List
) : BaseResult() {
override fun isEmpty() = data.isEmpty()
}
data class NewSongBean(
val name: String,
val artists: List,
val album: Album
)
data class ArtistBean(
val id: Long,
val name: String
)
data class Album(
val id: Long,
val name: String,
val picUrl: String
)
================================================
FILE: src/jvmMain/kotlin/model/PlayListResult.kt
================================================
package model
import androidx.annotation.Keep
import java.io.Serializable
/**
* 推荐歌单结果
*/
data class RecommendPlayListResult(
val result: List
) : BaseResult() {
override fun isEmpty() = result.isEmpty()
}
/**
* 推荐歌单列表item
*/
data class SimplePlayListItem(
val id: Long,
val name: String,
val picUrl: String,
val copywriter: String?,
val trackNumberUpdateTime: Long,
val playCount: Long,
val trackCount: Int,
val subscribedCount: Int,
val shareCount: Int
) : Serializable
@Keep
data class PlaylistDetail(
val tracks: List