[
  {
    "path": ".gitignore",
    "content": ".gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### IntelliJ IDEA ###\n.idea/modules.xml\n.idea/jarRepositories.xml\n.idea/compiler.xml\n.idea/libraries/\n*.iws\n*.iml\n*.ipr\nout/\n!**/src/main/**/out/\n!**/src/test/**/out/\n\n### Eclipse ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\nbin/\n!**/src/main/**/bin/\n!**/src/test/**/bin/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n\n### Mac OS ###\n.DS_Store"
  },
  {
    "path": ".idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n"
  },
  {
    "path": ".idea/artifacts/NCMusicDesktop_jvm_1_0_SNAPSHOT.xml",
    "content": "<component name=\"ArtifactManager\">\n  <artifact type=\"jar\" name=\"NCMusicDesktop-jvm-1.0-SNAPSHOT\">\n    <output-path>$PROJECT_DIR$/build/libs</output-path>\n    <root id=\"archive\" name=\"NCMusicDesktop-jvm-1.0-SNAPSHOT.jar\">\n      <element id=\"module-output\" name=\"NCMusicDesktop.jvmMain\" />\n    </root>\n  </artifact>\n</component>"
  },
  {
    "path": ".idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersion=\"1\" />\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <option name=\"delegatedBuild\" value=\"true\" />\n        <option name=\"testRunner\" value=\"GRADLE\" />\n        <option name=\"distributionType\" value=\"DEFAULT_WRAPPED\" />\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"gradleJvm\" value=\"11\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n          </set>\n        </option>\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/kotlinc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Kotlin2JvmCompilerArguments\">\n    <option name=\"jvmTarget\" value=\"18\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ExternalStorageConfigurationManager\" enabled=\"true\" />\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_11\" project-jdk-name=\"corretto-17\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/out\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/uiDesigner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Palette2\">\n    <group name=\"Swing\">\n      <item class=\"com.intellij.uiDesigner.HSpacer\" tooltip-text=\"Horizontal Spacer\" icon=\"/com/intellij/uiDesigner/icons/hspacer.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"1\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" />\n      </item>\n      <item class=\"com.intellij.uiDesigner.VSpacer\" tooltip-text=\"Vertical Spacer\" icon=\"/com/intellij/uiDesigner/icons/vspacer.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"1\" anchor=\"0\" fill=\"2\" />\n      </item>\n      <item class=\"javax.swing.JPanel\" icon=\"/com/intellij/uiDesigner/icons/panel.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\" />\n      </item>\n      <item class=\"javax.swing.JScrollPane\" icon=\"/com/intellij/uiDesigner/icons/scrollPane.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"7\" hsize-policy=\"7\" anchor=\"0\" fill=\"3\" />\n      </item>\n      <item class=\"javax.swing.JButton\" icon=\"/com/intellij/uiDesigner/icons/button.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"0\" fill=\"1\" />\n        <initial-values>\n          <property name=\"text\" value=\"Button\" />\n        </initial-values>\n      </item>\n      <item class=\"javax.swing.JRadioButton\" icon=\"/com/intellij/uiDesigner/icons/radioButton.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" />\n        <initial-values>\n          <property name=\"text\" value=\"RadioButton\" />\n        </initial-values>\n      </item>\n      <item class=\"javax.swing.JCheckBox\" icon=\"/com/intellij/uiDesigner/icons/checkBox.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"3\" anchor=\"8\" fill=\"0\" />\n        <initial-values>\n          <property name=\"text\" value=\"CheckBox\" />\n        </initial-values>\n      </item>\n      <item class=\"javax.swing.JLabel\" icon=\"/com/intellij/uiDesigner/icons/label.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"8\" fill=\"0\" />\n        <initial-values>\n          <property name=\"text\" value=\"Label\" />\n        </initial-values>\n      </item>\n      <item class=\"javax.swing.JTextField\" icon=\"/com/intellij/uiDesigner/icons/textField.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\">\n          <preferred-size width=\"150\" height=\"-1\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JPasswordField\" icon=\"/com/intellij/uiDesigner/icons/passwordField.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\">\n          <preferred-size width=\"150\" height=\"-1\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JFormattedTextField\" icon=\"/com/intellij/uiDesigner/icons/formattedTextField.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\">\n          <preferred-size width=\"150\" height=\"-1\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JTextArea\" icon=\"/com/intellij/uiDesigner/icons/textArea.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JTextPane\" icon=\"/com/intellij/uiDesigner/icons/textPane.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JEditorPane\" icon=\"/com/intellij/uiDesigner/icons/editorPane.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JComboBox\" icon=\"/com/intellij/uiDesigner/icons/comboBox.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"2\" anchor=\"8\" fill=\"1\" />\n      </item>\n      <item class=\"javax.swing.JTable\" icon=\"/com/intellij/uiDesigner/icons/table.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JList\" icon=\"/com/intellij/uiDesigner/icons/list.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"2\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JTree\" icon=\"/com/intellij/uiDesigner/icons/tree.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"150\" height=\"50\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JTabbedPane\" icon=\"/com/intellij/uiDesigner/icons/tabbedPane.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"200\" height=\"200\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JSplitPane\" icon=\"/com/intellij/uiDesigner/icons/splitPane.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"3\" hsize-policy=\"3\" anchor=\"0\" fill=\"3\">\n          <preferred-size width=\"200\" height=\"200\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JSpinner\" icon=\"/com/intellij/uiDesigner/icons/spinner.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"true\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" />\n      </item>\n      <item class=\"javax.swing.JSlider\" icon=\"/com/intellij/uiDesigner/icons/slider.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"8\" fill=\"1\" />\n      </item>\n      <item class=\"javax.swing.JSeparator\" icon=\"/com/intellij/uiDesigner/icons/separator.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"6\" anchor=\"0\" fill=\"3\" />\n      </item>\n      <item class=\"javax.swing.JProgressBar\" icon=\"/com/intellij/uiDesigner/icons/progressbar.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\" />\n      </item>\n      <item class=\"javax.swing.JToolBar\" icon=\"/com/intellij/uiDesigner/icons/toolbar.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"6\" anchor=\"0\" fill=\"1\">\n          <preferred-size width=\"-1\" height=\"20\" />\n        </default-constraints>\n      </item>\n      <item class=\"javax.swing.JToolBar$Separator\" icon=\"/com/intellij/uiDesigner/icons/toolbarSeparator.svg\" removable=\"false\" auto-create-binding=\"false\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"0\" hsize-policy=\"0\" anchor=\"0\" fill=\"1\" />\n      </item>\n      <item class=\"javax.swing.JScrollBar\" icon=\"/com/intellij/uiDesigner/icons/scrollbar.svg\" removable=\"false\" auto-create-binding=\"true\" can-attach-label=\"false\">\n        <default-constraints vsize-policy=\"6\" hsize-policy=\"0\" anchor=\"0\" fill=\"2\" />\n      </item>\n    </group>\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "README.md",
    "content": "# NCMusicDesktop\n\n小明非常喜欢网易云，去年刚用Jetpack Compose写了个仿网易云app [NCMusic](https://github.com/sskEvan/NCMusic) ，最近发现compose-jb正式版已经发布到了v1.3.1，\n又玩了一下Compose Desktop，决定搞了个桌面版的NCMusicDesktop，数据源还是来自[Binaryify](https://github.com/Binaryify)大佬的[NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)～\n\n由于以前没有开发桌面应用的经验，索性想按照Android jetpack的套路来开发，然而Navigation、Lifecycle 、ViewModel、LiveData等等这些在compose-jb中，暂时通通没有～ \n不要慌，一番查找在掘金上看到一篇文章[《推销 Compose 跨平台 Navigation：PreCompose》](https://juejin.cn/post/7122056172084920334)，\n讲了[Precompose](https://github.com/Tlaster/PreCompose)这个跨平台Navigation框架的使用， 它基本复刻了Jetpack Navigation、Lifecycle、ViewModel这些组件，\n使用方式也基本保持一致，美滋滋！当然LiveData已经被废弃了，推荐使用Flow代替～至于网络请求，Retrofit照用不误，又一次美滋滋～\n\n### 怎么用Android老套路来写Desktop应用\n- 老规矩，先定义一波BaseResult、BaseViewModel、ViewStateComponent(页面状态切换组件)\n```\n代码： 略\n```\n- Model层\n```\nclass LyricResult(\n    val transUser: LyricContributorBean?,\n    val lyricUser: LyricContributorBean?,\n    val lrc: LrcBean?,\n    val tlyric: LrcBean?\n) : BaseResult()\n```\n- ViewModel层\n```\nclass CpnLyricViewModel : BaseViewModel() {\n     fun getLyric(id: Long) = launchFlow {\n        NCRetrofitClient.getNCApi().getLyric(id)\n    }\n}\n\ninterface NCApi {\n    @GET(\"/lyric\")\n    suspend fun getLyric(@Query(\"id\") id: Long): LyricResult\n}\n```\n- View层\n```\n@Composable\nfun CpnLyric() {\n    ViewStateComponent(\n        key = \"CpnLyric-${id}\",\n        loadDataBlock = {viewModel.getLyric(id)}\n    ) {\n        LyricList(it)\n    }\n}\n```\n### 怎么播放音乐\n至于在Compose Desktop上怎么播放音乐呢，毕竟没有Android的MediaPlayer，在github上找了找，发现[succlz123](https://github.com/succlz123)大佬开源的Compose Multiplatform项目\n[AcFun-Client-Multiplatform](https://github.com/succlz123/AcFun-Client-Multiplatform)，里面有视频播放的功能，是基于[vlcj](https://github.com/caprica/vlcj)来实现的，看了下vlcj的api，使用AudioPlayerComponent播放音乐不是问题\n\n### 关于嵌套滑动\n开发过程中，有些交互感觉需要涉及到嵌套滑动，在Jetpack Compose中，使用NestedScrollConnection来处理嵌套滑动到场景，于是乎，写了一堆✨✨代码后，\n发现NestedScrollConnection在Compose Desktop中完全不起作用，后面找了下github的issue，发现有哥们也遇到了哈哈哈，然而官方21年的回复是暂时没有计划，\n到现在还是没有解决，凉飕飕～    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/nested_issue1.webp)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/nested_issue2.webp)   \n\n### 第三方框架\n- [PreCompose](https://github.com/Tlaster/PreCompose)\n- [zxing](https://github.com/zxing/zxing)\n- [compose-imageloader-desktop](https://github.com/succlz123/compose-desktop-imageloader)\n- [vlcj](https://github.com/caprica/vlcj)\n\n### 运行效果图\n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/登录.gif)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/首页.gif)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/歌单列表.gif)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/歌单详情.gif)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/音乐播放.gif)    \n![img](https://github.com/sskEvan/NCMusicDesktop/blob/master/readme/主题切换.gif)    "
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.jetbrains.compose.compose\nimport org.jetbrains.compose.desktop.application.dsl.TargetFormat\n\nplugins {\n    kotlin(\"multiplatform\")\n    id(\"org.jetbrains.compose\")\n}\n\ngroup = \"com.ssk.NCMusicDesktop\"\nversion = \"1.0-SNAPSHOT\"\n\nrepositories {\n    google()\n    mavenCentral()\n    maven(\"https://maven.pkg.jetbrains.space/public/p/compose/dev\")\n    maven(\"https://jitpack.io\")\n\n}\n\ntasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {\n    kotlinOptions.jvmTarget = \"11\"\n}\n\nkotlin {\n    jvm {\n        jvmToolchain(11)\n        withJava()\n    }\n    sourceSets {\n        val jvmMain by getting {\n            dependencies {\n                implementation(compose.desktop.currentOs)\n                implementation(\"com.google.zxing:javase:3.3.3\")\n                implementation(\"moe.tlaster:precompose:1.3.14\")\n                implementation(\"androidx.datastore:datastore-preferences-core:1.1.0-dev01\")\n\n                implementation(\"com.squareup.retrofit2:retrofit:2.9.0\")\n                implementation(\"com.squareup.retrofit2:converter-gson:2.9.0\")\n\n                implementation(\"io.github.succlz123:compose-imageloader-desktop:0.0.2\")\n                implementation(\"uk.co.caprica:vlcj:4.7.3\")\n            }\n        }\n        val jvmTest by getting\n    }\n}\n\ncompose.desktop {\n    application {\n        javaHome = \"/Users/anmin83/Library/Java/JavaVirtualMachines/corretto-17.0.6/Contents/Home\"\n        mainClass = \"MainKt\"\n        nativeDistributions {\n            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Exe)\n            packageName = \"NCMusicDesktop\"\n            packageVersion = \"1.0.0\"\n//            includeAllModules = true\n            modules(\"java.instrument\", \"java.sql\", \"jdk.unsupported\")\n\n            macOS {\n                // a version for all macOS distributables\n                packageVersion = \"1.0.0\"\n                // a version only for the dmg package\n                dmgPackageVersion = \"1.0.0\"\n                // a version only for the pkg package\n                pkgPackageVersion = \"1.0.0\"\n                // 显示在菜单栏、“关于”菜单项、停靠栏等中的应用程序名称\n                dockName = \"NCMusicDesktop\"\n                // a build version for all macOS distributables\n                packageBuildVersion = \"1.0.0\"\n                // a build version only for the dmg package\n                dmgPackageBuildVersion = \"1.0.0\"\n                // a build version only for the pkg package\n                pkgPackageBuildVersion = \"1.0.0\"\n                // 设置图标\n                iconFile.set(project.file(\"launcher/icon.icns\"))\n            }\n\n            windows {\n                // a version for all Windows distributables\n                packageVersion = \"1.0.0\"\n                // a version only for the msi package\n                msiPackageVersion = \"1.0.0\"\n                // a version only for the exe package\n                exePackageVersion = \"1.0.0\"\n                // 设置图标\n                iconFile.set(project.file(\"launcher/icon.ico\"))\n            }\n        }\n\n        buildTypes.release.proguard {\n            obfuscate.set(false)\n            configurationFiles.from(project.file(\"proguard-rules.pro\"))\n        }\n    }\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "kotlin.code.style=official\nkotlin.version=1.7.20\nagp.version=7.3.0\ncompose.version=1.3.1"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "proguard-rules.pro",
    "content": "# DataStore 混淆\n-dontwarn androidx.datastore.**\n\n# Retrofit2  混淆\n-dontwarn javax.annotation.**\n-dontwarn javax.inject.**\n# OkHttp3\n-dontwarn okhttp3.logging.**\n-keep class okhttp3.internal.**{*;}\n-dontwarn okio.**\n-dontwarn okhttp3.internal.**\n# Retrofit\n-dontwarn retrofit2.**\n-keep class retrofit2.** { *; }\n-keepattributes Signature\n-keepattributes Exceptions\n\n# Gson\n-keep class com.google.gson.stream.** { *; }\n-keepattributes EnclosingMethod"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google()\n        gradlePluginPortal()\n        mavenCentral()\n        maven(\"https://maven.pkg.jetbrains.space/public/p/compose/dev\")\n    }\n\n    plugins {\n        kotlin(\"multiplatform\").version(extra[\"kotlin.version\"] as String)\n        id(\"org.jetbrains.compose\").version(extra[\"compose.version\"] as String)\n    }\n}\n\nrootProject.name = \"NCMusicDesktop\"\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/Main.kt",
    "content": "import androidx.compose.desktop.ui.tooling.preview.Preview\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.window.application\nimport androidx.compose.ui.window.rememberWindowState\nimport base.AppConfig\nimport moe.tlaster.precompose.PreComposeWindow\nimport moe.tlaster.precompose.navigation.rememberNavigator\nimport org.succlz123.lib.imageloader.core.ImageLoader\nimport router.NCNavigatorManager\nimport ui.common.theme.AppTheme\nimport ui.common.theme.themeTypeState\nimport ui.main.MainPage\nimport ui.main.cpn.CpnWindowsPlaformDecoratedButtons\nimport util.EnvUtil\nimport java.awt.Dimension\nimport java.io.File\n\nfun main() = application {\n    initImageLoader()\n    val windowState = rememberWindowState(size = DpSize(AppConfig.windowMinWidth, AppConfig.windowMinHeight))\n    PreComposeWindow(\n        state = windowState,\n        onCloseRequest = ::exitApplication,\n        undecorated = EnvUtil.isWindows(),\n        title = \"\"\n    ) {\n        window.minimumSize = Dimension(AppConfig.windowMinWidth.value.toInt(), AppConfig.windowMinHeight.value.toInt())\n        window.rootPane.apply {\n            rootPane.putClientProperty(\"apple.awt.fullWindowContent\", true)\n            rootPane.putClientProperty(\"apple.awt.transparentTitleBar\", true)\n            rootPane.putClientProperty(\"apple.awt.windowTitleVisible\", false)\n        }\n        App()\n        CpnWindowsPlaformDecoratedButtons(windowState)\n    }\n}\n\n@Composable\n@Preview\nprivate fun App() {\n    AppTheme(themeTypeState.value) {\n        NCNavigatorManager.navigator = rememberNavigator()\n        MainPage()\n    }\n}\n\nprivate fun initImageLoader() {\n    ImageLoader.configuration(rootDirectory = File(AppConfig.cacheRootDir))\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/base/AppConfig.kt",
    "content": "package base\n\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.WindowState\nimport java.io.File\n\nobject AppConfig {\n\n    val topBarHeight = 50.dp\n    val windowMinWidth = 1000.dp\n    val windowMinHeight = 680.dp\n    var fullScreen = false\n\n    // 应用缓存目录\n    val cacheRootDir = System.getProperty(\"user.home\") + File.separator + \"Library\" + File.separator + \"NCMusicDesktop\"\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/BaseViewModel.kt",
    "content": "package base\n\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport model.BaseResult\nimport moe.tlaster.precompose.viewmodel.ViewModel\nimport moe.tlaster.precompose.viewmodel.viewModelScope\n\n\ntypealias ViewStateMutableStateFlow<T> = MutableStateFlow<ViewState<T>>\n\nopen class BaseViewModel : ViewModel() {\n    protected fun <T : BaseResult> launchFlow(\n        handleSuccessBlock: ((T) -> Unit)? = null,\n        handleFailBlock: ((code: Int?, message: String?) -> Unit)? = null,\n        judgeEmpty: ((T) -> Boolean)? = null,\n        call: suspend () -> T\n    ): ViewStateMutableStateFlow<T> {\n        val flow = MutableStateFlow<ViewState<T>>(ViewState.Loading)\n\n        viewModelScope.launch {\n            runCatching {\n                call()\n            }.onSuccess { result ->\n                if (result.resultOk()) {\n                    if (result.isEmpty() || judgeEmpty?.invoke(result) == true) {\n                        flow.emit(ViewState.Empty)\n                    } else {\n                        handleSuccessBlock?.invoke(result)\n                        flow.emit(ViewState.Success(result))\n                    }\n                } else {\n                    handleFailBlock?.invoke(result.code ?: -1, result.message ?: \"请求出错\")\n                    flow.emit(ViewState.Fail(result.code?.toString() ?: \"-1\", result.message ?: \"请求出错\"))\n                }\n            }.onFailure { e ->\n                flow.emit(ViewState.Error(e))\n            }\n        }\n\n        return flow\n    }\n\n    protected fun <T : BaseResult> launch(\n        handleSuccessBlock: ((T) -> Unit)? = null,\n        handleFailBlock: ((code: Int?, message: String?) -> Unit)? = null,\n        call: suspend () -> T\n    ) : Job {\n        return viewModelScope.launch {\n            runCatching {\n                call()\n            }.onSuccess { result ->\n                if (result.resultOk()) {\n                    handleSuccessBlock?.invoke(result)\n                } else {\n                    handleFailBlock?.invoke(result.code ?: -1, result.message ?: \"请求出错\")\n                }\n            }.onFailure { e ->\n                handleFailBlock?.invoke(-1000, \"请求出错\")\n               e.printStackTrace()\n            }\n        }\n    }\n\n}\n\nsealed class ViewState<out T> {\n    object Loading : ViewState<Nothing>()\n    data class Success<T>(val data: T) : ViewState<T>()\n    object Empty : ViewState<Nothing>()\n    data class Fail(val errorCode: String, val errorMsg: String) : ViewState<Nothing>()\n    data class Error(val exception: Throwable) : ViewState<Nothing>()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/MusicPlayController.kt",
    "content": "package base\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport base.player.*\nimport model.SongBean\nimport ui.common.toast.ToastManager\nimport util.StringUtil\nimport java.util.*\n\nobject MusicPlayController : IPlayerListener {\n    // 是否显示音乐播放抽屉组件\n    var showMusicPlayDrawer by mutableStateOf(false)\n    // 是否显示播当前播放列表\n    var showCurPlayListSheet by mutableStateOf(false)\n\n    // 原始歌曲列表\n    var originSongList = mutableStateListOf<SongBean>()\n    // 当前播放模式下的实际歌曲列表\n    var realSongList = mutableStateListOf<SongBean>()\n\n    var curSongBean by mutableStateOf<SongBean?>(null)\n    // 当前播放的歌曲在原始歌曲列表中的索引\n    var curOriginIndex by mutableStateOf(-1)\n        private set\n    // 当前播放的歌曲在当前播放模式下的实际歌曲列表中的索引\n    var curRealIndex by mutableStateOf(-1)\n        private set\n    // 当前播放进度\n    var progress by mutableStateOf(0f)\n    // 当前歌曲播放位置时间文本\n    var curPositionStr by mutableStateOf(\"00:00\")\n    // 当前歌曲总时长文本\n    var totalDuringStr by mutableStateOf(\"00:00\")\n    // 是否播放中\n    private var playing by mutableStateOf(false)\n    // 是否允许拖动进度条\n    var enableSeeking by mutableStateOf(false)\n        private set\n    // 播放模式\n    var playMode by mutableStateOf(PlayMode.LOOP)\n        private set\n    // 当前播放歌曲总时长\n    private var totalDuring = 0\n    // 是否拖动进度条中\n    private var seeking = false\n    // 当前播放状态\n    private var playerStatus: PlayerStatus = PlayerStatus.IDLE\n\n    val mediaPlayer: IPlayer by lazy { NCPlayer().apply {\n        addListener(this@MusicPlayController)\n    } }\n\n\n    /**\n     * 播放音乐列表\n     */\n    fun playMusicList(songBeans: List<SongBean>, originIndex: Int) {\n        originSongList.clear()\n        originSongList.addAll(songBeans)\n        println(\"MusicPlayController playMusicList curOriginIndex=${originIndex}\")\n        generateRealSongList(originIndex)\n        innerPlay(originSongList[originIndex])\n    }\n\n\n    /**\n     * 生成当前播放模式下的歌曲列表\n     */\n    private fun generateRealSongList(originIndex: Int) {\n        when (playMode) {\n            PlayMode.RANDOM -> {\n                val randomList = mutableListOf<SongBean>()\n                randomList.addAll(originSongList)\n                randomList.shuffle()\n                realSongList.clear()\n                realSongList.addAll(randomList)\n                val realIndex = realSongList.indexOfFirst { it.id == originSongList[originIndex].id }\n                if (realIndex != originIndex) {\n                    Collections.swap(realSongList, realIndex, originIndex)\n                }\n                curOriginIndex = originIndex\n                curRealIndex = originIndex\n            }\n            else -> {\n                realSongList.clear()\n                realSongList.addAll(originSongList)\n                curOriginIndex = originIndex\n                curRealIndex = originIndex\n            }\n        }\n//        originSongList.forEachIndexed { index, item ->\n//            println(\"songList $index --> ${item.name}\")\n//        }\n//        println(\"---------------------------------------\")\n//\n//        realSongList.forEachIndexed { index, item ->\n//            println(\"pagerSongList $index --> ${item.name}\")\n//        }\n    }\n\n    private fun innerPlay(songBean: SongBean) {\n        curSongBean = songBean\n        mediaPlayer.setDataSource(songBean)\n        mediaPlayer.start()\n    }\n\n    /**\n     * 根据原始歌曲列表索引播放音乐\n     */\n//    fun playByOriginIndex(originIndex: Int) {\n//        if (originSongList.size > originIndex) {\n//            curOriginIndex = originIndex\n//            curRealIndex = realSongList.indexOfFirst { it.id == originSongList[originIndex].id }\n//            innerPlay(originSongList[curOriginIndex])\n//        }\n//    }\n\n    /**\n     * 根据实际播放模式中的歌曲列表索引播放音乐\n     */\n    fun playByRealIndex(realIndex: Int) {\n        if (originSongList.getOrNull(realIndex) != null) {\n            curRealIndex = realIndex\n            curOriginIndex = originSongList.indexOfFirst { it.id == realSongList[realIndex].id }\n            innerPlay(realSongList[curRealIndex])\n        }\n    }\n\n    override fun onStatusChanged(status: PlayerStatus) {\n        playerStatus = status\n        playing = status == PlayerStatus.STARTED\n        enableSeeking = status == PlayerStatus.STARTED || status == PlayerStatus.PAUSED\n        when (status) {\n            PlayerStatus.COMPLETED -> {\n                autoPlayNext()\n            }\n            is PlayerStatus.ERROR -> {\n                println(\"PlayerStatus.ERROR->${status.errorMsg}\")\n                if (status.errorCode != PlayerErrorCode.ERROR_ENV_INVALID) {\n                    autoPlayNext()\n                } else {\n                    ToastManager.showToast(status.errorMsg, ToastManager.LENGTH_LONG)\n                }\n            }\n            PlayerStatus.STOPPED -> {\n                totalDuringStr = \"00:00\"\n                curPositionStr = \"00:00\"\n                this.progress = 0f\n            }\n            else -> {}\n        }\n    }\n\n    private fun autoPlayNext() {\n        if(playMode == PlayMode.SINGLE) {\n            resume()\n        }else {\n            val newIndex = getNextRealIndex()\n            playByRealIndex(newIndex)\n        }\n    }\n\n    fun pause() {\n        if (playerStatus == PlayerStatus.STARTED) {\n            mediaPlayer.pause()\n        }\n    }\n\n    fun resume() {\n        if (playerStatus == PlayerStatus.PAUSED) {\n            mediaPlayer.resume()\n        }\n    }\n\n    fun isPlaying(): Boolean {\n        return playing\n    }\n\n    /**\n     * 获取当前播放模式下的上一首歌曲索引\n     */\n    fun getPreRealIndex() = if (curRealIndex == 0) realSongList.size - 1 else curRealIndex - 1\n\n    /**\n     * 获取当前播放模式下的下一首歌曲索引\n     */\n    fun getNextRealIndex() = if (curRealIndex == realSongList.size - 1) 0 else curRealIndex + 1\n\n    fun changePlayMode(playMode: PlayMode) {\n        this.playMode = playMode\n        generateRealSongList(curOriginIndex)\n    }\n\n    fun seekTo(progress: Float) {\n        this.progress = progress\n        if (totalDuring != 0) {\n            mediaPlayer.seekTo(progress)\n        }\n        seeking = false\n    }\n\n    fun seeking(progress: Float) {\n        seeking = true\n        this.progress = progress\n        if (totalDuring != 0) {\n            this.curPositionStr = StringUtil.formatMilliseconds((progress * totalDuring / 100).toInt())\n        }\n    }\n\n    override fun onProgress(totalDuring: Int, currentPosition: Int, percentage: Float) {\n        if (!seeking) {\n            this.totalDuring = totalDuring\n            totalDuringStr = StringUtil.formatMilliseconds(totalDuring)\n            curPositionStr = StringUtil.formatMilliseconds(currentPosition)\n            progress = percentage\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/base/UserManager.kt",
    "content": "package base\n\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.collectAsState\nimport com.google.gson.Gson\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.catch\nimport kotlinx.coroutines.flow.map\nimport model.LoginResult\nimport util.DataStoreUtils\n\nobject UserManager {\n\n    const val KEY_LOCAL_LOGIN_RESULT = \"KEY_LOCAL_LOGIN_RESULT\"\n\n    fun getLoginResultFlow(): Flow<LoginResult?> {\n        return DataStoreUtils.getData(KEY_LOCAL_LOGIN_RESULT, \"\")\n            .map {\n\n                var loginResult: LoginResult? = null\n                try {\n                    loginResult = Gson().fromJson(it, LoginResult::class.java)\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                }\n                loginResult\n            }\n    }\n\n    fun getLoginResult(): LoginResult?  {\n        return try {\n            val json = DataStoreUtils.readStringData(KEY_LOCAL_LOGIN_RESULT, \"\")\n            Gson().fromJson(json, LoginResult::class.java)\n        } catch (e: Exception) {\n            null\n        }\n    }\n\n\n    suspend fun saveLoginResult(json: String) {\n        DataStoreUtils.putData(KEY_LOCAL_LOGIN_RESULT, json)\n    }\n\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/base/player/IPlayer.kt",
    "content": "package base.player\n\nimport model.SongBean\n\n\n/**\n * Created by ssk on 2022/4/23.\n */\ninterface IPlayer {\n    fun setDataSource(songBean: SongBean)\n    fun start()\n    fun pause()\n    fun resume()\n    fun stop()\n    fun seekTo(position: Float)\n\n    fun envAvailable() = false\n\n    fun addListener(listener: IPlayerListener)\n    fun removeListener(listener: IPlayerListener)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/player/IPlayerListener.kt",
    "content": "package base.player\n\n\n/**\n * Created by ssk on 2022/4/23.\n */\ninterface IPlayerListener {\n    fun onStatusChanged(status: PlayerStatus)\n    fun onProgress(totalDuring: Int, currentPosition: Int, percentage: Float)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/player/NCPlayer.kt",
    "content": "package base.player\n\nimport http.NCRetrofitClient\nimport kotlinx.coroutines.*\nimport model.SongBean\nimport uk.co.caprica.vlcj.factory.discovery.NativeDiscovery\nimport uk.co.caprica.vlcj.player.base.MediaPlayer\nimport uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter\nimport uk.co.caprica.vlcj.player.component.AudioPlayerComponent\n\n/**\n * 音乐播放器,基于vlcj\n */\nclass NCPlayer : IPlayer {\n\n    private var mStatus: PlayerStatus = PlayerStatus.IDLE\n    private var mCurSongBean: SongBean? = null\n    private val mListeners = ArrayList<IPlayerListener>()\n    private var mJob: Job? = null\n    private var mMediaPlayer: MediaPlayer? = null\n    private var mDuring: Int = 0\n    private var mCurTime: Int = 0\n\n    init {\n        if (envAvailable()) {\n            mMediaPlayer = AudioPlayerComponent().mediaPlayer().apply {\n                this.events().addMediaPlayerEventListener(object : MediaPlayerEventAdapter() {\n                    override fun mediaPlayerReady(mediaPlayer: MediaPlayer) {\n                        super.mediaPlayerReady(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----mediaPlayerReady\")\n                        innerStartPlay()\n                    }\n\n                    override fun finished(mediaPlayer: MediaPlayer) {\n                        super.finished(mediaPlayer)\n                        setStatus(PlayerStatus.COMPLETED)\n                        println(\"----${mCurSongBean?.name}----finished\")\n                    }\n\n                    override fun timeChanged(mediaPlayer: MediaPlayer, newTime: Long) {\n                        super.timeChanged(mediaPlayer, newTime)\n                        mCurTime = newTime.toInt()\n                        updateProgress()\n                    }\n\n                    override fun opening(mediaPlayer: MediaPlayer?) {\n                        super.opening(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----opening\")\n                    }\n\n                    override fun playing(mediaPlayer: MediaPlayer?) {\n                        super.playing(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----playing\")\n                    }\n\n                    override fun paused(mediaPlayer: MediaPlayer?) {\n                        super.paused(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----paused\")\n                    }\n\n                    override fun stopped(mediaPlayer: MediaPlayer?) {\n                        super.stopped(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----stopped\")\n                    }\n\n                    override fun error(mediaPlayer: MediaPlayer?) {\n                        super.error(mediaPlayer)\n                        println(\"----${mCurSongBean?.name}----error\")\n                        setStatus(PlayerStatus.ERROR(PlayerErrorCode.ERROR_PLAY, \"播放失败\"))\n                    }\n                })\n            }\n        }\n    }\n\n    private fun innerStartPlay() {\n        if (envAvailable()) {\n            println(\"----${mCurSongBean?.name}----innerStartPlay()\")\n            mMediaPlayer?.controls()?.start()\n            setStatus(PlayerStatus.STARTED)\n        }\n    }\n\n    override fun setDataSource(songBean: SongBean) {\n        mCurSongBean = songBean\n        println(\"----${mCurSongBean?.name}----setDataSource()\")\n    }\n\n    override fun start() {\n        if (envAvailable()) {\n            println(\"----${mCurSongBean?.name}----start()\")\n            if (mStatus == PlayerStatus.STARTED\n            ) {\n                pause()\n            }\n            mCurSongBean?.let {\n                mDuring = it.dt\n                mCurTime = 0\n                updateProgress()\n                getSongUrlAndPlay(it.id)\n            }\n        }\n    }\n\n    private fun getSongUrlAndPlay(songId: Long) {\n        mJob?.cancel()\n        mJob = GlobalScope.launch(context = Dispatchers.IO) {\n            try {\n                val url = NCRetrofitClient.getNCApi().getSongUrl(songId).data.firstOrNull()?.url\n                    ?: \"https://music.163.com/song/media/outer/url?id=$songId.mp3\"\n                mMediaPlayer?.media()?.play(url)\n            } catch (e: Exception) {\n                if (e !is CancellationException) {\n                    println(\"getSongUrlAndPlay e = $e\")\n                    e.printStackTrace()\n                    mListeners.forEach {\n                        it.onStatusChanged(PlayerStatus.ERROR(PlayerErrorCode.ERROR_GET_URL, \"获取歌曲播放链接失败\"))\n                    }\n                }\n            }\n        }\n    }\n\n    fun updateProgress() {\n        if (mDuring != 0) {\n            mListeners.forEach {\n                it.onProgress(mDuring, mCurTime, mCurTime.toFloat() * 100 / mDuring)\n            }\n        }\n    }\n\n\n    override fun pause() {\n        if (envAvailable()) {\n            if (mStatus == PlayerStatus.STARTED) {\n                println(\"----${mCurSongBean?.name}----pause()\")\n                mMediaPlayer?.controls()?.pause()\n                setStatus(PlayerStatus.PAUSED)\n            }\n        }\n    }\n\n    override fun resume() {\n        println(\"----${mCurSongBean?.name}----resume()\")\n        innerStartPlay()\n    }\n\n    override fun stop() {\n        if (envAvailable()) {\n            println(\"----${mCurSongBean?.name}----stop()\")\n            mMediaPlayer?.controls()?.stop()\n            mDuring = 0\n            setStatus(PlayerStatus.STOPPED)\n            setStatus(PlayerStatus.IDLE)\n        }\n    }\n\n    override fun seekTo(position: Float) {\n        if (envAvailable()) {\n            println(\"----${mCurSongBean?.name}----seekTo->${position}\")\n            mMediaPlayer?.controls()?.setPosition(position / 100)\n        }\n    }\n\n    private fun setStatus(status: PlayerStatus) {\n        mStatus = status\n        mListeners.forEach {\n            it.onStatusChanged(mStatus)\n        }\n    }\n\n    override fun envAvailable(): Boolean {\n        if (NativeDiscovery().discover()) {\n            return true\n        } else {\n            setStatus(PlayerStatus.ERROR(PlayerErrorCode.ERROR_ENV_INVALID, \"NCMusicDesktop播放音乐需依赖VLC组件，当前设备未检测到VLC组件，请前往 https://www.videolan.org/ 下载并安装。\"))\n            return false\n        }\n    }\n\n    override fun addListener(listener: IPlayerListener) {\n        mListeners.add(listener)\n    }\n\n    override fun removeListener(listener: IPlayerListener) {\n        mListeners.remove(listener)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/player/PlayMode.kt",
    "content": "package base.player\n\n/**\n * Created by ssk on 2022/4/23.\n */\nenum class PlayMode {\n    // 单曲循环\n    SINGLE,\n    // 随机\n    RANDOM,\n    // 列表循环\n    LOOP,\n}"
  },
  {
    "path": "src/jvmMain/kotlin/base/player/PlayerStatus.kt",
    "content": "package base.player\n\n/**\n * Created by ssk on 2022/4/23.\n */\nsealed class PlayerStatus() {\n    object IDLE: PlayerStatus()\n    object PREPARED: PlayerStatus()\n    object STARTED: PlayerStatus()\n    object PAUSED: PlayerStatus()\n    object STOPPED: PlayerStatus()\n    object COMPLETED: PlayerStatus()\n    class ERROR(val errorCode: Int, val errorMsg: String): PlayerStatus()\n}\n\nobject PlayerErrorCode {\n    // 环境错误，没有安装VLC组件\n    const val ERROR_ENV_INVALID = 1\n    // 获取歌曲播放URL错误\n    const val ERROR_GET_URL = 1\n    // 播放错误\n    const val ERROR_PLAY = 2\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/http/RetrofitClient.kt",
    "content": "package http\n\nimport http.api.NCApi\nimport http.interceptor.CookieIntercept\nimport okhttp3.OkHttpClient\nimport retrofit2.Retrofit\nimport retrofit2.converter.gson.GsonConverterFactory\nimport java.util.concurrent.TimeUnit\n\n\nobject NCRetrofitClient {\n    private var ncApi: NCApi? = null\n    fun getNCApi(): NCApi {\n        if (ncApi == null) {\n            ncApi = RetrofitClient.getApi(NCApi::class.java)\n        }\n        return ncApi!!\n    }\n}\n\nobject RetrofitClient {\n    const val BASE_URL = \"https://ncmusic.sskevan.cn\"\n    private const val CONNECT_TIMEOUT = 30L\n    private const val READ_TIMEOUT = 10L\n    fun <T> getApi(retrofit: Class<T>): T = createRetrofit().create(retrofit)\n\n    private fun createRetrofit(url: String = BASE_URL): Retrofit {\n        // okHttpClientBuilder\n        val okHttpClientBuilder = OkHttpClient().newBuilder().apply {\n            connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)\n            readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)\n            addInterceptor(CookieIntercept())\n        }\n\n        return RetrofitBuild(\n            url = url,\n            client = okHttpClientBuilder.build(),\n            gsonFactory = GsonConverterFactory.create()\n        ).retrofit\n    }\n\n}\n\nclass RetrofitBuild(\n    url: String, client: OkHttpClient,\n    gsonFactory: GsonConverterFactory\n) {\n    val retrofit: Retrofit = Retrofit.Builder().apply {\n        baseUrl(url)\n        client(client)\n        addConverterFactory(gsonFactory)\n\n    }.build()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/http/api/NCApi.kt",
    "content": "package http.api\n\nimport model.*\nimport retrofit2.http.GET\nimport retrofit2.http.Path\nimport retrofit2.http.Query\nimport java.util.*\n\ninterface NCApi {\n\n    /**\n     * 获取推荐歌单\n     */\n    @GET(\"personalized\")\n    suspend fun getRecommendPlayList(@Query(\"limit\") limit: Int = 20): RecommendPlayListResult\n\n    /**\n     * 获取独家放送\n     */\n    @GET(\"personalized/privatecontent\")\n    suspend fun getPrivateContent(): PrivateContentResult\n\n    /**\n     * 新歌速递\n     */\n    @GET(\"/top/song\")\n    suspend fun getNewSong(): NewSongResult\n\n    /**\n     * 新歌速递\n     */\n    @GET(\"/personalized/mv\")\n    suspend fun getRecommendMV(): RecommendMVResult\n\n    /**\n     * 精品歌单\n     */\n    @GET(\"/top/playlist/highquality\")\n    suspend fun getHighQualityPlayList(@Query(\"limit\") limit: Int = 20, @Query(\"cat\") cat: String?): PlayListResult\n\n    /**\n     * 热门歌单分类\n     */\n    @GET(\"/playlist/hot\")\n    suspend fun getHotPlayListCategories(): HotPlayListTabResult\n\n\n    /**\n     * 歌单分类\n     */\n    @GET(\"/playlist/catlist\")\n    suspend fun getPlayListCategories(): PlayListTabResult\n\n\n    /**\n     * 歌单列表\n     */\n    @GET(\"/top/playlist\")\n    suspend fun getPlayList(\n        @Query(\"limit\") limit: Int = 20,\n        @Query(\"tag\") tag: String,\n        @Query(\"offset\") offset: Int\n    ): PlayListResult\n\n    /**\n     * 获取歌单详情\n     */\n    @GET(\"playlist/detail\")\n    suspend fun getPlaylistDetail(@Query(\"id\") id: Long): PlaylistDetailResult\n\n    /**\n     * 获取歌曲详情\n     */\n    @GET(\"song/detail\")\n    suspend fun getSongDetail(@Query(\"ids\") ids: String): SongDetailResult\n\n    /**\n     * 获取评论列表\n     */\n    @GET(\"comment/{commentType}\")\n    suspend fun getCommentList(\n        @Path(\"commentType\") commentType: String,\n        @Query(\"id\") id: Long,\n        @Query(\"limit\") limit: Int = 20,\n        @Query(\"offset\") offset: Int,\n//        @Query(\"before\") before: Long, // 分页参数,取上一页最后一项的 time 获取下一页数据(获取超过 5000 条评论的时候需要用到)\n    ): CommentResult\n\n\n    /**\n     * 获取二维码登录key\n     */\n    @GET(\"/login/qr/key\")\n    suspend fun getLoginQrcodeKey(@Query(\"timeStamp\") timeStamp: Long = Date().time): QrcodeKeyResult\n\n    /**\n     * 获取二维码登录链接\n     */\n    @GET(\"/login/qr/create\")\n    suspend fun getLoginQrcodeValue(\n        @Query(\"key\") key: String,\n        @Query(\"timeStamp\") timeStamp: Long = Date().time\n    ): QrcodeValueResult\n\n    /**\n     * 验证二维码登录授权结果\n     */\n    @GET(\"/login/qr/check\")\n    suspend fun checkQrcodeAuthStatus(\n        @Query(\"key\") key: String,\n        @Query(\"timeStamp\") timeStamp: Long = Date().time\n    ): QrcodeAuthResult\n\n    /**\n     * 获取用户信息\n     */\n    @GET(\"/user/account\")\n    suspend fun getAccountInfo(\n        @Query(\"cookie\") cookie: String,\n    ): AccountInfoResult\n\n    /**\n     * 获取用户歌单\n     */\n    @GET(\"user/playlist\")\n    suspend fun getUserPlayList(@Query(\"uid\") uid: String): UserPlaylistResult\n\n    /**\n     * 获取歌曲url\n     */\n    @GET(\"/song/url\")\n    suspend fun getSongUrl(\n        @Query(\"id\") id: Long,\n        @Query(\"br\") br: Int = 128000\n    ): SongUrlBean\n\n    /**\n     * 获取歌词\n     */\n    @GET(\"/lyric\")\n    suspend fun getLyric(@Query(\"id\") id: Long): LyricResult\n\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/http/interceptor/CookieInterceptor.kt",
    "content": "package http.interceptor\n\nimport androidx.compose.runtime.collectAsState\nimport base.UserManager\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\n/**\n * Created by ssk on 2022/4/24.\n */\nclass CookieIntercept : Interceptor {\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val mLoginResult = UserManager.getLoginResult()\n        if (mLoginResult != null) {\n            val request = chain.request()\n            val url = if(request.url().toString().contains(\"?\")) {\n                request.url().toString() + \"&cookie=\" + mLoginResult.cookie\n            }else {\n                request.url().toString() + \"?cookie=\" + mLoginResult.cookie\n            }\n            val builder = request.newBuilder()\n            builder.get().url(url)\n            val newRequest = builder.build()\n            return chain.proceed(newRequest);\n        }\n        return chain.proceed(chain.request())\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/model/BasePagingBean.kt",
    "content": "package model\n\ninterface IBasePagingBean {\n    fun getTotalCount(): Int\n}"
  },
  {
    "path": "src/jvmMain/kotlin/model/BaseResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\nimport java.io.Serializable\n\n/**\n * Created by ssk on 2022/4/17.\n */\n@Keep\nopen class BaseResult(val code: Int? = 0, val message: String? = null) : Serializable {\n    open fun resultOk(): Boolean {\n        return code == 200\n    }\n\n    open fun isEmpty() = false\n}"
  },
  {
    "path": "src/jvmMain/kotlin/model/CommentResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\n\n@Keep\ndata class CommentResult(\n    val isMusician: Boolean = false,\n    val userId: Long = 0,\n    val total: Int = 0,\n    val more: Boolean = false,\n    val comments: List<CommentBean> = emptyList(),\n    val topComments: List<CommentBean> = emptyList(),\n    val hotComments: List<CommentBean> = emptyList(),\n) : BaseResult() {\n    override fun isEmpty() = comments.isEmpty()\n}\n\n@Keep\ndata class CommentBean(\n    val user: CommentUser,\n    val content: String = \"\",\n    val time: Long = 0,\n    var likedCount: Int = 0,\n    val showFloorComment: FloorComment? = null,\n    val tag: Tag? = null,\n    val commentId: Long = 0L,\n    val beReplied: List<BeReplied>? = null,\n    var liked: Boolean = false,\n) {\n    @Keep\n    data class Tag(\n        val datas: List<TagData>? = null,\n    )\n\n    @Keep\n    data class TagData(\n        val text: String = \"\",\n    )\n}\n\n@Keep\ndata class FloorComment(\n    val replyCount: Long = 0,\n    val showReplyCount: Boolean = false,\n)\n\n@Keep\ndata class CommentUser(\n    val nickname: String = \"\",\n    val userId: Long = 0,\n    val avatarUrl: String? = null,\n)\n\n@Keep\ndata class BeReplied(\n    val user: CommentUser,\n    val content: String? = null,\n    val status: Int = 0,\n    val beRepliedCommentId: Long = 0,\n)\n\n@Keep\ndata class NewCommentResult(\n    val data : CommentData\n) : BaseResult()\n\n@Keep\ndata class CommentData(\n    val totalCount: Int = 0,\n    val hasMore: Boolean = false,\n    var cursor: String,\n    val comments: List<CommentBean> = emptyList(),\n)\n\n@Keep\ndata class FloorCommentResult(\n    val data : FloorCommentData\n) : BaseResult()\n\n@Keep\ndata class FloorCommentData(\n    val totalCount: Int = 0,\n    val ownerComment: CommentBean,\n    val comments: List<CommentBean> = emptyList(),\n)"
  },
  {
    "path": "src/jvmMain/kotlin/model/LoginResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\n\n@Keep\ndata class QrcodeKeyResult(val data: QrcodeKeyBean): BaseResult()\n\n@Keep\ndata class QrcodeValueResult(val data: QrcodeValueBean): BaseResult()\n\n@Keep\ndata class QrcodeKeyBean(val unikey: String)\n\n@Keep\ndata class QrcodeValueBean(val qrurl: String, val qrimg: String?)\n\n@Keep\ndata class QrcodeAuthResult(val cookie: String): BaseResult() {\n    override fun resultOk(): Boolean {\n        return code == 803\n    }\n}\n\n@Keep\ndata class AccountInfoResult(val account: AccountBean, val profile: ProfileBean): BaseResult()\n\n@Keep\ndata class LoginResult(\n    val account: AccountBean,\n    val profile: ProfileBean,\n    val cookie: String\n)\n\n@Keep\ndata class AccountBean(\n    val id: Long,\n    val userName: String,\n    val type: Int,\n    val status: Int,\n    val whitelistAuthority: Int,\n    val createTime: Long,\n    val tokenVersion: Int,\n    val ban: Int,\n    val baoyueVersion: Int,\n    val donateVersion: Int,\n    val vipType: Int,\n    val viptypeVersion: Double,\n    val anonimousUser: Boolean\n)\n\n@Keep\ndata class ProfileBean(\n    val followed: Boolean,\n    val userId: Int,\n    val defaultAvatar: Boolean,\n    val avatarUrl: String?,\n    val nickname: String,\n    val birthday: Long,\n    val province: Int,\n    val accountStatus: Int,\n    val vipType: Int,\n    val gender: Int,\n    val djStatus: Int,\n    val mutual: Boolean,\n    val authStatus: Int,\n    val backgroundImgId: Long,\n    val userType: Int,\n    val city: Int,\n    val backgroundUrl: String?,\n    val followeds: Int,\n    val follows: Int,\n    val eventCount: Int,\n    val playlistCount: Int,\n    val playlistBeSubscribedCount: Int\n)\n"
  },
  {
    "path": "src/jvmMain/kotlin/model/LyricResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\nimport model.BaseResult\n\n/**\n * Created by ssk on 2022/5/11.\n */\n@Keep\nclass LyricResult(\n    val transUser: LyricContributorBean?,\n    val lyricUser: LyricContributorBean?,\n    val lrc: LrcBean?,\n    val tlyric: LrcBean?\n) : BaseResult() {\n    override fun isEmpty() = transUser == null && lyricUser == null && lrc == null && tlyric == null\n}\n\n@Keep\ndata class LyricContributorBean(\n    val id: Long,\n    val status: Int,\n    val demand: Int,\n    val userid: Long,\n    val nickname: String,\n    val uptime: Long\n)\n\n\n/**\n * version : 11\n * lyric : [00:00.42]遠く離れてるほどに  近くに感じてる\n * [00:07.87]寂しさも強さへと  変換(かわ)ってく\n * [00:13.74]…君を想ったなら\n * [00:34.98]街も 人も 夢も 変えていく時間に\n * [00:41.54]ただ  逆らっていた\n * [00:48.35]言葉を重ねても  理解(わか)り合えないこと\n * [00:56.18]まだ  知らなかったね\n * [01:01.76]\n * [01:03.20]君だけを抱きしめたくて失くした夢  君は\n * [01:10.55]「諦メナイデ」と云った\n * [01:16.48]\n * [01:17.63]遠く離れてるほどに  近くに感じてる\n * [01:24.38]寂しさも強さへと  変換(かわ)ってく\n * [01:29.66]…君を想ったなら\n * [01:32.75]切なく胸を刺す  それは夢の欠片(かけら)\n * [01:39.06]ありのまま出逢えてた  その奇跡\n * [01:44.42]もう一度信じて\n * [01:47.85]\n * [01:54.90]君がいない日々に  ずっと  立ち止まった\n * [02:02.73]でも  歩き出してる\n * [02:09.58]君と分かち合った  どの偶然にも意味が\n * [02:17.50]そう  必ずあった\n * [02:23.18]\n * [02:24.53]それぞれの夢を叶えて  まためぐり逢う時\n * [02:31.85]偶然は運命になる\n * [02:37.46]\n * [02:38.91]破れた約束さえも  誓いに変えたなら\n * [02:45.55]あの場所で  出逢う時  あの頃の\n * [02:50.97]二人に戻(なれ)るかな?\n * [02:54.05]\"優しさ\"  に似ている  懐かしい面影\n * [03:00.50]瞳(め)を閉じて見えるから\n * [03:04.23]手を触れず在(あ)ることを知るから\n * [03:09.01]\n * [03:34.32]明日(あす)に  はぐれて  答えが\n * [03:37.79]何も見えなくても\n * [03:41.10]君に逢う  そのために重ねてく\n * [03:46.43]\"今日\"  という真実\n * [03:50.27]\n * [03:50.95]遠く離れてるほどに  近くに感じてる\n * [03:57.77]寂しさも強さへと  変換(かわ)ってく\n * [04:03.03]…君を想ったなら\n * [04:06.02]切なく胸を刺す  それは夢の欠片(かけら)\n * [04:12.53]ありのまま出逢えてた  その奇跡\n * [04:17.75]もう一度信じて\n */\n@Keep\ndata class LrcBean(\n    val version: Int,\n    val lyric: String\n)"
  },
  {
    "path": "src/jvmMain/kotlin/model/NewSongResult.kt",
    "content": "package model\n\n/**\n * 新歌速递\n */\ndata class NewSongResult(\n    var data: List<NewSongBean>\n) : BaseResult() {\n    override fun isEmpty() = data.isEmpty()\n}\n\ndata class NewSongBean(\n    val name: String,\n    val artists: List<ArtistBean>,\n    val album: Album\n)\n\ndata class ArtistBean(\n    val id: Long,\n    val name: String\n)\n\ndata class Album(\n    val id: Long,\n    val name: String,\n    val picUrl: String\n)"
  },
  {
    "path": "src/jvmMain/kotlin/model/PlayListResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\nimport java.io.Serializable\n\n/**\n * 推荐歌单结果\n */\ndata class RecommendPlayListResult(\n    val result: List<SimplePlayListItem>\n) : BaseResult() {\n    override fun isEmpty() = result.isEmpty()\n}\n\n/**\n * 推荐歌单列表item\n */\ndata class SimplePlayListItem(\n    val id: Long,\n    val name: String,\n    val picUrl: String,\n    val copywriter: String?,\n    val trackNumberUpdateTime: Long,\n    val playCount: Long,\n    val trackCount: Int,\n    val subscribedCount: Int,\n    val shareCount: Int\n) : Serializable\n\n@Keep\ndata class PlaylistDetail(\n    val tracks: List<Track>?,\n    val trackIds: List<TrackId>?,\n    val creator: Subscribers,\n    val name: String = \"\",\n    val coverImgUrl: String = \"\",\n    val trackCount: Int = 0,\n    val id: Long = 0,\n    val playCount: Long = 0,\n    val description: String?,\n    val shareCount: Int,\n    val commentCount: Int,\n    val trackUpdateTime: Long,\n    val subscribedCount: Int,\n    val subscribers: List<Subscribers>,\n    val tags: List<String>,\n) : Serializable {\n    fun convertToSimple()  = SimplePlayListItem(id, name, coverImgUrl, description, trackUpdateTime, playCount, trackCount, subscribedCount, shareCount)\n}\n\n/**\n * 歌单结果\n */\ndata class PlayListResult(\n    val playlists: List<PlaylistDetail>,\n    val total: Int,\n    val more: Boolean\n) : BaseResult(), IBasePagingBean {\n    override fun getTotalCount() = total\n}\n\n\n\n\n@Keep\ndata class Subscribers(\n    val userId: Long, val avatarUrl: String, val nickname: String, val description: String?\n) : Serializable\n\n@Keep\ndata class Track(\n    val name: String,\n    val id: Long,\n    val mv: Long,\n    val ar: List<Ar>,\n    val al: Al,\n) : Serializable\n\n@Keep\ndata class TrackId(\n    val id: Int = 0, val v: Int = 0, val alg: String? = null\n) : Serializable\n\n@Keep\ndata class Ar(\n    val id: Long,\n    val name: String,\n)\n\n@Keep\ndata class Al(\n    val id: Long,\n    val name: String,\n    val picUrl: String,\n)\n\n\n\n/**\n * 热门歌单分类\n *\n */\ndata class HotPlayListTabResult(val tags: List<PlayListTab>) : BaseResult()\n\n\ndata class PlayListTab(\n    val id: Int,\n    val name: String,\n    val category: Int,\n    val hot: Boolean,\n)\n\n\n/**\n * 歌单分类\n */\ndata class PlayListTabResult(\n    val all: PlayListTab,\n    val sub: List<PlayListTab>,\n    val categories: Map<Int, String>) : BaseResult()\n\n@Keep\ndata class PlaylistDetailResult(\n    val playlist: PlaylistDetail,\n) : BaseResult()\n\n\n/**\n * 个人歌单\n */\n@Keep\ndata class UserPlaylistResult(\n    val playlist: List<PlaylistDetail>,\n) : BaseResult()\n"
  },
  {
    "path": "src/jvmMain/kotlin/model/PrivateContentResult.kt",
    "content": "package model\n\n\n/**\n * 独家放送\n */\ndata class PrivateContentResult(\n    val result: List<PrivateContentItem>\n) : BaseResult() {\n    override fun isEmpty() = result.isEmpty()\n}\n\ndata class PrivateContentItem(\n    val id: Long,\n    val url: String?,\n    val picUrl: String,\n    val sPicUrl: String,\n    val type: Int,\n    val copywriter: String,\n    val name: String,\n    val alg: String\n)"
  },
  {
    "path": "src/jvmMain/kotlin/model/RecommendMVResult.kt",
    "content": "package model\n\n/**\n * 推荐MV\n */\ndata class RecommendMVResult(\n    val result: List<RecommendMVItem>\n) : BaseResult() {\n    override fun isEmpty() = result.isEmpty()\n}\n\ndata class RecommendMVItem(\n    val id: Long,\n    val name: String,\n    val artistName: String,\n    val copywriter: String,\n    val picUrl: String,\n    val playCount: Long\n)"
  },
  {
    "path": "src/jvmMain/kotlin/model/SongDetailResult.kt",
    "content": "package model\n\nimport androidx.annotation.Keep\n\n@Keep\ndata class SongDetailResult(val songs: List<SongBean>) : BaseResult() {\n    override fun isEmpty() = songs.isEmpty()\n}\n\n@Keep\ndata class SongBean(\n    //歌曲id\n    val id: Long,\n    //歌曲名称\n    val name: String,\n    val al: Al,\n    val ar: List<Ar>,\n    val dt: Int,\n) {\n    fun getSongTimeLength() : String {\n        val dtSecond = dt / 1000\n        val min = dtSecond  / 60\n        val second = dtSecond - min * 60\n        val minStr = if (min < 10) \"0$min\" else min.toString()\n        val secondStr = if (second < 10) \"0$second\" else second.toString()\n        return \"$minStr:$secondStr\"\n    }\n}\n\n@Keep\ndata class SongUrlBean(val data: List<SongUrl>)\n\n@Keep\ndata class SongUrl(val url: String)\n"
  },
  {
    "path": "src/jvmMain/kotlin/router/NavGraph.kt",
    "content": "package router\n\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.google.gson.Gson\nimport model.SimplePlayListItem\nimport moe.tlaster.precompose.navigation.NavHost\nimport moe.tlaster.precompose.navigation.Navigator\nimport moe.tlaster.precompose.navigation.query\nimport moe.tlaster.precompose.navigation.transition.NavTransition\nimport ui.discovery.DiscoveryPage\nimport ui.playlist.PlayListDetailPage\nimport ui.setting.SettingPage\nimport ui.todo.TodoPage\n\n\nobject NCNavigatorManager {\n    lateinit var navigator: Navigator\n}\n\n@Composable\nfun NavGraph() {\n    val navigator = NCNavigatorManager.navigator\n    NavHost(\n        navigator = navigator,\n        navTransition = remember {\n            NavTransition(\n                createTransition = fadeIn(),\n                destroyTransition = fadeOut(),\n                pauseTransition = fadeOut(),\n                resumeTransition = fadeIn(),\n            )\n        }, initialRoute = RouterUrls.DISCOVERY\n    ) {\n        scene(RouterUrls.DISCOVERY) {\n            DiscoveryPage()\n        }\n        scene(RouterUrls.PODCAST) {\n            TodoPage(\"播客\")\n        }\n        scene(RouterUrls.PERSONAL_FM) {\n            TodoPage(\"私人fm\")\n        }\n        scene(RouterUrls.VIDEO) {\n            TodoPage(\"视频\")\n        }\n        scene(RouterUrls.FOLLOW) {\n            TodoPage(\"关注\")\n        }\n        scene(RouterUrls.FAVORITE_MUSIC) {\n            TodoPage(\"我喜欢的音乐\")\n        }\n        scene(RouterUrls.DOWNLOAD_MANAGER) {\n            TodoPage(\"个性推荐\")\n        }\n        scene(RouterUrls.RECENT_PLAYLIST) {\n            TodoPage(\"最近播放\")\n        }\n        scene(RouterUrls.MY_CLOUD_DISK) {\n            TodoPage(\"我的音乐云盘\")\n        }\n        scene(RouterUrls.MY_PODCAST) {\n            TodoPage(\"我的播客\")\n        }\n        scene(RouterUrls.MY_COLLECT) {\n            TodoPage(\"我的收藏\")\n        }\n        scene(RouterUrls.SETTING) {\n            SettingPage()\n        }\n\n        scene(\"${RouterUrls.PLAY_LIST_DETAIL}\") {backStackEntry ->\n            val simplePlayListInfo = backStackEntry.query<String>(\"simplePlayListInfo\")\n            val simplePlayListItem = Gson().fromJson(simplePlayListInfo, SimplePlayListItem::class.java)\n            PlayListDetailPage(simplePlayListItem)\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/router/RouterUrls.kt",
    "content": "package router\n\nobject RouterUrls {\n    // 发现音乐\n    const val DISCOVERY = \"discovery\"\n\n    // 播客\n    const val PODCAST = \"podcast\"\n\n    // 私人fm\n    const val PERSONAL_FM = \"personalFm\"\n\n    // 视频\n    const val VIDEO = \"video\"\n\n    // 关注\n    const val FOLLOW = \"follow\"\n\n    // 我喜欢的音乐\n    const val FAVORITE_MUSIC = \"favoriteMusic\"\n\n    // 下载管理\n    const val DOWNLOAD_MANAGER = \"downloadManager\"\n\n    // 最近播放\n    const val RECENT_PLAYLIST = \"recentPlaylist\"\n\n    // 我的音乐云盘\n    const val MY_CLOUD_DISK = \"myCloudDisk\"\n\n    // 我的播客\n    const val MY_PODCAST = \"myPodcast\"\n\n    // 我的收藏\n    const val MY_COLLECT = \"myCollect\"\n\n    // 歌单详情\n    const val PLAY_LIST_DETAIL = \"playListDetail\"\n\n    // 设置\n    const val SETTING = \"setting\"\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/CommonImage.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport org.succlz123.lib.imageloader.ImageAsyncImageFile\nimport org.succlz123.lib.imageloader.ImageAsyncImageUrl\nimport org.succlz123.lib.imageloader.ImageRes\nimport org.succlz123.lib.imageloader.core.ImageCallback\n\n\n@Composable\nfun AsyncImage(\n    modifier: Modifier,\n    url: String?,\n    placeHolderUrl: String? = \"image/ic_disk_place_holder.webp\",\n    errorUrl: String? = \"image/ic_disk_place_holder.webp\",\n    contentScale: ContentScale = ContentScale.Crop\n) {\n    val imgUrl = url ?: \"image/ic_disk_place_holder.webp\"\n    if (imgUrl.startsWith(\"http\")) {\n        ImageAsyncImageUrl(url = imgUrl, imageCallback = ImageCallback(placeHolderView = {\n            placeHolderUrl?.let {\n                Image(\n                    painter = painterResource(placeHolderUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }, errorView = {\n            errorUrl?.let {\n                Image(\n                    painter = painterResource(errorUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }) {\n            Image(\n                painter = it, contentDescription = imgUrl, modifier = modifier, contentScale = contentScale\n            )\n        })\n    } else if (imgUrl.startsWith(\"/\") || imgUrl.contains(\":\\\\\")) {\n        ImageAsyncImageFile(filePath = imgUrl, imageCallback = ImageCallback(placeHolderView = {\n            placeHolderUrl?.let {\n                Image(\n                    painter = painterResource(placeHolderUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }, errorView = {\n            errorUrl?.let {\n                Image(\n                    painter = painterResource(errorUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }) {\n            Image(\n                painter = it, contentDescription = imgUrl, modifier = modifier, contentScale = contentScale\n            )\n        })\n    } else {\n        ImageRes(resName = imgUrl, imageCallback = ImageCallback(placeHolderView = {\n            placeHolderUrl?.let {\n                Image(\n                    painter = painterResource(placeHolderUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }, errorView = {\n            errorUrl?.let {\n                Image(\n                    painter = painterResource(errorUrl),\n                    contentDescription = imgUrl,\n                    modifier = modifier,\n                    contentScale = contentScale\n                )\n            }\n        }) {\n            Image(\n                painter = it, contentDescription = imgUrl, modifier = modifier, contentScale = contentScale\n            )\n        })\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/CommonTabLayout.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.material.TabRowDefaults.tabIndicatorOffset\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.zIndex\nimport ui.common.theme.AppColorsProvider\n\n@Composable\nfun CommonTabLayout(\n    selectedIndex: Int = 0,\n    tabTexts: List<String>,\n    backgroundColor: Color = AppColorsProvider.current.background,  // 背景颜色\n    selectedTextColor: Color = AppColorsProvider.current.firstText,  // 选中tab字体颜色\n    unselectedTextColor: Color = AppColorsProvider.current.secondText,  // 未选中tab字体颜色\n    indicatorColor: Brush = Brush.horizontalGradient(listOf(AppColorsProvider.current.primary, AppColorsProvider.current.secondary)),  // 指示器颜色\n    style: CommonTabLayoutStyle = CommonTabLayoutStyle(),\n    onTabSelected: ((index: Int) -> Unit)? = null\n) {\n    if (style.isScrollable) {\n        ScrollableTabRow(\n            selectedTabIndex = selectedIndex,\n            modifier = style.modifier,\n            edgePadding = 0.dp,\n            backgroundColor = backgroundColor,\n            indicator = @Composable { tabPositions ->\n                if (style.showIndicator) {\n                    if (style.customIndicator != null) {\n                        style.customIndicator.invoke(tabPositions[selectedIndex], selectedIndex)\n                    } else {\n                        Box(\n                            modifier = Modifier\n                                .tabIndicatorOffset(tabPositions[selectedIndex])\n                                .fillMaxSize(),\n                            contentAlignment = Alignment.BottomCenter\n                        ) {\n                            Divider(\n                                modifier = Modifier\n                                    .width(style.indicatorWidth)\n                                    .padding(bottom = style.indicatorPaddingBottom)\n                                    .background(\n                                        brush = indicatorColor,\n                                        shape = RoundedCornerShape(50)\n                                    ),\n                                thickness = style.indicatorHeight,\n                                color = Color.Transparent\n                            )\n                        }\n                    }\n                }\n            },\n            divider = @Composable {\n                Divider(color = Color.Transparent)\n            }\n        ) {\n            tabTexts.forEachIndexed { i, tabText ->\n                var fontWeight = FontWeight.Normal\n                if (selectedIndex == i) {\n                    if (style.selectedTextBold) {\n                        fontWeight = FontWeight.Bold\n                    }\n                } else {\n                    if (style.unselectedTextBold) {\n                        fontWeight = FontWeight.Bold\n                    }\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .pointerInput(Unit) {\n                            detectTapGestures(\n                                onTap = {\n                                    onTabSelected?.invoke(i)\n                                }\n                            )\n                        }\n                        .zIndex(1f),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Text(\n                        text = tabText,\n                        fontSize = if (selectedIndex == i) style.selectedTextSize else style.unselectedTextSize,\n                        fontWeight = fontWeight,\n                        color = if (selectedIndex == i) selectedTextColor else unselectedTextColor,\n                        textAlign = TextAlign.Center,\n                    )\n                }\n\n            }\n        }\n    } else {\n        TabRow(\n            selectedTabIndex = selectedIndex,\n            modifier = style.modifier,\n            backgroundColor = backgroundColor,\n            indicator = @Composable { tabPositions ->\n                if (style.showIndicator) {\n                    if (style.customIndicator != null) {\n                        style.customIndicator.invoke(tabPositions[selectedIndex], selectedIndex)\n                    } else {\n                        Box(\n                            modifier = Modifier\n                                .tabIndicatorOffset(tabPositions[selectedIndex])\n                                .fillMaxSize(),\n                            contentAlignment = Alignment.BottomCenter\n                        ) {\n                            Divider(\n                                modifier = Modifier\n                                    .width(style.indicatorWidth)\n                                    .padding(bottom = style.indicatorPaddingBottom)\n                                    .background(\n                                        brush = indicatorColor,\n                                        shape = RoundedCornerShape(50)\n                                    ),\n                                thickness = style.indicatorHeight,\n                                color = Color.Transparent\n                            )\n                        }\n                    }\n                }\n            },\n            divider = @Composable {\n                Divider(color = Color.Transparent)\n            }\n        ) {\n            tabTexts.forEachIndexed { i, tabText ->\n                var fontWeight = FontWeight.Normal\n                if (selectedIndex == i) {\n                    if (style.selectedTextBold) {\n                        fontWeight = FontWeight.Bold\n                    }\n                } else {\n                    if (style.unselectedTextBold) {\n                        fontWeight = FontWeight.Bold\n                    }\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .pointerInput(Unit) {\n                            detectTapGestures(\n                                onTap = {\n                                    onTabSelected?.invoke(i)\n                                }\n                            )\n                        }\n                        .zIndex(1f),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Text(\n                        text = tabText,\n                        fontSize = if (selectedIndex == i) style.selectedTextSize else style.unselectedTextSize,\n                        fontWeight = fontWeight,\n                        color = if (selectedIndex == i) selectedTextColor else unselectedTextColor,\n                        textAlign = TextAlign.Center\n                    )\n                }\n            }\n        }\n    }\n}\n\n/**\n * 通用TabBar样式\n */\ndata class CommonTabLayoutStyle(\n    val modifier: Modifier = Modifier, // 修饰\n    val selectedTextSize: TextUnit = 14.sp,  // 选中tab字体大小\n    val unselectedTextSize: TextUnit = 14.sp,  // 未选中tab字体大小\n    val selectedTextBold: Boolean = true,  // 选中tab字体加粗\n    val unselectedTextBold: Boolean = false, // 未选中tab字体加粗\n    val showIndicator: Boolean = false, // 是否显示指示器\n    val indicatorWidth: Dp = 50.dp,  // 指示器宽度\n    val indicatorHeight: Dp = 3.dp,  // 指示器高度\n    val indicatorPaddingBottom: Dp = 0.dp,  // 指示器高度\n    val isScrollable: Boolean = true,  // 是否可滑动\n    val customIndicator: @Composable ((selectedTabPosition: TabPosition, selectedPosition: Int) -> Unit)? = null  // 自定义指示器\n)\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/CpnActionMore.kt",
    "content": "package ui.common\n\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.AppColorsProvider\n\n@Composable\nfun CpnActionMore(title: String, onClickMore: (() -> Unit) ?= null) {\n    Row(\n        modifier = Modifier.fillMaxWidth().height(60.dp)\n            .onClick {\n                onClickMore?.invoke()\n            },\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = AppColorsProvider.current.firstText)\n        Icon(\n            painterResource(\"image/ic_more.webp\"),\n            contentDescription = \"更多\",\n            modifier = Modifier.size(16.dp)\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/ExpandableText.kt",
    "content": "package ui.common\n\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport kotlinx.coroutines.launch\nimport ui.common.theme.AppColorsProvider\n\n@Composable\nfun ExpandableText(\n    modifier: Modifier = Modifier,\n    defaultLine: Int = 2,\n    text: String = \"\",\n    color: Color = AppColorsProvider.current.secondText,\n    fontSize: TextUnit = 12.sp,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n) {\n\n    //如果小于 defaultLine\n    var isLessDefaultLine by remember {\n        mutableStateOf(false)\n    }\n\n    var expand by remember {\n        mutableStateOf(false)\n    }\n\n    var subContent by remember(text) {\n        mutableStateOf(text)\n    }\n\n    var measureLineCount by remember(text) { mutableStateOf(false) }\n\n    val anim = remember { Animatable(0f) }\n    val scope = rememberCoroutineScope()\n\n    val content = remember(measureLineCount, expand, text) {\n        if (expand) text else subContent\n    }\n\n    Row(\n        modifier.animateContentSize()\n    ) {\n\n        Text(\n            text = content,\n            modifier = Modifier.padding(end = 10.dp).weight(1f),\n            maxLines = if (expand) Int.MAX_VALUE else defaultLine,\n            color = color,\n            fontSize = fontSize,\n            fontStyle = fontStyle,\n            fontWeight = fontWeight,\n            fontFamily = fontFamily,\n            onTextLayout = { textLayoutResult ->\n\n                if (textLayoutResult.lineCount <= defaultLine && !expand) {\n                    val hasVisualOverflow = textLayoutResult.hasVisualOverflow\n                    if (!measureLineCount) {\n                        isLessDefaultLine = !hasVisualOverflow\n                    }\n\n                    if (hasVisualOverflow) {\n                        val lastCharIndex = textLayoutResult.getLineEnd(defaultLine - 1, true)\n                        //截取 Less状态的内容\n                        val substring = content.substring(0, lastCharIndex)\n                        subContent = substring.substring(0, substring.length) + \"...\"\n                    }\n                }\n                measureLineCount = true\n\n            },\n        )\n\n        if (!isLessDefaultLine) {\n            Icon(\n                painter = painterResource(\"image/ic_triangle_up.webp\"),\n                contentDescription = null,\n                modifier = Modifier.clip(RoundedCornerShape(50)).onClick  {\n                    expand = !expand\n                    scope.launch {\n                        anim.animateTo(if (expand) 1f else 0f)\n                    }\n                }\n                    .size(24.dp)\n                    .padding(6.dp)\n                    .rotate(\n                        anim.value * 180\n                    ),\n                tint = AppColorsProvider.current.secondText\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/ListToGridItems.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\n\nfun <T> LazyListScope.ListToGridItems(\n    data: List<T>,\n    columns: Int,\n    itemContent: @Composable BoxScope.(index: Int, item: T) -> Unit\n) {\n    val rows = (data.size + columns - 1) / columns\n    val groups = mutableListOf<MutableList<T>>()\n    for (row in 0 until rows) {\n        val group = mutableListOf<T>()\n        for (column in 0 until columns) {\n            val originIndex = row * columns + column\n            if (originIndex < data.size) {\n                group.add(data[originIndex])\n            }\n        }\n        groups.add(group)\n    }\n    items(groups.size) {\n        val group = groups[it]\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            group.forEachIndexed { index, item ->\n                val originIndex = index + it * columns\n                Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {\n                    itemContent(originIndex, item)\n                }\n            }\n            if (it == rows - 1 && group.size < columns) {\n                Spacer(modifier = Modifier.weight(columns - group.size.toFloat()))\n            }\n        }\n    }\n}\n\n@Composable\nfun <T> ColumnScope.ListToGridItems(\n    data: List<T>,\n    columns: Int,\n    itemContent: @Composable BoxScope.(index: Int, item: T) -> Unit\n) {\n    val rows = (data.size + columns - 1) / columns\n    val groups = mutableListOf<MutableList<T>>()\n    for (row in 0 until rows) {\n        val group = mutableListOf<T>()\n        for (column in 0 until columns) {\n            val originIndex = row * columns + column\n            if (originIndex < data.size) {\n                group.add(data[originIndex])\n            }\n        }\n        groups.add(group)\n    }\n    for (it in 0 until groups.size) {\n        val group = groups[it]\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            group.forEachIndexed { index, item ->\n                val originIndex = index + it * columns\n                Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {\n                    itemContent(originIndex, item)\n                }\n            }\n            if (it == rows - 1 && group.size < columns) {\n                Spacer(modifier = Modifier.weight(columns - group.size.toFloat()))\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/LoadingComponent.kt",
    "content": "package ui.common\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport ui.common.theme.AppColorsProvider\n\n/**\n * Created by ssk on 2021/9/15.\n */\n@Composable\nfun LoadingComponent(\n    modifier: Modifier = Modifier,\n    loading: Boolean = true,\n    loadingWidth: Dp = 30.dp,\n    loadingHeight: Dp = 25.dp,\n    loadingRadius: Boolean = true,\n    color: Color = AppColorsProvider.current.primary,\n    contentAlignment: Alignment = Alignment.Center\n) {\n\n    val anim by remember {\n        mutableStateOf(Animatable(0.4f))\n    }\n\n    LaunchedEffect(loading) {\n        if (loading) {\n            anim.animateTo(\n                targetValue = 1f,\n                animationSpec = infiniteRepeatable(\n                    animation = tween(durationMillis = 450, easing = LinearEasing),\n                    repeatMode = RepeatMode.Reverse\n                )\n            )\n        } else {\n            anim.stop()\n        }\n    }\n\n\n    Box(\n        modifier = modifier,\n        contentAlignment = contentAlignment\n    ) {\n\n        Canvas(\n            modifier = Modifier\n                .width(loadingWidth)\n                .height(loadingHeight)\n        ) {\n            val rectWidth = size.width / 7\n            val canvasHeight = size.height\n\n            val rectHeight1 = if (loading) {\n                canvasHeight * (0.75f - anim.value * 0.75f + 0.25f)\n            } else {\n                canvasHeight * 0.7f\n            }\n            drawRoundRect(\n                color = color,\n                cornerRadius = CornerRadius(if(loadingRadius) rectWidth / 2 else 0f),\n                topLeft = Offset(0f, canvasHeight - rectHeight1),\n                size = Size(rectWidth, rectHeight1)\n            )\n\n            val rectHeight2 = if (loading) {\n                canvasHeight * (anim.value * 0.65f + 0.2f)\n            } else {\n                canvasHeight * 0.52f\n            }\n            drawRoundRect(\n                color = color,\n                cornerRadius = CornerRadius(if(loadingRadius) rectWidth / 2 else 0f),\n                topLeft = Offset(rectWidth * 2, canvasHeight - rectHeight2),\n                size = Size(rectWidth, rectHeight2)\n            )\n\n\n            val rectHeight3 = if (loading) {\n                canvasHeight * (0.6f - anim.value * 0.6f + 0.4f)\n            } else {\n                canvasHeight * 0.43f\n            }\n            drawRoundRect(\n                color = color,\n                cornerRadius = CornerRadius(if(loadingRadius) rectWidth / 2 else 0f),\n                topLeft = Offset(rectWidth * 4, canvasHeight - rectHeight3),\n                size = Size(rectWidth, rectHeight3)\n            )\n\n            val rectHeight4 = if (loading) {\n                //canvasHeight * (0.8f - anim.value * 0.8f + 0.2f)\n                canvasHeight * (anim.value * 0.45f + 0.3f)\n            } else {\n                canvasHeight * 0.48f\n            }\n            drawRoundRect(\n                color = color,\n                cornerRadius = CornerRadius(if(loadingRadius) rectWidth / 2 else 0f),\n                topLeft = Offset(rectWidth * 6, canvasHeight - rectHeight4),\n                size = Size(rectWidth, rectHeight4)\n            )\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/ModifierExt.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.material.ripple.rememberRipple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\n\n/**\n * Created by ssk on 2022/3/2.\n */\n\n/**\n * 带水波纹点击事件\n * [enableRipple]:是否支持水波纹效果\n * [rippleColor]:水波纹颜色\n * [onClick]:点击回调\n */\n@Composable\nfun Modifier.onClick(enableRipple: Boolean = false, rippleColor: Color = Color.Unspecified, onClick: () -> Unit) = this.clickable (\n    interactionSource = remember { MutableInteractionSource() },\n    indication = if (enableRipple) rememberRipple(color = rippleColor, bounded = true) else null\n) {\n    onClick()\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/NoSuccessComponent.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.AppColorsProvider\n\n@Composable\nfun NoSuccessComponent(\n    modifier: Modifier = Modifier.fillMaxWidth().heightIn(min = 320.dp),\n    contentAlignment: Alignment = Alignment.Center,\n    iconResId: String = \"image/ic_empty.xml\",\n    message: String = \"暂无数据\",\n    retryBlock: (() -> Unit)? = null,\n) {\n    Box(\n        modifier = modifier\n            .let {\n                if (retryBlock != null)\n                    it.onClick { retryBlock.invoke() }\n                else it\n            },\n        contentAlignment = contentAlignment\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center,\n            modifier = Modifier\n                .fillMaxWidth()\n        ) {\n            Icon(\n                painterResource(iconResId),\n                null,\n                tint = AppColorsProvider.current.primary,\n                modifier = Modifier.size(100.dp)\n            )\n            if (!message.isEmpty()) {\n                Text(\n                    \"$message\",\n                    fontSize = 14.sp,\n                    color = AppColorsProvider.current.thirdText,\n                    modifier = Modifier.padding(top = 20.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/PaingFooterNumBar.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.AppColorsProvider\n\n\n/**\n * 分页页数footerBar\n */\n@Composable\nfun PaingFooterNumBar(totalNum: Int, pageSize: Int, curPage: Int, onSelectedPageCallback: (curPage: Int) -> Unit) {\n    val totalPage = remember { (totalNum + (pageSize - 1)) / pageSize }\n    Row(\n        modifier = Modifier.padding(vertical = 10.dp).fillMaxWidth().height(30.dp),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        PaingFooterPreItem(curPage, onSelectedPageCallback)\n\n        // 最多显示10个item\n        if (totalPage <= 10) {\n            for (pageIndex in 1..totalPage) {\n                PaingFooterNumItem(pageIndex, curPage, onSelectedPageCallback)\n            }\n        } else {  // 总页数大于10，需要按照一定的规则显示省略号\n            if (curPage <= 5) {  //当前页码小于5，前8个正常显示，第9个显示省略号，第10个显示最后一个页码\n                for (pageIndex in 1..8) {\n                    PaingFooterNumItem(pageIndex, curPage, onSelectedPageCallback)\n                }\n                PaingFooterMoreItem()\n                PaingFooterNumItem(totalPage, curPage, onSelectedPageCallback)\n            } else if (curPage >= totalPage - 5) {   //当前页码大于倒数第五个页码，后8个正常显示，第1个显示省略号，第1个显示第一个页码\n                PaingFooterNumItem(1, curPage, onSelectedPageCallback)\n                PaingFooterMoreItem()\n                for (pageIndex in totalPage - 8..totalPage) {\n                    PaingFooterNumItem(pageIndex, curPage, onSelectedPageCallback)\n                }\n            } else {  // 否则第1个显示第一个页码，第2个显示省略号，第9个显示省略号，第10个显示最后一个页码，其余6个以当前页码为中心，显示相邻第6个页码\n                PaingFooterNumItem(1, curPage, onSelectedPageCallback)\n                PaingFooterMoreItem()\n                for (pageIndex in curPage - 2..curPage + 3) {\n                    PaingFooterNumItem(pageIndex, curPage, onSelectedPageCallback)\n                }\n                PaingFooterMoreItem()\n                PaingFooterNumItem(totalPage, curPage, onSelectedPageCallback)\n            }\n        }\n        PaingFooterNextItem(totalPage, curPage, onSelectedPageCallback)\n\n    }\n}\n\n@Composable\nprivate fun PaingFooterNumItem(pageIndex: Int, curPage: Int, onSelectedPage: (curPage: Int) -> Unit) {\n    Box(\n        modifier = Modifier.padding(horizontal = 2.dp).height(28.dp).widthIn(min = 28.dp)\n            .clip(RoundedCornerShape(2.dp))\n            .onClick  {\n                onSelectedPage.invoke(pageIndex)\n            }\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(2.dp))\n            .let {\n                if (curPage == pageIndex) it.background(\n                    AppColorsProvider.current.primary,\n                    RoundedCornerShape(2.dp)\n                ) else it\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Text(\n            pageIndex.toString(),\n            color = if (curPage == pageIndex) Color.White else AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            modifier = Modifier.padding(horizontal = 2.dp)\n        )\n    }\n}\n\n@Composable\nprivate fun PaingFooterMoreItem() {\n    Box(\n        modifier = Modifier.padding(horizontal = 2.dp).size(28.dp)\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(2.dp)),\n        contentAlignment = Alignment.Center\n    ) {\n        Text(\n            \"...\",\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp\n        )\n    }\n}\n\n@Composable\nprivate fun PaingFooterPreItem(curPage: Int, onSelectedPage: (curPage: Int) -> Unit) {\n    Box(\n        modifier = Modifier.padding(horizontal = 2.dp).size(28.dp)\n            .clip(RoundedCornerShape(2.dp))\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(2.dp))\n            .let {\n                if (curPage > 1) it.onClick  {\n                    onSelectedPage.invoke(curPage - 1)\n                } else it\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Icon(\n            painterResource(\"image/ic_more.webp\"),\n            tint = if (curPage == 1) AppColorsProvider.current.divider else AppColorsProvider.current.secondIcon,\n            contentDescription = \"\",\n            modifier = Modifier.size(14.dp).rotate(180f)\n        )\n    }\n}\n\n@Composable\nprivate fun PaingFooterNextItem(totalNum: Int, curPage: Int, onSelectedPage: (curPage: Int) -> Unit) {\n    Box(\n        modifier = Modifier.padding(horizontal = 2.dp).size(28.dp)\n            .clip(RoundedCornerShape(2.dp))\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(2.dp))\n            .let {\n                if (curPage < totalNum) it.onClick  {\n                    onSelectedPage.invoke(curPage + 1)\n                } else it\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Icon(\n            painterResource(\"image/ic_more.webp\"),\n            tint = if (curPage == totalNum) AppColorsProvider.current.divider else AppColorsProvider.current.secondIcon,\n            contentDescription = \"\",\n            modifier = Modifier.size(14.dp)\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/SeekBar.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Paint\nimport androidx.compose.ui.graphics.PaintingStyle\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.input.pointer.*\nimport androidx.compose.ui.unit.dp\nimport ui.common.theme.AppColorsProvider\n\n/**\n * Created by ssk on 2022/4/23.\n */\nprivate val progressPaint = Paint().apply {\n    isAntiAlias = true\n    style = PaintingStyle.Fill\n}\n\nprivate val circlePaint = Paint().apply {\n    isAntiAlias = true\n    style = PaintingStyle.Fill\n}\n\nvar width = 0f\nvar height = 0f\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun SeekBar(\n    progress: Float = 0f,\n    enableSeek: Boolean = false,\n    seeking: (Float) -> Unit = {},\n    seekTo: (Float) -> Unit = {},\n    smallRadius: Float = 8.dp.value,\n    largeRadius: Float = 12.dp.value,\n    progressHeight: Float = 4.dp.value,\n    seekBarColor: Color = Color.LightGray.copy(0.3f),\n    progressColor: Color = AppColorsProvider.current.primary,\n    circleColor: Color = Color.LightGray,\n    modifier: Modifier = Modifier\n) {\n\n    var isPressed by remember {\n        mutableStateOf(false)\n    }\n\n    var circleCenterX by remember {\n        mutableStateOf(0f)\n    }\n\n    circlePaint.color = circleColor\n\n    Box(\n        modifier = modifier\n            .onPointerEvent(PointerEventType.Press) {\n                if (enableSeek) {\n                    isPressed = true\n                    val x = it.changes.first().position.x\n                    seeking.invoke(x * 100f / width)\n                }\n            }\n            .onPointerEvent(PointerEventType.Move) {\n                if (isPressed) {\n                    val x = it.changes.first().position.x\n                    circleCenterX = x\n\n                    if (x < 0f) {\n                        circleCenterX = 0f\n                    } else if (x > width) {\n                        circleCenterX = width\n                    } else {\n                        circleCenterX = x\n                    }\n                    seeking.invoke(circleCenterX * 100f / width)\n                }\n            }\n            .onPointerEvent(PointerEventType.Release) {\n                if (enableSeek) {\n                    seekTo.invoke(circleCenterX * 100f / width)\n                    isPressed = false\n                }\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Canvas(\n            modifier = Modifier\n                .fillMaxWidth()\n        ) {\n            width = drawContext.size.width\n            height = drawContext.size.height\n            drawIntoCanvas {\n                progressPaint.color = seekBarColor\n                val seekBarRect = Rect(\n                    Offset(0f, (height - progressHeight) / 2),\n                    Offset(width, (height + progressHeight) / 2)\n                )\n                it.drawRect(seekBarRect, progressPaint)\n\n                progressPaint.color = progressColor\n                val progressRect = Rect(\n                    Offset(0f, (height - progressHeight) / 2),\n                    Offset(width * progress / 100, (height + progressHeight) / 2)\n                )\n                it.drawRect(progressRect, progressPaint)\n\n                var x = width * progress / 100\n                val radius = if (isPressed) largeRadius else smallRadius\n                if (x < radius) {\n                    x = radius\n                } else if (x > width - radius) {\n                    x = width - radius\n                }\n                it.drawCircle(\n                    Offset(x, height / 2),\n                    radius,\n                    circlePaint\n                )\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/TableLayout.kt",
    "content": "package ui.common\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.Layout\n\n/**\n * Created by ssk on 2022/4/18.\n */\n@Composable\nfun TableLayout(\n    modifier: Modifier = Modifier,\n    cellsCount: Int,\n    content: @Composable () -> Unit\n) {\n    Layout(\n        content = content,\n        modifier = modifier,\n    ) { measurables, constraints ->\n        val parentWidth = constraints.maxWidth\n        val cellWidth = parentWidth / cellsCount\n        var totalHeight = 0\n        val cellsHeightPerRow = mutableListOf<Int>()\n        val rowHeights = mutableListOf<Int>()\n        val placeables = measurables.mapIndexed { index, measurable ->\n            val newConstraints = constraints.copy(minWidth = cellWidth, maxWidth = cellWidth)\n            val placeable = measurable.measure(newConstraints)\n            val childWidth = placeable.width\n            val childHeight = placeable.height\n            cellsHeightPerRow.add(childHeight)\n            if (cellsHeightPerRow.size == cellsCount || index == measurables.size - 1) {\n                var maxChildHeight = 0\n                cellsHeightPerRow.forEach {\n                    if (it > maxChildHeight)\n                        maxChildHeight = it\n                }\n                totalHeight += maxChildHeight\n                rowHeights.add(maxChildHeight)\n                cellsHeightPerRow.clear()\n            }\n            placeable\n        }\n        layout(parentWidth, totalHeight) {\n            placeables.forEachIndexed { index, placeable ->\n                val column = index % cellsCount\n                val row = index / cellsCount\n                val positionX = cellWidth * column\n                var positionY = 0\n                for (i in 0 until row) {\n                    positionY += rowHeights[i]\n                }\n                placeable.placeRelative(positionX, positionY)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/ViewStateComponent.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.google.gson.JsonParseException\nimport moe.tlaster.precompose.ui.viewModel\nimport moe.tlaster.precompose.viewmodel.ViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\nimport java.net.ConnectException\nimport java.net.SocketTimeoutException\nimport java.net.UnknownHostException\nimport java.util.UUID\n\n/**\n * Description->页面状态切换组件, 根据viewStateLiveData，自动切换各种状态页面\n * @param modifier：页面布局修饰\n * @param key: 用于维护ViewStateComponentViewModel中的flow,避免页面重建后，数据重新加载\n * @param initFlow: 初始化数据流，使用场景：某页面有需要显示popup，popup的内容来自网络，而且进入页面后可以预加载数据，等到用户点击显示popup时，数据以及加载完成\n * @param loadDataBlock：数据加载块\n * @param viewStateComponentModifier: 状态页面修饰\n * @param viewStateContentAlignment：状态页面居中方式\n * @param customEmptyComponent：自定义加载中布局,没设置则使用默认加载中布局\n * @param customEmptyComponent：自定义空布局,没设置则使用默认空布局\n * @param customFailComponent：自定义失败布局,没设置则使用默认失败布局\n * @param customErrorComponent：自定义错误布局,没设置则使用默认错误布局\n * @param contentView：正常页面内容\n */\n@Composable\nfun <T> ViewStateComponent(\n    modifier: Modifier = Modifier,\n    key: String = UUID.randomUUID().toString(),\n    initFlow: ViewStateMutableStateFlow<T>? = null,\n    loadDataBlock: (() -> ViewStateMutableStateFlow<T>),\n    viewStateComponentModifier: Modifier = Modifier.fillMaxWidth().heightIn(min = 320.dp),\n    viewStateContentAlignment: Alignment = Alignment.Center,\n    customLoadingComponent: @Composable (() -> Unit)? = null,\n    customEmptyComponent: @Composable (() -> Unit)? = null,\n    customFailComponent: @Composable ((errorMessage: String?, loadDataBlock: () -> Unit) -> Unit)? = null,\n    customErrorComponent: @Composable ((errorMessage: Pair<String, String>, loadDataBlock: () -> Unit) -> Unit)? = null,\n    contentView: @Composable BoxScope.(data: T) -> Unit\n) {\n\n    val vm = viewModel(listOf(key)) { ViewStateComponentViewModel<T>() }\n    val reloadFlag = vm.reloadFlag\n\n    val flow = remember(reloadFlag, key) {\n        if (reloadFlag == 0) {  // first load data\n            if (vm.flow == null) {\n                vm.flow = initFlow ?: loadDataBlock.invoke()\n            }\n        } else {  // retry load data when user trigger loadDataBlock\n            vm.flow = loadDataBlock.invoke()\n        }\n        vm.flow!!\n    }\n    val viewState by flow.collectAsState()\n    Box(\n        modifier = modifier.fillMaxWidth(),\n        contentAlignment = Alignment.Center\n    ) {\n        when (viewState) {\n            is ViewState.Loading -> {\n                if (customLoadingComponent != null) {\n                    customLoadingComponent.invoke()\n                } else {\n                    LoadingComponent(\n                        modifier = viewStateComponentModifier,\n                        contentAlignment = viewStateContentAlignment\n                    )\n                }\n            }\n\n            is ViewState.Success -> {\n                contentView((viewState as ViewState.Success<T>).data!!)\n            }\n\n            is ViewState.Empty -> {\n                if (customEmptyComponent != null) {\n                    customEmptyComponent.invoke()\n                } else {\n                    NoSuccessComponent(\n                        contentAlignment = viewStateContentAlignment,\n                        modifier = viewStateComponentModifier,\n                    ) {\n                        vm.reload()\n                    }\n                }\n            }\n\n            is ViewState.Fail -> {\n                if (customFailComponent != null) {\n                    customFailComponent.invoke(\n                        \"错误码：${(viewState as ViewState.Fail).errorCode}；${(viewState as ViewState.Fail).errorMsg}，点我重试\",\n                    ) {\n                        vm.reload()\n                    }\n                } else {\n                    NoSuccessComponent(\n                        modifier = viewStateComponentModifier,\n                        message = \"错误码：${(viewState as ViewState.Fail).errorCode}；${(viewState as ViewState.Fail).errorMsg}，点我重试\",\n                        contentAlignment = viewStateContentAlignment\n                    ) {\n                        vm.reload()\n                    }\n                }\n            }\n\n            is ViewState.Error -> {\n                if (customErrorComponent != null) {\n                    customErrorComponent.invoke(\n                        getErrorMessagePair((viewState as ViewState.Error).exception),\n                    )  {\n                        vm.reload()\n                    }\n                } else {\n                    val errorMessagePair = getErrorMessagePair((viewState as ViewState.Error).exception)\n                    NoSuccessComponent(\n                        modifier = viewStateComponentModifier,\n                        message = errorMessagePair.first,\n                        iconResId = errorMessagePair.second,\n                        contentAlignment = viewStateContentAlignment,\n                    ) {\n                        vm.reload()\n                    }\n                }\n            }\n        }\n    }\n}\n\nclass ViewStateComponentViewModel<T> : ViewModel() {\n    var flow: ViewStateMutableStateFlow<T>? = null\n    var reloadFlag by mutableStateOf(0)\n        private set\n\n    fun reload() {\n        reloadFlag++\n    }\n}\n\n\n\nfun getErrorMessagePair(exception: Throwable): Pair<String, String> {\n    return when (exception) {\n        is ConnectException,\n        is UnknownHostException -> {\n            Pair(\"网络连接失败\", \"image/ic_network_error.xml\")\n        }\n\n        is SocketTimeoutException -> {\n            Pair(\"网络连接超时\", \"image/ic_network_error.xml\")\n        }\n\n        is JsonParseException -> {\n            Pair(\"数据解析错误\", \"image/ic_network_error.xml\")\n        }\n\n        else -> {\n            Pair(\"未知错误\", \"image/ic_network_error.xml\")\n        }\n    }\n}\n\n\n@Composable\nfun <T> ViewState<T>.handleSuccess(callback: @Composable (data: T) -> Unit) {\n    if (this is ViewState.Success) {\n        callback.invoke(this.data)\n    }\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/ViewStateLazyGridPagingComponent.kt",
    "content": "package ui.common\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.grid.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.layout.Placeable\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport model.IBasePagingBean\nimport moe.tlaster.precompose.ui.viewModel\nimport moe.tlaster.precompose.viewmodel.ViewModel\nimport ui.playlist.cpn.CommentListViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\nimport java.util.*\n\n@Composable\nfun <T : IBasePagingBean> ViewStateLazyGridPagingComponent(\n    modifier: Modifier = Modifier,\n    key: String = UUID.randomUUID().toString(),\n    columns: Int,\n    pageSize: Int = 20,\n    loadDataBlock: (pageSize: Int, curPage: Int) -> ViewStateMutableStateFlow<T>,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    scrollHeader: (@Composable () -> Unit)? = null,\n    stickyHeader: (@Composable () -> Unit)? = null,\n    viewStateComponentModifier: Modifier = Modifier.fillMaxWidth().heightIn(min = 320.dp),\n    viewStateContentAlignment: Alignment = Alignment.Center,\n    customLoadingComponent: @Composable (() -> Unit)? = null,\n    customEmptyComponent: @Composable (() -> Unit)? = null,\n    customFailComponent: @Composable ((errorMessage: String?, loadDataBlock: () -> Unit) -> Unit)? = null,\n    customErrorComponent: @Composable ((errorMessage: Pair<String, String>, loadDataBlock: () -> Unit) -> Unit)? = null,\n    gridContent: LazyGridScope.(data: T) -> Unit,\n) {\n\n    var height by remember { mutableStateOf(0) }\n\n    SubComposeLazyList(\n        modifier = Modifier.onGloballyPositioned {\n            height = it.size.height\n        },\n        scrollHeader, stickyHeader\n    ) { scrollHeaderHeight, stickyHeaderHeight ->\n        val localDensity = LocalDensity.current\n        // fix 当有scrollHeader和stickyHeader时，当列表显示滑动，并且通过stickyHeader切换数据源，造成列表滚动的bug\n        val viewStateComponentModifier = remember(height) {\n            if (height == 0) {\n                viewStateComponentModifier\n            } else {\n                viewStateComponentModifier.height(height = ((height + scrollHeaderHeight + stickyHeaderHeight) / localDensity.density).dp)\n            }\n        }\n\n        val vm = viewModel(listOf(key)) { ViewStateLazyListViewModel<T>() }\n        val reloadFlag = vm.reloadFlag\n\n        val flow = remember(reloadFlag, key) {\n            if (reloadFlag == 0) {  // first load data\n                if (vm.flow == null) {\n                    vm.flow = loadDataBlock.invoke(pageSize, vm.curPage.value)\n                }\n            } else {  // retry load data when user trigger loadDataBlock\n                vm.flow = loadDataBlock.invoke(pageSize, vm.curPage.value)\n            }\n            vm.flow!!\n        }\n\n        val viewState by flow.collectAsState()\n\n        var showStickyHeader by remember { mutableStateOf(false) }\n        val firstVisibleItemIndex by remember { derivedStateOf { lazyGridState.firstVisibleItemIndex } }\n\n        LaunchedEffect(Unit) {\n            snapshotFlow { firstVisibleItemIndex }\n                .collect { firstVisibleItemIndex ->\n                    if (stickyHeader != null) {\n                        val minShowStickyIndex = if (scrollHeader == null) 0 else 1\n                        showStickyHeader = firstVisibleItemIndex >= minShowStickyIndex\n                    }\n                }\n        }\n\n        Box {\n            LazyVerticalGrid(\n                GridCells.Fixed(columns),\n                modifier,\n                lazyGridState,\n                contentPadding\n            ) {\n                if (scrollHeader != null) {\n                    item(span = { GridItemSpan(columns) }) { scrollHeader() }\n                }\n                if (stickyHeader != null) {\n                    item(span = { GridItemSpan(columns) }) {\n                        stickyHeader()\n                    }\n                }\n                when (viewState) {\n                    is ViewState.Loading -> {\n                        item(span = { GridItemSpan(columns) }) {\n                            if (customLoadingComponent != null) {\n                                customLoadingComponent.invoke()\n                            } else {\n                                LoadingComponent(\n                                    modifier = viewStateComponentModifier,\n                                    contentAlignment = viewStateContentAlignment\n                                )\n                            }\n                        }\n                    }\n\n\n                    is ViewState.Empty -> {\n                        item(span = { GridItemSpan(columns) }) {\n                            if (customEmptyComponent != null) {\n                                customEmptyComponent.invoke()\n                            } else {\n                                NoSuccessComponent(\n                                    contentAlignment = viewStateContentAlignment,\n                                    modifier = viewStateComponentModifier,\n                                ) {\n                                    vm.reload()\n                                }\n                            }\n                        }\n                    }\n\n                    is ViewState.Fail -> {\n                        item(span = { GridItemSpan(columns) }) {\n                            if (customFailComponent != null) {\n                                customFailComponent.invoke(\n                                    \"错误码：${(viewState as ViewState.Fail).errorCode}；${(viewState as ViewState.Fail).errorMsg}，点我重试\",\n                                ) {\n                                    vm.reload()\n                                }\n                            } else {\n                                NoSuccessComponent(\n                                    modifier = viewStateComponentModifier,\n                                    message = \"错误码：${(viewState as ViewState.Fail).errorCode}；${(viewState as ViewState.Fail).errorMsg}，点我重试\",\n                                    contentAlignment = viewStateContentAlignment\n                                ) {\n                                   vm.reload()\n                                }\n                            }\n                        }\n                    }\n\n                    is ViewState.Error -> {\n                        item(span = { GridItemSpan(columns) }) {\n                            if (customErrorComponent != null) {\n                                customErrorComponent.invoke(\n                                    getErrorMessagePair((viewState as ViewState.Error).exception),\n                                ) {\n                                    vm.reload()\n                                }\n                            } else {\n                                val errorMessagePair = getErrorMessagePair((viewState as ViewState.Error).exception)\n                                NoSuccessComponent(\n                                    modifier = viewStateComponentModifier,\n                                    message = errorMessagePair.first,\n                                    iconResId = errorMessagePair.second,\n                                    contentAlignment = viewStateContentAlignment,\n                                ) {\n                                    vm.reload()\n                                }\n                            }\n                        }\n                    }\n\n                    is ViewState.Success -> {\n                        val data = (viewState as ViewState.Success<T>).data\n                        gridContent(data)\n\n                        // 底部分页组件\n                        if (data.getTotalCount() > CommentListViewModel.pageSize) {\n                            item(span = { GridItemSpan(columns) }) {\n                                PaingFooterNumBar(data.getTotalCount(), pageSize, vm.curPage.value) {\n                                    vm.curPage.value = it\n                                    vm.reload()\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            if (showStickyHeader) {\n                Box(\n                    modifier = Modifier.padding(\n                        start = contentPadding.calculateLeftPadding(LayoutDirection.Ltr),\n                        end = contentPadding.calculateEndPadding(LayoutDirection.Ltr)\n                    )\n                ) {\n                    stickyHeader?.invoke()\n                }\n            }\n        }\n\n    }\n}\n\nclass ViewStateLazyListViewModel<T> : ViewModel() {\n    var flow: ViewStateMutableStateFlow<T>? = null\n    var reloadFlag by mutableStateOf(0)\n        private set\n\n    var curPage = mutableStateOf(1)\n\n    fun reload() {\n        reloadFlag++\n    }\n}\n\n\n@Composable\nprivate fun SubComposeLazyList(\n    modifier: Modifier,\n    scrollHeader: (@Composable () -> Unit)? = null,\n    stickyHeader: (@Composable () -> Unit)? = null,\n    content: @Composable (scrollHeader: Float, stickyHeader: Float) -> Unit\n) {\n    SubcomposeLayout(\n        modifier = modifier\n            .clipToBounds()\n    ) { constraints ->\n        var scrollHeaderPlaceable: Placeable? = null\n        scrollHeader?.let {\n            scrollHeaderPlaceable = subcompose(\"scrollHeader\", scrollHeader).first().measure(constraints)\n        }\n        var stickyHeaderPlaceable: Placeable? = null\n        stickyHeader?.let {\n            stickyHeaderPlaceable = subcompose(\"stickyHeader\", stickyHeader).first().measure(constraints)\n        }\n        val contentPlaceable = subcompose(\"content\") {\n            content(\n                scrollHeaderPlaceable?.height?.toFloat() ?: 0f,\n                stickyHeaderPlaceable?.height?.toFloat() ?: 0f,\n            )\n        }.map {\n            it.measure(constraints)\n        }.first()\n\n        layout(contentPlaceable.width, contentPlaceable.height) {\n            contentPlaceable.placeRelative(0, 0)\n        }\n    }\n}\n\n\nfun <T> LazyListScope.handleListContent(\n    viewState:  ViewState<T>?,\n    reloadDataBlock: () -> Unit,\n    viewStateComponentModifier: Modifier = Modifier.fillMaxWidth().heightIn(min = 320.dp),\n    callback: LazyListScope.(data: T) -> Unit,\n) {\n\n    when (viewState) {\n        is ViewState.Empty -> {\n            item {\n                NoSuccessComponent(\n                    modifier = viewStateComponentModifier,\n                ) {\n                    reloadDataBlock.invoke()\n                }\n            }\n        }\n\n        is ViewState.Fail -> {\n            item {\n                NoSuccessComponent(\n                    modifier = viewStateComponentModifier,\n                    message = \"错误码：${viewState.errorCode}；${viewState.errorMsg}，点我重试\",\n                ) {\n                   reloadDataBlock.invoke()\n                }\n            }\n        }\n\n        is ViewState.Error -> {\n            item {\n                val errorMessagePair = getErrorMessagePair(viewState.exception)\n                NoSuccessComponent(\n                    modifier = viewStateComponentModifier,\n                    message = errorMessagePair.first,\n                    iconResId = errorMessagePair.second,\n                ) {\n                    reloadDataBlock.invoke()\n\n                }\n            }\n        }\n\n        is ViewState.Success -> {\n            val data = viewState.data\n            callback(data)\n        }\n         else -> {\n             item {\n                 LoadingComponent(viewStateComponentModifier)\n             }\n         }\n    }\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/common/toast/Toast.kt",
    "content": "package ui.common.toast\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.BiasAlignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.AppColorsProvider\nimport java.util.concurrent.ConcurrentLinkedQueue\n\nobject ToastManager {\n\n    const val LENGTH_SHORT = 3000L\n    const val LENGTH_LONG = 5000L\n\n    var toastMessage by mutableStateOf<ToastMessage?>(null)\n    private val messageQueue = ConcurrentLinkedQueue<ToastMessage>()\n\n    init {\n        val loopThread = Thread {\n            while (true) {\n                Thread.sleep(50)\n                if (!messageQueue.isEmpty()) {\n                    val nextToastMessage = messageQueue.poll()\n                    if (nextToastMessage != null) {\n                        toastMessage = nextToastMessage\n                        Thread.sleep(nextToastMessage.during)\n                    }\n                } else {\n                    toastMessage = null\n                }\n            }\n        }\n        loopThread.start()\n    }\n\n    fun showToast(message: String?, during: Long = LENGTH_SHORT) {\n        message?.let { msg ->\n            if (messageQueue.size > 5) {\n                messageQueue.clear()\n            }\n            messageQueue.add(ToastMessage(message, during))\n        }\n    }\n}\n\ndata class ToastMessage(val message: String, val during: Long)\n\n@Composable\nfun BoxScope.Toast() {\n    ToastManager.toastMessage?.message?.let {msg ->\n        Box(\n            modifier = Modifier.padding(horizontal = 50.dp).align(BiasAlignment(0f, 0.75f))\n                .border(BorderStroke(1.dp, AppColorsProvider.current.divider), RoundedCornerShape(6.dp))\n                .background(AppColorsProvider.current.pure.copy(0.9f), RoundedCornerShape(6.dp))\n                .padding(horizontal = 20.dp, vertical = 8.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(msg, color = AppColorsProvider.current.firstText, fontSize = 14.sp, textAlign = TextAlign.Center)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/DiscoveryPage.kt",
    "content": "package ui.discovery\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport base.MusicPlayController\nimport moe.tlaster.precompose.ui.viewModel\nimport moe.tlaster.precompose.viewmodel.ViewModel\nimport router.NCNavigatorManager\nimport ui.common.CommonTabLayout\nimport ui.common.CommonTabLayoutStyle\nimport ui.common.theme.AppColorsProvider\nimport ui.discovery.cpn.CpnPersonalRecommendContainer\nimport ui.discovery.cpn.CpnRecommendPlayList\nimport ui.main.cpn.CommonTitleBar\nimport ui.todo.TodoPage\n\n/**\n * 发现音乐页面\n */\n@Composable\nfun DiscoveryPage() {\n    val tabs = remember {\n        listOf(\"个性推荐\", \"歌单\", \"排行榜\", \"歌手\", \"最新音乐\")\n    }\n    val viewModel = viewModel { DiscoveryPageViewModel() }\n    Column {\n        CommonTitleBar {\n            CommonTabLayout(\n                selectedIndex = viewModel.selectedIndex.value,\n                tabTexts = tabs,\n                backgroundColor = if (MusicPlayController.showMusicPlayDrawer) AppColorsProvider.current.pure else AppColorsProvider.current.topBarColor,\n                style = CommonTabLayoutStyle(modifier = Modifier.height(50.dp))\n            ) {\n                viewModel.selectedIndex.value = it\n            }\n        }\n\n        when (viewModel.selectedIndex.value) {\n            0 -> CpnPersonalRecommendContainer(viewModel.selectedIndex)\n            1 -> CpnRecommendPlayList()\n            else -> TodoPage(tabs[viewModel.selectedIndex.value], false)\n        }\n    }\n\n}\n\nclass DiscoveryPageViewModel : ViewModel() {\n    val selectedIndex = mutableStateOf(0)\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnHighQualityPlayListEntrance.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport http.NCRetrofitClient\nimport model.PlayListResult\nimport model.PlaylistDetail\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.AsyncImage\nimport ui.common.handleSuccess\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport base.ViewStateMutableStateFlow\n\n/**\n * 个性推荐-精品歌单-入口\n */\n@Composable\nfun CpnHighQualityPlayListEntrance(tag: String) {\n    val highQualityPlayListEntranceViewModel = viewModel { HighQualityPlayListEntranceViewModel() }\n    val flow = remember(tag) {\n        highQualityPlayListEntranceViewModel.getHighQualityPlayList(tag)\n    }\n    Box(\n        modifier = Modifier.fillMaxWidth().height(200.dp)\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(6.dp))\n            .background(AppColorsProvider.current.divider.copy(0.2f), RoundedCornerShape(6.dp))\n    ) {\n        flow?.collectAsState()?.value?.handleSuccess {\n            Content(it.playlists.getOrNull(0))\n        }\n    }\n\n}\n\n@Composable\nprivate fun Content(playlistBean: PlaylistDetail?) {\n    playlistBean?.let {\n        Box(Modifier.fillMaxWidth().clip(RoundedCornerShape(6.dp))) {\n            AsyncImage(\n                modifier = Modifier.fillMaxSize().blur(80.dp),\n                playlistBean.coverImgUrl,\n            )\n\n            Row(\n                modifier = Modifier.fillMaxSize().padding(15.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n\n                AsyncImage(\n                    modifier = Modifier.padding(end = 20.dp).size(130.dp).clip(RoundedCornerShape(6.dp)),\n                    playlistBean.coverImgUrl\n                )\n\n                Column {\n                    Row(\n                        modifier = Modifier.width(100.dp).height(30.dp)\n                            .border(BorderStroke(1.dp, color = Color(0xFFD8B839)), RoundedCornerShape(50)),\n                        horizontalArrangement = Arrangement.Center,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Icon(\n                            painterResource(\"image/ic_queue.webp\"),\n                            modifier = Modifier.padding(end = 6.dp).size(16.dp),\n                            contentDescription = \"\",\n                            tint = Color(0xFFD8B839)\n                        )\n                        Text(\"精品歌单\", color = Color(0xFFD8B839), fontSize = 12.sp)\n                    }\n\n                    Text(\n                        playlistBean.name,\n                        color = AppColorsProvider.current.pure,\n                        fontSize = 14.sp,\n                        modifier = Modifier.padding(top = 16.dp)\n                    )\n                    Text(\n                        playlistBean.description ?: \"\", color = AppColorsProvider.current.pure,\n                        fontSize = 12.sp, modifier = Modifier.padding(top = 8.dp),\n                        maxLines = 2,\n                        overflow = TextOverflow.Ellipsis\n                    )\n\n                }\n            }\n        }\n    }\n}\n\nclass HighQualityPlayListEntranceViewModel : BaseViewModel() {\n    var flow: ViewStateMutableStateFlow<PlayListResult>? = null\n    var lastTag = \"\"\n    fun getHighQualityPlayList(tag: String?): ViewStateMutableStateFlow<PlayListResult>? {\n        if (lastTag != tag) {\n            lastTag = tag ?: \"\"\n            flow = launchFlow {\n                println(\"----getHighQualityPlayList done, lastTag=${lastTag}\")\n                NCRetrofitClient.getNCApi().getHighQualityPlayList(1, tag)\n            }\n        }\n        return flow\n    }\n\n}\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnNewSongEntrance.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Divider\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport http.NCRetrofitClient\nimport model.NewSongBean\nimport model.NewSongResult\nimport ui.common.*\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\n\n/**\n * 个性推荐-最新音乐入口\n */\nfun LazyListScope.CpnNewSongEntrance(viewModel: NewSongEntranceViewModel,\n                                     viewState: ViewState<NewSongResult>?) {\n\n    item {\n        CpnActionMore(\"最新音乐\")\n    }\n\n    handleListContent(viewState, reloadDataBlock = {\n        viewModel.getNewSong(false)\n    }) { data ->\n        ListToGridItems(data.data, 2) { index, item ->\n            NewSongItem(index, item)\n        }\n    }\n}\n\n@Composable\nprivate fun NewSongItem(index: Int, item: NewSongBean) {\n\n\n    Column(verticalArrangement = Arrangement.Center) {\n        Divider(\n            modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),\n            thickness = 0.5.dp,\n            color = AppColorsProvider.current.divider\n        )\n        Row(\n            modifier = Modifier.padding(horizontal = 6.dp), verticalAlignment = Alignment.CenterVertically\n        ) {\n            Box {\n\n                AsyncImage(modifier = Modifier.padding(vertical = 10.dp).size(72.dp).clip(RoundedCornerShape(6.dp)), item.album.picUrl)\n\n                Icon(\n                    painter = painterResource(\"image/ic_logo_play.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.size(28.dp).align(Alignment.Center),\n                    tint = Color.White\n                )\n            }\n\n            val num = if (index < 10) \"0$index\" else \"$index\"\n            Text(num, color = AppColorsProvider.current.thirdText, fontSize = 12.sp, modifier = Modifier.padding(12.dp))\n\n            Column {\n                Text(\n                    item.name,\n                    color = AppColorsProvider.current.firstText,\n                    fontSize = 14.sp,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Text(\n                    item.artists[0].name,\n                    color = AppColorsProvider.current.secondText,\n                    fontSize = 12.sp,\n                    modifier = Modifier.padding(top = 6.dp),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n\n            }\n\n        }\n        Divider(\n            modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp),\n            thickness = 0.5.dp,\n            color = AppColorsProvider.current.divider\n        )\n    }\n}\n\nclass NewSongEntranceViewModel : BaseViewModel() {\n\n    var flow by mutableStateOf<ViewStateMutableStateFlow<NewSongResult>?>(null)\n    fun getNewSong(firstLoad: Boolean)  {\n        if (!firstLoad || flow == null) {\n            flow = launchFlow(handleSuccessBlock = {\n                it.data = it.data.take(10)\n            }) {\n                println(\"获取新歌速递...\")\n                NCRetrofitClient.getNCApi().getNewSong()\n            }\n        }\n    }\n\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnPersonalRecommend.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport moe.tlaster.precompose.ui.viewModel\nimport base.BaseViewModel\n\n/**\n * 个性推荐\n */\n@Composable\nfun CpnPersonalRecommendContainer(recommendTagIndex: MutableState<Int>) {\n    val personalRecommendViewModel = viewModel { PersonalRecommendViewModel() }\n    val playListEntranceViewModel = viewModel { RecommendPlayListEntranceViewModel() }\n    val privateContentViewModel = viewModel { PrivateContentViewModel() }\n    val newSongViewModel = viewModel { NewSongEntranceViewModel() }\n    val recommendMVEntranceViewModel = viewModel { RecommendMVEntranceViewModel() }\n\n    LaunchedEffect(Unit) {\n        playListEntranceViewModel.getRecommendPlayList(true)\n        privateContentViewModel.getPrivateContent(true)\n        newSongViewModel.getNewSong(true)\n        recommendMVEntranceViewModel.getRecommendMV(true)\n    }\n    val playListEntranceViewState = playListEntranceViewModel.flow?.collectAsState()?.value\n    val privateContentViewState = privateContentViewModel.flow?.collectAsState()?.value\n    val newSongViewState = newSongViewModel.flow?.collectAsState()?.value\n    val recommendMVViewState = recommendMVEntranceViewModel.flow?.collectAsState()?.value\n\n    LazyColumn(\n        modifier = Modifier.padding(horizontal = 20.dp),\n        state = personalRecommendViewModel.getLazyListStateState(rememberLazyListState())\n    ) {\n        // 推荐歌单\n        CpnRecommandPlayListEntrance(playListEntranceViewModel, playListEntranceViewState) {\n            recommendTagIndex.value = 1\n        }\n\n        // 独家放送\n        CpnPrivateContentEntrance(privateContentViewModel, privateContentViewState)\n\n\n        // 最新音乐\n        CpnNewSongEntrance(newSongViewModel, newSongViewState)\n\n        // 推荐MV\n        CpnRecommendMVEntrance(recommendMVEntranceViewModel, recommendMVViewState)\n    }\n}\n\n\nclass PersonalRecommendViewModel : BaseViewModel() {\n    private var lazyListState: LazyListState? = null\n\n    fun getLazyListStateState(state: LazyListState): LazyListState {\n        if (lazyListState == null) {\n            lazyListState = state\n        }\n        return lazyListState!!\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnPlayListItem.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.Image\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.google.gson.Gson\nimport model.PlaylistDetail\nimport moe.tlaster.precompose.navigation.NavOptions\nimport router.NCNavigatorManager\nimport router.RouterUrls\nimport ui.common.AsyncImage\nimport ui.common.theme.AppColorsProvider\nimport util.StringUtil\n\n/**\n * 歌单item组件\n */\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun CpnPlayListItem(item: PlaylistDetail) {\n    var focusState by remember { mutableStateOf(false) }\n    val navigator = NCNavigatorManager.navigator\n\n    Column(\n        modifier = Modifier.onPointerEvent(PointerEventType.Enter) {\n            focusState = true\n        }.onPointerEvent(PointerEventType.Exit) {\n            focusState = false\n        }.onClick  {\n            val url = \"${RouterUrls.PLAY_LIST_DETAIL}?simplePlayListInfo=${Gson().toJson(item.convertToSimple())}\"\n            navigator.navigate(url)\n        },\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Box {\n            AsyncImage(modifier = Modifier.size(172.dp).clip(RoundedCornerShape(6.dp)), item.coverImgUrl)\n\n            Row(\n                modifier = Modifier.padding(top = 6.dp, end = 6.dp).align(Alignment.TopEnd),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Image(\n                    painter = painterResource(\"image/ic_play_count.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(end = 6.dp).size(12.dp)\n                )\n                Text(\n                    StringUtil.friendlyNumber(item.playCount),\n                    color = Color.White,\n                    fontSize = 12.sp,\n                )\n            }\n\n            if (focusState) {\n                Icon(\n                    painter = painterResource(\"image/ic_logo_play.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(bottom = 6.dp, end = 6.dp).size(32.dp).align(Alignment.BottomEnd),\n                    tint = Color.White\n                )\n            }\n        }\n\n        Text(\n            item.name,\n            color = AppColorsProvider.current.firstText,\n            fontSize = 12.sp,\n            maxLines = 2,\n            modifier = Modifier.padding(top = 10.dp, start = 16.dp, end = 16.dp).height(48.dp)\n        )\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnPlayListTabSelectedBar.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.TableLayout\nimport http.NCRetrofitClient\nimport model.PlayListTab\nimport model.PlayListTabResult\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.ViewStateComponent\nimport ui.common.handleSuccess\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport ui.common.onClick\n\n/**\n * 歌单详情-歌单标签tab组件\n */\n@Composable\nfun CpnPlayListTabSelectedBar() {\n    val viewModel = viewModel { PlayListTabSelectedBarViewModel() }\n    val showTabsPopup = remember { mutableStateOf(false) }\n    Row(modifier = Modifier.background(AppColorsProvider.current.pure).padding(vertical = 16.dp).fillMaxWidth()) {\n        PlayListTabToggle(showTabsPopup)\n        HotPlayListTabs(viewModel)\n    }\n    TabsPopup(showTabsPopup)\n}\n\n\n@Composable\nprivate fun TabsPopup(showTabsPopup: MutableState<Boolean>) {\n    CursorDropdownMenu(\n        expanded = showTabsPopup.value,\n        onDismissRequest = {\n            showTabsPopup.value = false\n        },\n        //offset = DpOffset(20.dp, 10.dp),\n    ) {\n        TabsPopupContent(showTabsPopup)\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun TabsPopupContent(showTabsPopup: MutableState<Boolean>) {\n    val viewModel = viewModel { PlayListTabSelectedBarViewModel() }\n\n    ViewStateComponent(modifier = Modifier.width(660.dp).height(320.dp),\n        initFlow = viewModel.playListTabFlow,\n        key = \"TabsPopupContent\",\n        loadDataBlock = { viewModel.getPlayListCategories() }) { data ->\n\n        val groupTabsMap = remember { viewModel.generateGroupTabsMap(data) }\n        LazyColumn {\n            stickyHeader {\n                PlayListTabItem(\n                    modifier = Modifier.padding(start = 20.dp, bottom = 15.dp, top = 6.dp).height(32.dp),\n                    showTabsPopup,\n                    viewModel = viewModel,\n                    textSize = 13.sp,\n                    tag = data.all\n                )\n                Divider(modifier = Modifier.fillMaxWidth(), thickness = 1.dp, color = AppColorsProvider.current.divider)\n            }\n            item {\n                groupTabsMap.forEach {\n                    TabsPopupGroupTabsItem(showTabsPopup, viewModel, it.key, it.value)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TabsPopupGroupTabsItem(\n    showTabsPopup: MutableState<Boolean>,\n    viewModel: PlayListTabSelectedBarViewModel,\n    category: String,\n    tabs: List<PlayListTab>\n) {\n    Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 10.dp)) {\n        Text(\n            category,\n            modifier = Modifier.padding(end = 20.dp, top = 10.dp),\n            color = AppColorsProvider.current.thirdText,\n            fontSize = 12.sp\n        )\n        TableLayout(cellsCount = 6, modifier = Modifier.fillMaxWidth()) {\n            tabs.forEach {\n                PlayListTabItem(\n                    modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp).height(30.dp),\n                    showTabsPopup,\n                    viewModel = viewModel,\n                    tag = it\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PlayListTabToggle(showTabsPopup: MutableState<Boolean>) {\n    val viewModel = viewModel { PlayListTabSelectedBarViewModel() }\n\n    Row(\n        modifier = Modifier.padding(end = 20.dp).width(110.dp).height(30.dp).clip(RoundedCornerShape(50)).onClick {\n            showTabsPopup.value = true\n        }.border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(50)),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n\n        Text(\n            viewModel.selectedTab?.name ?: \"选择标签\", color = AppColorsProvider.current.firstIcon, fontSize = 14.sp\n        )\n\n        Icon(\n            painterResource(\"image/ic_more.webp\"),\n            modifier = Modifier.size(16.dp),\n            contentDescription = \"\",\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n}\n\n@Composable\nprivate fun PlayListTabItem(\n    modifier: Modifier,\n    showTabsPopup: MutableState<Boolean>,\n    textSize: TextUnit = 12.sp,\n    viewModel: PlayListTabSelectedBarViewModel,\n    tag: PlayListTab\n) {\n\n    Box(modifier = Modifier.fillMaxWidth().background(AppColorsProvider.current.pure)) {\n        Box(modifier = modifier.clip(RoundedCornerShape(50)).onClick {\n            viewModel.selectedTab = tag\n            showTabsPopup.value = false\n        }.let {\n            if (tag.name == viewModel.selectedTab?.name) {\n                it.background(AppColorsProvider.current.primary.copy(0.2f))\n            } else {\n                it\n            }.padding(horizontal = 6.dp, vertical = 3.dp)\n        }, contentAlignment = Alignment.Center\n        ) {\n            Row {\n                Text(\n                    tag.name,\n                    color = if (tag.name == viewModel.selectedTab?.name) AppColorsProvider.current.primary else AppColorsProvider.current.firstText,\n                    fontSize = textSize,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                if (tag.hot) {\n                    Icon(\n                        painterResource(\"image/ic_hot.webp\"),\n                        contentDescription = null,\n                        modifier = Modifier.padding(start = 2.dp).size(14.dp),\n                        tint = AppColorsProvider.current.primary\n                    )\n\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RowScope.HotPlayListTabs(viewModel: PlayListTabSelectedBarViewModel) {\n    viewModel.hotTabFlow.collectAsState().value.handleSuccess { data ->\n        Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {\n            LazyRow {\n                items(data.tags.size) {\n                    HotPlayListTabItem(viewModel, data.tags[it], it == data.tags.size - 1)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HotPlayListTabItem(viewModel: PlayListTabSelectedBarViewModel, tag: PlayListTab, lastIndex: Boolean) {\n    Row(\n        modifier = Modifier.height(30.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Box(modifier = Modifier.clip(RoundedCornerShape(50)).onClick {\n            viewModel.selectedTab = tag\n        }.let {\n            if (tag.name == viewModel.selectedTab?.name) {\n                it.background(AppColorsProvider.current.primary.copy(0.2f))\n            } else {\n                it\n            }.padding(horizontal = 6.dp, vertical = 3.dp)\n        }) {\n            Text(\n                tag.name,\n                color = if (tag.name == viewModel.selectedTab?.name) AppColorsProvider.current.primary else AppColorsProvider.current.secondText,\n                fontSize = 12.sp\n            )\n        }\n        if (!lastIndex) {\n            Divider(\n                modifier = Modifier.padding(horizontal = 6.dp).width(1.dp),\n                thickness = 12.dp,\n                color = AppColorsProvider.current.divider\n            )\n        }\n    }\n}\n\n\nclass PlayListTabSelectedBarViewModel : BaseViewModel() {\n    var selectedTab by mutableStateOf<PlayListTab?>(null)\n\n    val hotTabFlow by lazy {\n        launchFlow {\n            println(\"hotTabFlow done\")\n            NCRetrofitClient.getNCApi().getHotPlayListCategories()\n        }\n    }\n\n    val playListTabFlow = getPlayListCategories()\n\n    fun getPlayListCategories() = launchFlow(handleSuccessBlock = {\n        if (selectedTab == null) {\n            selectedTab = it.all\n        }\n    }) {\n        println(\"getPlayListCategories done\")\n        NCRetrofitClient.getNCApi().getPlayListCategories()\n    }\n\n    fun generateGroupTabsMap(data: PlayListTabResult): Map<String, MutableList<PlayListTab>> {\n        val categories = data.categories\n        val groupTabsMap = hashMapOf<String, MutableList<PlayListTab>>()\n        categories.forEach {\n            groupTabsMap[it.value] = mutableListOf()\n        }\n        data.sub.forEach { tab ->\n            val categoryId = tab.category\n            val category = data.categories[categoryId]\n            groupTabsMap[category]?.add(tab)\n        }\n        return groupTabsMap\n    }\n\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnPrivateContentEntrance.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport http.NCRetrofitClient\nimport model.PrivateContentItem\nimport model.PrivateContentResult\nimport ui.common.*\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\n\n/**\n * 个性推荐-独家放送入口\n */\nfun LazyListScope.CpnPrivateContentEntrance(viewModel: PrivateContentViewModel,\n                                            viewState: ViewState<PrivateContentResult>?) {\n\n    item {\n        CpnActionMore(\"独家放送\")\n    }\n\n    handleListContent(viewState, reloadDataBlock = {\n        viewModel.getPrivateContent(false)\n    }) { data ->\n        ListToGridItems(data.result, 4) { _, item ->\n            PrivateContentItem(item)\n        }\n    }\n\n}\n\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nprivate fun PrivateContentItem(item: PrivateContentItem) {\n    var focusState by remember { mutableStateOf(false) }\n\n    Column(\n        modifier = Modifier.onPointerEvent(PointerEventType.Enter) {\n            focusState = true\n        }.onPointerEvent(PointerEventType.Exit) {\n           focusState = false\n        },\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Box {\n\n            AsyncImage(\n                modifier = Modifier.width(180.dp).height(100.dp).clip(RoundedCornerShape(6.dp)),\n                item.picUrl\n            )\n            Row(modifier = Modifier.padding(top = 6.dp, end = 6.dp).align(Alignment.TopEnd), verticalAlignment = Alignment.CenterVertically) {\n                Image(\n                    painter = painterResource(\"image/ic_play_count.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(end = 6.dp).size(12.dp)\n                )\n            }\n\n            if (focusState) {\n                Icon(\n                    painter = painterResource(\"image/ic_logo_play.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(top = 6.dp, start = 6.dp).size(32.dp),\n                    tint = Color.White\n                )\n            }\n        }\n\n        Text(\n            item.name,\n            color = AppColorsProvider.current.firstText,\n            fontSize = 12.sp,\n            maxLines = 2,\n            modifier = Modifier.padding(top = 10.dp, start = 16.dp, end = 16.dp).height(48.dp)\n        )\n    }\n}\n\nclass PrivateContentViewModel : BaseViewModel() {\n\n    var flow by mutableStateOf<ViewStateMutableStateFlow<PrivateContentResult>?>(null)\n    fun getPrivateContent(firstLoad: Boolean)  {\n        if (!firstLoad || flow == null) {\n            flow = launchFlow {\n                println(\"获取独家放送...\")\n                NCRetrofitClient.getNCApi().getPrivateContent()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnRecommandPlayListEntrance.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.Image\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.google.gson.Gson\nimport http.NCRetrofitClient\nimport model.RecommendPlayListResult\nimport model.SimplePlayListItem\nimport router.NCNavigatorManager\nimport router.RouterUrls\nimport ui.common.*\nimport ui.common.theme.AppColorsProvider\nimport util.StringUtil\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\nimport moe.tlaster.precompose.navigation.NavOptions\n\n/**\n * 个性推荐-推荐歌单入口\n */\nfun LazyListScope.CpnRecommandPlayListEntrance(viewModel: RecommendPlayListEntranceViewModel,\n                                               viewState: ViewState<RecommendPlayListResult>?,\n                                               onClickMore: () -> Unit) {\n\n    item {\n        CpnActionMore(\"推荐歌单\") {\n            onClickMore.invoke()\n        }\n    }\n\n    handleListContent(viewState, reloadDataBlock = {\n        viewModel.getRecommendPlayList(false)\n    }) { data ->\n        ListToGridItems(data.result, 5) { _, item ->\n            CpnPlayListItem(item)\n        }\n    }\n}\n\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nprivate fun CpnPlayListItem(item: SimplePlayListItem) {\n    var focusState by remember { mutableStateOf(false) }\n\n    Column(\n        modifier = Modifier.onPointerEvent(PointerEventType.Enter) {\n            focusState = true\n        }.onPointerEvent(PointerEventType.Exit) {\n            focusState = false\n        }.onClick  {\n            val url = \"${RouterUrls.PLAY_LIST_DETAIL}?simplePlayListInfo=${Gson().toJson(item)}\"\n            println(\"navigate to PLAY_LIST_DETAIL, url=$url\")\n            NCNavigatorManager.navigator.navigate(url)\n        },\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Box {\n            AsyncImage(\n                modifier = Modifier.size(140.dp).clip(RoundedCornerShape(6.dp)),\n                item.picUrl\n            )\n\n            Row(\n                modifier = Modifier.padding(top = 6.dp, end = 6.dp).align(Alignment.TopEnd),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Image(\n                    painter = painterResource(\"image/ic_play_count.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(end = 6.dp).size(12.dp)\n                )\n                Text(\n                    StringUtil.friendlyNumber(item.playCount),\n                    color = Color.White,\n                    fontSize = 12.sp,\n                )\n            }\n\n            if (focusState) {\n                Icon(\n                    painter = painterResource(\"image/ic_logo_play.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(bottom = 6.dp, end = 6.dp).size(32.dp).align(Alignment.BottomEnd),\n                    tint = Color.White\n                )\n            }\n        }\n\n        Text(\n            item.name,\n            color = AppColorsProvider.current.firstText,\n            fontSize = 12.sp,\n            maxLines = 2,\n            modifier = Modifier.padding(top = 10.dp, start = 16.dp, end = 16.dp).height(48.dp)\n        )\n    }\n}\n\n\nclass RecommendPlayListEntranceViewModel : BaseViewModel() {\n    var flow by mutableStateOf<ViewStateMutableStateFlow<RecommendPlayListResult>?>(null)\n    fun getRecommendPlayList(firstLoad: Boolean)  {\n        if (!firstLoad || flow == null) {\n            flow = launchFlow {\n                println(\"获取推荐歌单...\")\n                NCRetrofitClient.getNCApi().getRecommendPlayList(15)\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnRecommendPlayList.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.BiasAlignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport http.NCRetrofitClient\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.ViewStateLazyGridPagingComponent\nimport base.BaseViewModel\n\n/**\n *  推荐歌单-更多列表组件\n */\n@Composable\nfun CpnRecommendPlayList() {\n    val playListTabSelectedBarViewModel = viewModel { PlayListTabSelectedBarViewModel() }\n    val cpnRecommendPlayListViewModel = viewModel { RecommendPlayListViewModel() }\n\n    val requestTag = playListTabSelectedBarViewModel.selectedTab?.name ?: \"全部歌单\"\n\n    ViewStateLazyGridPagingComponent(modifier = Modifier.fillMaxSize(),\n        key = \"CpnRecommendPlayList-${requestTag}\",\n        columns = 4,\n        pageSize = 40,\n        contentPadding = PaddingValues(horizontal = 20.dp, vertical = 14.dp),\n        viewStateContentAlignment = BiasAlignment(0f, -0.6f),\n        loadDataBlock = { pageSize, cupage ->\n            cpnRecommendPlayListViewModel.getPlayList(requestTag, pageSize, cupage)\n        },\n        scrollHeader = {\n            CpnHighQualityPlayListEntrance(requestTag)\n        },\n        stickyHeader = {\n            CpnPlayListTabSelectedBar()\n        }\n    ) { data ->\n        items(data.playlists.size) {\n            CpnPlayListItem(data.playlists[it])\n        }\n    }\n\n}\n\n\n\n\nclass RecommendPlayListViewModel : BaseViewModel() {\n\n    fun getPlayList(tag: String, pageSize: Int, curPage: Int) = launchFlow {\n        val offset = (curPage - 1) * pageSize\n        println(\"CpnRecommendPlayListViewModel getPlayList tag=$tag,pageSize=$pageSize,offset=$offset\")\n        NCRetrofitClient.getNCApi().getPlayList(pageSize, tag, offset)\n    }\n\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/discovery/cpn/CpnRecommentMVEntrance.kt",
    "content": "package ui.discovery.cpn\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport http.NCRetrofitClient\nimport model.RecommendMVItem\nimport model.RecommendMVResult\nimport ui.common.*\nimport ui.common.theme.AppColorsProvider\nimport util.StringUtil\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\n\n/**\n * 个性推荐-推荐MV入口\n */\nfun LazyListScope.CpnRecommendMVEntrance(viewModel: RecommendMVEntranceViewModel,\n                                         viewState: ViewState<RecommendMVResult>?) {\n    item {\n        CpnActionMore(\"推荐MV\")\n    }\n\n    handleListContent(viewState, reloadDataBlock = {\n        viewModel.getRecommendMV(false)\n    }) { data ->\n        ListToGridItems(data.result, 4) { _, item ->\n            RecommendMVItem(item)\n        }\n    }\n}\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nprivate fun RecommendMVItem(item: RecommendMVItem) {\n    var focusState by remember { mutableStateOf(false) }\n\n    Column(modifier = Modifier.onPointerEvent(PointerEventType.Enter) {\n        focusState = true\n    }.onPointerEvent(PointerEventType.Exit) {\n        focusState = false\n    }, horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Box {\n            AsyncImage(\n                modifier = Modifier.width(180.dp).height(100.dp).clip(RoundedCornerShape(6.dp)),\n                item.picUrl\n            )\n\n            Row(\n                modifier = Modifier.padding(top = 6.dp, end = 6.dp).align(Alignment.TopEnd),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Image(\n                    painter = painterResource(\"image/ic_play_count.webp\"),\n                    contentDescription = \"\",\n                    modifier = Modifier.padding(end = 6.dp).size(12.dp)\n                )\n                Text(\n                    StringUtil.friendlyNumber(item.playCount),\n                    color = Color.White,\n                    fontSize = 12.sp,\n                )\n            }\n            Column {\n                AnimatedVisibility(\n                    visible = focusState\n                ) {\n                    Box(\n                        modifier = Modifier.width(180.dp).background(\n                                Brush.verticalGradient(\n                                    listOf(\n                                        Color.Black.copy(0.75f), Color.Black.copy(0.25f)\n                                    )\n                                ), shape = RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp)\n                            ).padding(vertical = 6.dp, horizontal = 10.dp)\n                    ) {\n                        Text(\n                            item.copywriter,\n                            color = Color.White,\n                            fontSize = 12.sp,\n                            maxLines = 3,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n            }\n        }\n\n        Text(\n            item.name,\n            color = AppColorsProvider.current.firstText,\n            fontSize = 12.sp,\n            maxLines = 1,\n            modifier = Modifier.padding(top = 6.dp, start = 16.dp, end = 16.dp)\n        )\n\n        Text(\n            item.artistName,\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            maxLines = 1,\n            modifier = Modifier.padding(top = 4.dp, start = 16.dp, end = 16.dp, bottom = 16.dp)\n        )\n    }\n}\n\nclass RecommendMVEntranceViewModel : BaseViewModel() {\n\n    var flow by mutableStateOf<ViewStateMutableStateFlow<RecommendMVResult>?>(null)\n    fun getRecommendMV(firstLoad: Boolean)  {\n        if (!firstLoad || flow == null) {\n            flow = launchFlow {\n                println(\"获取推荐MV...\")\n                NCRetrofitClient.getNCApi().getRecommendMV()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/login/QrcodeLoginDialog.kt",
    "content": "package ui.login\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.rememberDialogState\nimport base.UserManager\nimport com.google.gson.Gson\nimport http.NCRetrofitClient\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport util.QrcodeUtil\nimport kotlinx.coroutines.launch\nimport model.LoginResult\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.AsyncImage\nimport ui.common.LoadingComponent\nimport ui.common.NoSuccessComponent\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport java.io.File\n\n/**\n * 二维码登录对话框\n */\n@Composable\nfun QrcodeLoginDialog(show: MutableState<Boolean>) {\n    val viewModel: LoginViewModel = viewModel { LoginViewModel() }\n\n    LaunchedEffect(show.value) {\n        if (show.value) {\n            viewModel.qrcodeAuth()\n        } else {\n            viewModel.cancelLastJob()\n        }\n    }\n\n    Dialog(\n        onCloseRequest = { show.value = false }, visible = show.value,\n        state = rememberDialogState(size = DpSize(400.dp, 480.dp)),\n        resizable = false,\n        title = \"\"\n    ) {\n        if (show.value) {\n            QrcodeLoginDialogContent(show)\n        }\n    }\n}\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nprivate fun QrcodeLoginDialogContent(show: MutableState<Boolean>) {\n    val viewModel: LoginViewModel = viewModel { LoginViewModel() }\n\n//    LaunchedEffect(show.value) {\n//        if (show.value) {\n//            viewModel.qrcodeAuth()\n//        } else {\n//            viewModel.clear()\n//        }\n//    }\n\n    LaunchedEffect(viewModel.qrcodeAuthStatus) {\n        if (viewModel.qrcodeAuthStatus == 800) {  // 二维码过期，重走认证流程\n            println(\"----二维码过期，重新生成\")\n            viewModel.qrcodeAuth()\n        }\n    }\n    LaunchedEffect(viewModel.getAccountInfoSuccess) {\n        if (viewModel.getAccountInfoSuccess == true) {\n            show.value = false\n        } else if (viewModel.getAccountInfoSuccess == false) {  // 获取用户信息失败，重走认证流程\n            viewModel.qrcodeAuth()\n        }\n    }\n\n\n    var focusState by remember { mutableStateOf(false) }\n    val totalWidth = remember { 400.dp }\n    val smallWidth = remember { 180.dp }\n    val largeWidth = remember { 220.dp }\n\n    val imageInitOffset = remember { (totalWidth - largeWidth) / 2 + largeWidth - smallWidth }\n    val imageTargetOffset = remember { (totalWidth - smallWidth * 2) / 2 }\n    val qrcodeInitOffset = remember { (totalWidth - largeWidth) / 2 }\n    val qrcodeTargetOffset = remember { totalWidth - (totalWidth - smallWidth * 2) / 2 - smallWidth }\n\n    val scope = rememberCoroutineScope()\n    val offsetAnim = remember { Animatable(0f) }\n\n    Column(\n        modifier = Modifier.height(480.dp).background(AppColorsProvider.current.pure).padding(top = 60.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n\n        Text(\n            \"扫码登录\",\n            textAlign = TextAlign.Center,\n            fontWeight = FontWeight.Bold,\n            fontSize = 24.sp,\n            color = AppColorsProvider.current.firstText\n        )\n\n        Box(modifier = Modifier.width(totalWidth)\n            .onPointerEvent(PointerEventType.Enter) {\n                focusState = true\n                scope.launch {\n                    offsetAnim.animateTo(1f, tween(600))\n                }\n            }\n            .onPointerEvent(PointerEventType.Exit) {\n                focusState = false\n                scope.launch {\n                    offsetAnim.animateTo(0f, tween(600))\n                }\n            },\n            contentAlignment = Alignment.CenterStart\n        ) {\n            CpnScanTipImage(\n                Modifier.width(smallWidth).height(260.dp).padding(12.dp)\n                    .scale(offsetAnim.value * 0.3f + 0.7f)\n                    .offset(imageInitOffset + (imageTargetOffset - imageInitOffset) * offsetAnim.value)\n                    .alpha(offsetAnim.value)\n            )\n\n            val cpnScanQrcodeWidth = largeWidth - (largeWidth - smallWidth) * offsetAnim.value\n\n            CpnScanQrcode(\n                Modifier.width(cpnScanQrcodeWidth)\n                    .height(300.dp)\n                    .offset(qrcodeInitOffset + (qrcodeTargetOffset - qrcodeInitOffset) * offsetAnim.value)\n                    .padding(top = 20.dp),\n                cpnScanQrcodeWidth\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CpnScanTipImage(modifier: Modifier) {\n    Image(painterResource(\"image/ic_scan_code_tip.webp\"), contentDescription = \"扫描二维码提示\", modifier = modifier)\n}\n\n@Composable\nprivate fun CpnScanQrcode(modifier: Modifier, qrcodeSize: Dp) {\n    val viewModel: LoginViewModel = viewModel { LoginViewModel() }\n    val tip = when (viewModel.qrcodeAuthStatus) {\n        801, 802 -> \"请使用网易云音乐APP\\n扫码登录\"\n        803 -> \"正在获取用户信息...\"\n        null -> \"正在加载二维码\"\n        else -> \"加载二维码出错\"\n    }\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        if (viewModel.qrcodeAuthStatus == 801 || viewModel.qrcodeAuthStatus == 802) {\n            AsyncImage(\n                modifier = Modifier.size(qrcodeSize),\n                viewModel.qrcodeFile?.absolutePath ?: \"\",\n                placeHolderUrl = null,\n                errorUrl = null\n            )\n        } else {\n            Box(\n                modifier = Modifier.size(qrcodeSize),\n                contentAlignment = Alignment.Center\n            ) {\n                if (viewModel.qrcodeAuthStatus == null || viewModel.qrcodeAuthStatus == 803) {\n                    LoadingComponent()\n                } else {\n                    NoSuccessComponent(modifier = Modifier.wrapContentSize(), message = \"\") {\n                        viewModel.qrcodeAuth()\n                    }\n                }\n            }\n        }\n\n        Text(\n            tip,\n            textAlign = TextAlign.Center,\n            fontSize = 14.sp,\n            color = AppColorsProvider.current.firstText,\n            modifier = Modifier.padding(top = 14.dp)\n        )\n    }\n}\n\nclass LoginViewModel : BaseViewModel() {\n    var qrcodeAuthStatus by mutableStateOf<Int?>(null)\n    var qrcodeFile by mutableStateOf<File?>(null)\n    var getAccountInfoSuccess by mutableStateOf<Boolean?>(null)\n    private var mLastQrcodeAuthJob: Job? = null\n    private var mCookie = \"\"\n\n    fun qrcodeAuth() {\n        mLastQrcodeAuthJob?.let {\n            it.cancel()\n        }\n        qrcodeAuthStatus = null\n        mLastQrcodeAuthJob = launch(\n            handleFailBlock = { code, msg ->\n                println(\"----handleFailBlock\")\n                qrcodeAuthStatus = code\n            }\n        ) {\n            println(\"----start getLoginQrcodeKey\")\n            val qrcodeKeyResult = NCRetrofitClient.getNCApi().getLoginQrcodeKey()\n            println(\"----start getLoginQrcodeValue\")\n            val qrcodeValueResult = NCRetrofitClient.getNCApi().getLoginQrcodeValue(qrcodeKeyResult.data.unikey)\n            println(\"----start createQrcodeFile\")\n            qrcodeFile = QrcodeUtil.createQrcodeFile(\n                qrcodeValueResult.data.qrurl,\n                500,\n                500\n            )\n            println(\"----start checkQrcodeAuthStatus\")\n            var qrcodeAuthResult = NCRetrofitClient.getNCApi().checkQrcodeAuthStatus(qrcodeKeyResult.data.unikey)\n            qrcodeAuthStatus = qrcodeAuthResult.code\n            println(\"----qrcodeAuthStatus = $qrcodeAuthStatus\")\n            while (mLastQrcodeAuthJob?.isActive != false) {\n                // 4s轮训一次登录授权状态\n                delay(4000)\n                qrcodeAuthResult = NCRetrofitClient.getNCApi().checkQrcodeAuthStatus(qrcodeKeyResult.data.unikey)\n                qrcodeAuthStatus = qrcodeAuthResult.code\n                if (qrcodeAuthResult.resultOk()) {  // 授权成功\n                    println(\"----授权成功\")\n                    mCookie = qrcodeAuthResult.cookie\n                    getAccountInfo()\n                    break\n                } else if (qrcodeAuthStatus == 800) {\n                    println(\"----二维码过期\")\n                } else if (qrcodeAuthStatus == 801) {\n                    println(\"----等待扫码\")\n                } else if (qrcodeAuthStatus == 802) {\n                    println(\"----待确认\")\n                }\n            }\n            println(\"授权成功----请求个人信息和账户信息\")\n            qrcodeAuthResult\n        }\n    }\n\n    private suspend fun getAccountInfo() {\n        val accountInfoResult = NCRetrofitClient.getNCApi().getAccountInfo(mCookie)\n        if (accountInfoResult.resultOk()) {\n            val loginResult = LoginResult(accountInfoResult.account, accountInfoResult.profile, mCookie)\n            UserManager.saveLoginResult(Gson().toJson(loginResult))\n            getAccountInfoSuccess = true\n        } else {\n            getAccountInfoSuccess = false\n        }\n    }\n\n    fun cancelLastJob() {\n        mLastQrcodeAuthJob?.cancel()\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/MainPage.kt",
    "content": "package ui.main\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport ui.common.toast.Toast\nimport ui.main.cpn.CpnMusicPlayBottomBar\nimport ui.main.cpn.CpnMainLeftMenu\nimport ui.main.cpn.CpnMainMusicPlayDrawer\nimport ui.main.cpn.CpnMainRightContainer\nimport ui.play.CpnCurrentPlayListSheet\n\n/**\n * 主页\n */\n@Composable\nfun MainPage() {\n\n    Column {\n        Box(modifier = Modifier.weight(1f)) {\n            Row(modifier = Modifier.fillMaxSize()) {\n                CpnMainLeftMenu()\n                CpnMainRightContainer()\n            }\n            CpnMainMusicPlayDrawer()\n            CpnCurrentPlayListSheet()\n            Toast()\n        }\n\n        CpnMusicPlayBottomBar()\n    }\n\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnMainLeftMenu.kt",
    "content": "package ui.main.cpn\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.*\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.AppConfig\nimport base.MusicPlayController\nimport base.UserManager\nimport com.google.gson.Gson\nimport http.NCRetrofitClient\nimport kotlinx.coroutines.launch\nimport model.PlaylistDetail\nimport moe.tlaster.precompose.navigation.NavOptions\nimport moe.tlaster.precompose.ui.viewModel\nimport router.NCNavigatorManager\nimport router.RouterUrls\nimport ui.common.AsyncImage\nimport ui.common.theme.AppColorsProvider\nimport ui.login.QrcodeLoginDialog\nimport base.BaseViewModel\nimport ui.common.onClick\n\n/**\n * 主页左边菜单栏组件\n */\n@Composable\nfun CpnMainLeftMenu() {\n    val navigator = NCNavigatorManager.navigator\n    val loginResult = UserManager.getLoginResultFlow().collectAsState(null).value\n    val viewModel: MainLeftMenuViewModel = viewModel { MainLeftMenuViewModel() }\n    LaunchedEffect(loginResult) {\n        if (loginResult != null) {\n            viewModel.getUserPlayList(loginResult.account.id)\n        }\n    }\n\n    Column(modifier = Modifier.width(200.dp).fillMaxHeight().background(AppColorsProvider.current.background)) {\n        Spacer(\n            modifier = Modifier.fillMaxWidth().height(AppConfig.topBarHeight)\n                .background(if (MusicPlayController.showMusicPlayDrawer) AppColorsProvider.current.pure else AppColorsProvider.current.topBarColor)\n        )\n        CpnUserInfo()\n        Column(\n            modifier = Modifier.verticalScroll(rememberScrollState()),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            CpnMenuItem(viewModel, \"image/ic_my_music.webp\", \"发现音乐\") {\n                while (navigator.canGoBack) {\n                    navigator.popBackStack()\n                }\n                navigator.navigate(RouterUrls.DISCOVERY, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_podcast.webp\", \"播客\") {\n                navigator.navigate(RouterUrls.PODCAST, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_fm.webp\", \"私人FM\") {\n                navigator.navigate(RouterUrls.PERSONAL_FM, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_video.webp\", \"视频\") {\n                navigator.navigate(RouterUrls.VIDEO, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_follows.webp\", \"关注\") {\n                navigator.navigate(RouterUrls.FOLLOW, NavOptions(launchSingleTop = true))\n            }\n            CpnMyMusicTitle()\n            viewModel.favoritePlayList?.let {\n                CpnSongSheetItem(viewModel, \"image/ic_like.webp\", it)\n            }\n            CpnMenuItem(viewModel, \"image/ic_download.webp\", \"下载管理\") {\n                navigator.navigate(RouterUrls.DOWNLOAD_MANAGER, NavOptions(launchSingleTop = true))\n\n            }\n            CpnMenuItem(viewModel, \"image/ic_recent_play_list.webp\", \"最近播放\") {\n                navigator.navigate(RouterUrls.RECENT_PLAYLIST, NavOptions(launchSingleTop = true))\n\n            }\n            CpnMenuItem(viewModel, \"image/ic_cloud.webp\", \"我的音乐云盘\") {\n                navigator.navigate(RouterUrls.MY_CLOUD_DISK, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_podcast.webp\", \"我的播客\") {\n                navigator.navigate(RouterUrls.MY_PODCAST, NavOptions(launchSingleTop = true))\n            }\n            CpnMenuItem(viewModel, \"image/ic_collect.webp\", \"我的收藏\") {\n                navigator.navigate(RouterUrls.MY_COLLECT, NavOptions(launchSingleTop = true))\n\n            }\n            viewModel.selfCreatePlayList?.let {\n                CpnSongSheet(\"创建的歌单\", it)\n            }\n            viewModel.collectPlayList?.let {\n                CpnSongSheet(\"收藏的歌单\", it)\n            }\n\n            LogoutButton()\n        }\n    }\n\n}\n\n@Composable\nprivate fun CpnUserInfo() {\n    val showLoginDialog = rememberSaveable { mutableStateOf(false) }\n    val loginResult = UserManager.getLoginResultFlow().collectAsState(null).value\n\n    Row(\n        modifier = Modifier.fillMaxWidth().height(56.dp).onClick {\n            if (loginResult == null) {\n                showLoginDialog.value = true\n            }\n        }.padding(horizontal = 14.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n\n        AsyncImage(\n            modifier = Modifier.clip(RoundedCornerShape(50)).size(36.dp),\n            loginResult?.profile?.avatarUrl ?: \"image/ic_default_avator.webp\",\n            \"image/ic_default_avator.webp\",\n            \"image/ic_default_avator.webp\"\n        )\n\n        Text(\n            text = loginResult?.profile?.nickname ?: \"未登录\",\n            modifier = Modifier.padding(horizontal = 10.dp),\n            fontSize = 14.sp,\n            maxLines = 1,\n            color = AppColorsProvider.current.firstText,\n            overflow = TextOverflow.Ellipsis\n        )\n        Icon(\n            painterResource(\"image/ic_triangle_right.webp\"), contentDescription = \"\", modifier = Modifier.size(8.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n    QrcodeLoginDialog(showLoginDialog)\n}\n\n@Composable\nprivate fun CpnSongSheetItem(viewModel: MainLeftMenuViewModel, icon: String, playlistDetail: PlaylistDetail) {\n    CpnMenuItem(viewModel, icon, playlistDetail.name, type = 1) {\n        val url = \"${RouterUrls.PLAY_LIST_DETAIL}?simplePlayListInfo=${Gson().toJson(playlistDetail.convertToSimple())}\"\n        println(\"navigate to PLAY_LIST_DETAIL, url=$url\")\n        NCNavigatorManager.navigator.navigate(url)\n    }\n}\n\n/**\n * type:菜单类型，0表普通，1表歌单\n */\n@Composable\nprivate fun CpnMenuItem(\n    viewModel: MainLeftMenuViewModel,\n    logoPath: String,\n    title: String,\n    markLogoPath: String? = null,\n    type: Int = 0,\n    onClick: (title: Any) -> Unit\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth().height(40.dp).onClick {\n            if (type == 0) {\n                viewModel.selectedMenuTag = title\n                viewModel.selectedSongSheetTag = null\n            } else {\n                viewModel.selectedMenuTag = null\n                viewModel.selectedSongSheetTag = title\n            }\n            onClick(title)\n        }.let {\n            if (viewModel.selectedMenuTag == title || viewModel.selectedSongSheetTag == title) it.background(\n                AppColorsProvider.current.pure\n            ) else {\n                it\n            }\n        }.padding(horizontal = 14.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            painterResource(logoPath),\n            contentDescription = title,\n            modifier = Modifier.size(18.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Text(\n            text = title,\n            modifier = Modifier.weight(1f).padding(horizontal = 6.dp),\n            fontSize = 12.sp,\n            maxLines = 1,\n            color = AppColorsProvider.current.firstText,\n            overflow = TextOverflow.Ellipsis\n        )\n        markLogoPath?.let {\n            Icon(painterResource(it), contentDescription = null, modifier = Modifier.size(14.dp))\n        }\n    }\n}\n\n@Composable\nprivate fun CpnMyMusicTitle() {\n    Text(\n        text = \"我的音乐\",\n        modifier = Modifier.padding(top = 10.dp, bottom = 4.dp).fillMaxWidth().padding(horizontal = 16.dp),\n        fontSize = 12.sp,\n        maxLines = 1,\n        color = AppColorsProvider.current.secondText,\n        overflow = TextOverflow.Ellipsis\n    )\n}\n\n@Composable\nprivate fun ColumnScope.CpnSongSheet(title: String, list: List<PlaylistDetail>) {\n    var expanded by remember { mutableStateOf(true) }\n    val animValue = remember { Animatable(1f) }\n    val scope = rememberCoroutineScope()\n    val viewModel: MainLeftMenuViewModel = viewModel { MainLeftMenuViewModel() }\n    Row(\n        modifier = Modifier.onClick {\n            expanded = !expanded\n            scope.launch {\n                animValue.animateTo(if (expanded) 1f else 0f)\n            }\n        }.padding(top = 16.dp, bottom = 8.dp).fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            painterResource(\"image/ic_triangle_right.webp\"),\n            contentDescription = \"\",\n            modifier = Modifier.padding(start = 8.dp).size(8.dp).rotate(\n                90 * animValue.value\n            ),\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Text(\n            text = title,\n            modifier = Modifier.weight(1f).padding(horizontal = 6.dp),\n            fontSize = 12.sp,\n            maxLines = 1,\n            color = AppColorsProvider.current.secondText,\n            overflow = TextOverflow.Ellipsis\n        )\n    }\n\n    Column(modifier = Modifier.fillMaxWidth().height((40f * animValue.value * list.size).dp)) {\n        for (i in 0 until list.size) {\n            CpnSongSheetItem(viewModel, \"image/ic_song_sheet.webp\", list[i])\n        }\n\n    }\n}\n\n@Composable\nprivate fun LogoutButton() {\n    val loginResult = UserManager.getLoginResultFlow().collectAsState(null).value\n    if (loginResult != null) {\n        val scope = rememberCoroutineScope()\n        val viewModel = viewModel { MainLeftMenuViewModel() }\n        Button(modifier = Modifier.padding(top = 60.dp, bottom = 24.dp).width(160.dp),\n            colors = ButtonDefaults.buttonColors(backgroundColor = AppColorsProvider.current.primary),\n            onClick = {\n                scope.launch {\n                    UserManager.saveLoginResult(\"\")\n                    viewModel.favoritePlayList = null\n                    viewModel.selfCreatePlayList = null\n                    viewModel.collectPlayList = null\n                    if (viewModel.selectedSongSheetTag != null) {\n                        viewModel.selectedSongSheetTag = null\n                        viewModel.selectedMenuTag = \"发现音乐\"\n                        while (NCNavigatorManager.navigator.canGoBack) {\n                            NCNavigatorManager.navigator.popBackStack()\n                        }\n                        NCNavigatorManager.navigator.navigate(RouterUrls.DISCOVERY, NavOptions(launchSingleTop = true))\n                    }\n                }\n            }) {\n            Text(\"退出登陆\", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Medium)\n        }\n    }\n}\n\nclass MainLeftMenuViewModel : BaseViewModel() {\n    var favoritePlayList: PlaylistDetail? by mutableStateOf(null)\n    var selfCreatePlayList: List<PlaylistDetail>? by mutableStateOf(null)\n    var collectPlayList: List<PlaylistDetail>? by mutableStateOf(null)\n    var selectedMenuTag: String? by mutableStateOf(\"发现音乐\")\n    var selectedSongSheetTag: String? by mutableStateOf(null)\n\n    fun getUserPlayList(userId: Long) {\n        launch(handleSuccessBlock = {\n            val selfCreateList = mutableListOf<PlaylistDetail>()\n            val collectList = mutableListOf<PlaylistDetail>()\n\n            it.playlist.forEach { PlaylistDetail ->\n                if (PlaylistDetail.creator.userId == userId) {\n                    if (PlaylistDetail.name == PlaylistDetail.creator.nickname + \"喜欢的音乐\") {\n                        favoritePlayList = PlaylistDetail\n                    } else {\n                        selfCreateList.add(PlaylistDetail)\n                    }\n                } else {\n                    collectList.add(PlaylistDetail)\n                }\n            }\n            selfCreatePlayList = selfCreateList\n            collectPlayList = collectList\n        }) {\n            NCRetrofitClient.getNCApi().getUserPlayList(userId.toString())\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnMainMusicPlayDrawer.kt",
    "content": "package ui.main.cpn\n\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.background\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport base.AppConfig\nimport base.MusicPlayController\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.theme.AppColorsProvider\nimport ui.play.CpnLyric\nimport ui.play.CpnMusicPlay\nimport ui.play.CpnSongInfo\nimport ui.playlist.cpn.CpnCommentList\nimport ui.playlist.cpn.CommentListViewModel\n\n/**\n * 音乐播放抽屉组件\n */\n@Composable\nfun CpnMainMusicPlayDrawer() {\n    Box {\n        if (MusicPlayController.showMusicPlayDrawer) {\n            Box(\n                modifier = Modifier.height(AppConfig.topBarHeight).width(200.dp),\n            ) {\n                Icon(\n                    painterResource(\"image/ic_back.webp\"),\n                    modifier = Modifier.padding(start = 72.dp, top = 4.dp).clip(RoundedCornerShape(50))\n                        .background(AppColorsProvider.current.secondary.copy(0.05f))\n                        .onClick  {\n                            MusicPlayController.showMusicPlayDrawer = false\n                        }.padding(4.dp).size(12.dp).rotate(270f),\n                    contentDescription = \"返回上一页\",\n                    tint = AppColorsProvider.current.thirdIcon\n\n                )\n            }\n        }\n\n        AnimatedVisibility(\n            MusicPlayController.showMusicPlayDrawer,\n            enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),\n            exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()\n        ) {\n\n            MusicPlayController.curSongBean?.let { songBean ->\n                val commentViewModel = viewModel(keys = listOf(songBean.id)) { CommentListViewModel() }\n                LaunchedEffect(songBean.id) {\n                    commentViewModel.fetchDataPaging(\"music\", songBean.id, 1, true)\n                }\n                val commentViewState = commentViewModel.flow?.collectAsState()\n\n                LazyColumn(\n                    modifier = Modifier.padding(top = AppConfig.topBarHeight).background(AppColorsProvider.current.pure).padding(horizontal = 30.dp)\n                ) {\n                    item {\n                        Header()\n                    }\n\n                    CpnCommentList(commentViewState?.value, commentViewModel) { curPage ->\n                        commentViewModel.fetchDataPaging(\"music\", songBean.id, curPage)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun Header() {\n    val height = remember { AppConfig.windowMinHeight - AppConfig.topBarHeight - 90.dp }\n    Row(modifier = Modifier.fillMaxWidth().height(height)) {\n        CpnMusicPlay(Modifier.weight(1f).fillMaxHeight())\n        Column(Modifier.padding(end = 80.dp).weight(1f).fillMaxHeight()) {\n            CpnSongInfo()\n            CpnLyric(Modifier)\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnMainRightContainer.kt",
    "content": "package ui.main.cpn\n\nimport androidx.compose.foundation.background\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.onClick\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.AppConfig\nimport base.MusicPlayController\nimport router.NCNavigatorManager\nimport router.NavGraph\nimport router.RouterUrls\nimport ui.common.theme.AppColorsProvider\n\n/**\n * 主页右边布局组件\n */\n@Composable\nfun CpnMainRightContainer() {\n    Box(modifier = Modifier.fillMaxSize().background(color = AppColorsProvider.current.pure)) {\n        Spacer(\n            modifier = Modifier.fillMaxWidth().height(50.dp)\n                .background(if (MusicPlayController.showMusicPlayDrawer) AppColorsProvider.current.pure else AppColorsProvider.current.topBarColor)\n        )\n        NavGraph()\n        CpnRightTopActionButtons()\n    }\n}\n\n/**\n * 顶部TitleBar组件\n */\n@Composable\nfun CommonTitleBar(\n    title: String = \"\",\n    showBackButton: Boolean = false,\n    customerContent: (@Composable () -> Unit)? = null\n) {\n    Box(\n        modifier = Modifier.padding(end = 320.dp).fillMaxWidth().height(AppConfig.topBarHeight)\n            .background(if (MusicPlayController.showMusicPlayDrawer) AppColorsProvider.current.pure else AppColorsProvider.current.topBarColor),\n        contentAlignment = Alignment.CenterStart\n    ) {\n        if (!MusicPlayController.showMusicPlayDrawer) {\n            if (customerContent != null) {\n                customerContent.invoke()\n            } else {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    if (showBackButton) {\n                        val navigator = NCNavigatorManager.navigator\n\n                        Icon(\n                            painterResource(\"image/ic_back.webp\"),\n                            modifier = Modifier.padding(start = 20.dp).clip(RoundedCornerShape(50)).onClick {\n                                navigator.popBackStack()\n                            }.padding(4.dp).size(18.dp),\n                            contentDescription = \"返回上一页\",\n                            tint = AppColorsProvider.current.firstIcon\n\n                        )\n                    }\n\n                    Text(\n                        title,\n                        color = AppColorsProvider.current.firstText,\n                        fontSize = 16.sp,\n                        fontWeight = FontWeight.Bold,\n                        modifier = Modifier.padding(start = 20.dp)\n                    )\n                }\n            }\n        }\n\n    }\n}\n\n\n@Composable\nprivate fun BoxScope.CpnRightTopActionButtons() {\n    val showPopupWindow = remember { mutableStateOf(false) }\n\n    Row(\n        modifier = Modifier.height(50.dp).width(320.dp).align(Alignment.TopEnd),\n        horizontalArrangement = Arrangement.End,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            painterResource(\"image/ic_setting.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(24.dp).padding(3.dp).onClick {\n                NCNavigatorManager.navigator.navigate(RouterUrls.SETTING)\n            },\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_message.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(24.dp).padding(3.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_theme.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(24.dp).padding(3.dp).onClick {\n                showPopupWindow.value = true\n            },\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_screen_min.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 24.dp).size(24.dp).padding(3.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n\n    CpnThemePopup(showPopupWindow)\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnMusicPlayBottomBar.kt",
    "content": "package ui.main.cpn\n\nimport ui.common.SeekBar\nimport androidx.compose.foundation.background\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.MusicPlayController\nimport base.player.PlayMode\nimport ui.common.AsyncImage\nimport ui.common.theme.AppColorsProvider\n\n/**\n * 主页底部音乐播放组件\n */\n@Composable\nfun CpnMusicPlayBottomBar() {\n    Column(modifier = Modifier.background(color = AppColorsProvider.current.pure).fillMaxWidth().height(80.dp)) {\n        CpnSeekBar()\n        Row(\n            modifier = Modifier.padding(horizontal = 20.dp).fillMaxSize(),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            CpnMusicInfo()\n            CpnMiddleActionButtons()\n            CpnRightActionButtons()\n        }\n    }\n}\n\n@Composable\nprivate fun CpnSeekBar() {\n    SeekBar(\n        progress = MusicPlayController.progress,\n        enableSeek = MusicPlayController.enableSeeking,\n        seeking = {\n            MusicPlayController.seeking(it)\n        },\n        seekTo = {\n            MusicPlayController.seekTo(it)\n        },\n        modifier = Modifier\n            .fillMaxWidth()\n            .height(10.dp)\n    )\n}\n\n@Composable\nprivate fun RowScope.CpnMusicInfo() {\n    Row(\n        modifier = Modifier.weight(1.5f).onClick {\n            MusicPlayController.showMusicPlayDrawer = !MusicPlayController.showMusicPlayDrawer\n        },\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        MusicPlayController.curSongBean?.let { curSong ->\n            AsyncImage(\n                Modifier.padding(end = 10.dp).size(48.dp).clip(RoundedCornerShape(4.dp)),\n                url = curSong.al.picUrl\n            )\n\n            Column {\n                Row {\n                    Text(\n                        curSong.name, fontSize = 14.sp, color = AppColorsProvider.current.firstText,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                    Text(\n                        \" - ${curSong.ar.getOrNull(0)?.name ?: \"未知歌手\"}\",\n                        fontSize = 14.sp,\n                        color = AppColorsProvider.current.secondText,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                }\n                Text(\n                    \"${MusicPlayController.curPositionStr} / ${curSong.getSongTimeLength()}\",\n                    fontSize = 12.sp,\n                    color = AppColorsProvider.current.secondText,\n                    modifier = Modifier.padding(top = 6.dp)\n                )\n            }\n        }\n    }\n\n}\n\n@Composable\nprivate fun RowScope.CpnMiddleActionButtons() {\n    Row(\n        modifier = Modifier.weight(1f),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Icon(\n            painterResource(\"image/ic_like.webp\"), contentDescription = null,\n            modifier = Modifier.padding(end = 30.dp).size(20.dp).padding(2.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_action_pre.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 20.dp).size(20.dp).padding(2.dp).onClick {\n                val newIndex = MusicPlayController.getPreRealIndex()\n                MusicPlayController.playByRealIndex(newIndex)\n            },\n            tint = AppColorsProvider.current.primary\n        )\n        Icon(\n            painterResource(if (MusicPlayController.isPlaying()) \"image/ic_action_pause.webp\" else \"image/ic_action_play.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 20.dp).size(40.dp).padding(2.dp).onClick {\n                if (MusicPlayController.isPlaying()) {\n                    MusicPlayController.pause()\n                } else {\n                    MusicPlayController.resume()\n                }\n            },\n            tint = AppColorsProvider.current.primary\n        )\n        Icon(\n            painterResource(\"image/ic_action_next.webp\"), contentDescription = null,\n            modifier = Modifier.padding(end = 30.dp).size(20.dp).padding(2.dp).onClick {\n                val newIndex = MusicPlayController.getNextRealIndex()\n                MusicPlayController.playByRealIndex(newIndex)\n            }, tint = AppColorsProvider.current.primary\n        )\n        Icon(\n            painterResource(\"image/ic_share.webp\"), contentDescription = null,\n            modifier = Modifier.size(20.dp).padding(2.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n}\n\n@Composable\nprivate fun RowScope.CpnRightActionButtons() {\n    Row(modifier = Modifier.weight(1.5f), horizontalArrangement = Arrangement.End) {\n        Icon(\n            painterResource(\"image/ic_sound_effect.webp\"), contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(20.dp).padding(2.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n        val playModeResId = when (MusicPlayController.playMode) {\n            PlayMode.RANDOM -> \"image/ic_play_mode_random.webp\"\n            PlayMode.SINGLE -> \"image/ic_play_mode_single.webp\"\n            PlayMode.LOOP -> \"image/ic_play_mode_loop.webp\"\n        }\n        Icon(\n            painterResource(playModeResId),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(20.dp).padding(2.dp).onClick {\n                when (MusicPlayController.playMode) {\n                    PlayMode.RANDOM -> MusicPlayController.changePlayMode(PlayMode.SINGLE)\n                    PlayMode.SINGLE -> MusicPlayController.changePlayMode(PlayMode.LOOP)\n                    PlayMode.LOOP -> MusicPlayController.changePlayMode(PlayMode.RANDOM)\n                }\n            },\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_play_list.webp\"),\n            contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(20.dp).padding(2.dp).onClick {\n                MusicPlayController.showCurPlayListSheet = !MusicPlayController.showCurPlayListSheet\n            },\n            tint = AppColorsProvider.current.firstIcon\n        )\n        Icon(\n            painterResource(\"image/ic_song_words.webp\"), contentDescription = null,\n            modifier = Modifier.padding(end = 14.dp).size(20.dp).padding(2.dp), tint = AppColorsProvider.current.primary\n        )\n        Icon(\n            painterResource(\"image/ic_volumn.webp\"), contentDescription = null,\n            modifier = Modifier.size(20.dp).padding(2.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnPlaformDecoratedButtons.kt",
    "content": "package ui.main.cpn\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.Icon\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.onPointerEvent\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.ApplicationScope\nimport androidx.compose.ui.window.WindowPlacement\nimport androidx.compose.ui.window.WindowState\nimport base.AppConfig\nimport ui.common.onClick\nimport util.EnvUtil\nimport kotlin.system.exitProcess\n\n/**\n * 自定义windows平台关闭、最小化、全屏按钮\n */\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun ApplicationScope.CpnWindowsPlaformDecoratedButtons(windowState: WindowState) {\n\n//    LaunchedEffect(windowState.placement) {\n//        if (windowState.placement == WindowPlacement.Maximized) {\n//            windowState.placement = WindowPlacement.Fullscreen\n//            println(\"Fullscreen done...\")\n//        }\n//    }\n\n    if (EnvUtil.isWindows()) {\n        var focusState by remember { mutableStateOf(false) }\n        Row(modifier = Modifier.padding(start = 8.dp, top = 8.dp)\n            .onPointerEvent(PointerEventType.Enter) {\n                focusState = true\n            }.onPointerEvent(PointerEventType.Exit) {\n                focusState = false\n            }) {\n            ActionButton(\"image/ic_window_close.webp\", Color(0xFFFC615D), focusState) {\n                ::exitApplication\n                exitProcess(0)\n            }\n\n            ActionButton(\"image/ic_window_min.webp\", Color(0xFFFDBC40), focusState) {\n                windowState.isMinimized = true\n            }\n            val imgUrl = if (AppConfig.fullScreen) {\n                \"image/ic_window_floating.webp\"\n            } else {\n                \"image/ic_window_fullscreen.webp\"\n            }\n\n            ActionButton(imgUrl, Color(0xFF35CA4A), focusState) {\n                if (AppConfig.fullScreen) {\n                    windowState.placement = WindowPlacement.Floating\n                } else {\n                    windowState.placement = WindowPlacement.Maximized\n                }\n                AppConfig.fullScreen = !AppConfig.fullScreen\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ActionButton(img: String, backgroundColor: Color, focusState: Boolean, onClick: () -> Unit) {\n\n    Box(\n        modifier = Modifier.padding(end = 8.dp).clip(CircleShape)\n            .size(13.dp).background(backgroundColor).onClick {\n                onClick()\n            }, contentAlignment = Alignment.Center\n    ) {\n\n        if (focusState) {\n            Icon(painterResource(img), modifier = Modifier.size(10.dp), contentDescription = null)\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/main/cpn/CpnThemePopup.kt",
    "content": "package ui.main.cpn\n\nimport androidx.compose.foundation.background\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.CursorDropdownMenu\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.*\nimport ui.common.theme.color.light.*\n\n/**\n * app主题色切换popup\n */\n@Composable\nfun CpnThemePopup(showPopupWindow: MutableState<Boolean>) {\n\n    val themeModels = remember {\n        mutableStateListOf(\n            mutableStateOf(ThemeModel(\"默认\", THEME_DEFAULT, DefaultColorPalette.primary, true)),\n            mutableStateOf(ThemeModel(\"夜间\", THEME_NIGHT, DarkColorPalette.pure, false)),\n            mutableStateOf(ThemeModel(\"蓝色\", THEME_BLUE, BlueColorPalette.primary, false)),\n            mutableStateOf(ThemeModel(\"绿色\", THEME_GREEN, GreenColorPalette.primary, false)),\n            mutableStateOf(ThemeModel(\"橙色\", THEME_ORIGIN, OriginColorPalette.primary, false)),\n            mutableStateOf(ThemeModel(\"紫色\", THEME_PURPLE, PurpleColorPalette.primary, false)),\n            mutableStateOf(ThemeModel(\"黄色\", THEME_YELLOW, YellowColorPalette.primary, false)),\n        )\n    }\n\n    CursorDropdownMenu(\n        showPopupWindow.value,\n        onDismissRequest = { showPopupWindow.value = false },\n    ) {\n        themeModels.forEachIndexed { index, themeModel ->\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(36.dp)\n                    .onClick  {\n                        themeTypeState.value = themeModel.value.themeType\n                        themeModels[lastSelectedThemeIndex].value = themeModels[lastSelectedThemeIndex].value.copy(selected = false)\n                        lastSelectedThemeIndex = index\n                        themeModels[index].value = themeModels[index].value.copy(selected = true)\n                    }\n                    .padding(horizontal = 16.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Box(\n                    modifier = Modifier\n                        .size(16.dp)\n                        .clip(RoundedCornerShape(50))\n                        .background(themeModel.value.color)\n                )\n\n                Text(\n                    text = themeModel.value.name,\n                    modifier = Modifier.padding(start = 12.dp, end = 20.dp),\n                    fontSize = 14.sp,\n                    color = AppColorsProvider.current.secondText\n                )\n\n                if (themeModels[index].value.selected) {\n                    Icon(\n                        painterResource(\"image/ic_checked.webp\"),\n                        contentDescription = null,\n                        modifier = Modifier.size(18.dp),\n                        tint = themeModel.value.color\n                    )\n                }\n            }\n        }\n    }\n\n}\n\nprivate var lastSelectedThemeIndex = 0\ndata class ThemeModel(val name: String, val themeType: Int, val color: Color, val selected: Boolean)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/play/CpnCurrentPlayListSheet.kt",
    "content": "package ui.play\n\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.AppConfig\nimport base.MusicPlayController\nimport ui.common.onClick\nimport ui.common.theme.AppColorsProvider\n\n/**\n * 当前播放列表组件\n */\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun CpnCurrentPlayListSheet() {\n\n    val sheetState = rememberModalBottomSheetState(\n        ModalBottomSheetValue.Hidden,\n        animationSpec = tween(durationMillis = 300),\n        skipHalfExpanded = true,\n    )\n\n    LaunchedEffect(MusicPlayController.showCurPlayListSheet) {\n        if (MusicPlayController.showCurPlayListSheet) {\n            sheetState.show()\n        } else {\n            sheetState.hide()\n        }\n    }\n\n    ModalBottomSheetLayout(\n        sheetContent = {\n            Column {\n                Spacer(modifier = Modifier.fillMaxWidth().height(AppConfig.topBarHeight).onClick {\n                    MusicPlayController.showCurPlayListSheet = false\n                })\n                CpnCurrentPlayList {\n                    MusicPlayController.showCurPlayListSheet = false\n                }\n            }\n        },\n        sheetState = sheetState,\n        sheetElevation = 0.dp,\n        sheetContentColor = Color.Transparent,\n        sheetBackgroundColor = Color.Transparent,\n        scrimColor = Color.Transparent\n    ) {\n\n    }\n}\n\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun CpnCurrentPlayList(hideCallback: () -> Unit) {\n    val lazyListState = rememberLazyListState()\n\n    LaunchedEffect(MusicPlayController.showCurPlayListSheet) {\n        if (MusicPlayController.showCurPlayListSheet\n            && MusicPlayController.curRealIndex >= 0\n            && MusicPlayController.curRealIndex < MusicPlayController.realSongList.size) {\n            lazyListState.animateScrollToItem(MusicPlayController.curRealIndex, -36.dp.value.toInt())\n        }\n    }\n\n    Row(modifier = Modifier.fillMaxWidth()) {\n        Spacer(modifier = Modifier.weight(1f).fillMaxHeight().onClick {\n            hideCallback.invoke()\n        })\n        Box(\n            modifier = Modifier\n                .width(3.dp)\n                .fillMaxHeight()\n                .background(\n                    brush = Brush.horizontalGradient(listOf(Color(0x00FFFFFF), AppColorsProvider.current.background))\n                )\n        )\n        LazyColumn(\n            modifier = Modifier.width(420.dp).fillMaxHeight().background(AppColorsProvider.current.pure),\n            state = lazyListState\n        ) {\n            stickyHeader {\n                Row(\n                    modifier = Modifier.background(AppColorsProvider.current.pure)\n                        .padding(vertical = 15.dp, horizontal = 20.dp).fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom\n                ) {\n                    Text(\n                        \"播放列表\",\n                        fontSize = 16.sp,\n                        fontWeight = FontWeight.Bold,\n                        color = AppColorsProvider.current.firstText\n                    )\n                    Text(\n                        \"  (共${MusicPlayController.realSongList.size}首)\",\n                        fontSize = 14.sp,\n                        color = AppColorsProvider.current.secondText,\n                    )\n                }\n                Divider(\n                    modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),\n                    thickness = 0.5.dp,\n                    color = AppColorsProvider.current.divider\n                )\n            }\n            items(MusicPlayController.realSongList.size) {\n                SongItem(it)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SongItem(index: Int) {\n    val curSongBean = MusicPlayController.realSongList[index]\n\n    Row(\n        modifier = Modifier\n            .onClick {\n                MusicPlayController.playByRealIndex(index)\n            }\n            .background(\n                if (index % 2 == 0) Color.Transparent else AppColorsProvider.current.divider.copy(\n                    0.25f\n                )\n            ).height(36.dp).fillMaxWidth().padding(horizontal = 15.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Row(modifier = Modifier.weight(2f).fillMaxHeight(), verticalAlignment = Alignment.CenterVertically) {\n            val num = if (index + 1 < 10) \"0${index + 1}\" else (index + 1).toString()\n            if (MusicPlayController.curSongBean?.id != curSongBean.id) {\n                Text(\n                    text = num,\n                    color = AppColorsProvider.current.thirdText,\n                    fontSize = 12.sp,\n                    modifier = Modifier.width(40.dp),\n                    textAlign = TextAlign.Center\n                )\n            } else {\n                Icon(\n                    painterResource(\"image/ic_playing.webp\"),\n                    contentDescription = null,\n                    modifier = Modifier.width(40.dp).height(36.dp).padding(horizontal = 14.dp, vertical = 12.dp),\n                    tint = AppColorsProvider.current.primary\n                )\n            }\n\n            Text(\n                text = curSongBean.name,\n                color = AppColorsProvider.current.firstText,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                fontSize = 12.sp,\n                textAlign = TextAlign.Center\n            )\n        }\n        Text(\n            modifier = Modifier.padding(start = 10.dp).weight(1f),\n            text = curSongBean.ar.getOrNull(0)?.name ?: \"未知歌手\",\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n\n        Text(\n            modifier = Modifier.weight(1f),\n            text = curSongBean.getSongTimeLength(),\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/play/CpnLyric.kt",
    "content": "package ui.play\n\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.*\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.MusicPlayController\nimport base.player.IPlayerListener\nimport base.player.PlayerStatus\nimport http.NCRetrofitClient\nimport model.LyricContributorBean\nimport model.LyricResult\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.ViewStateComponent\nimport ui.common.theme.AppColorsProvider\nimport util.LyricUtil\nimport util.convertDp\nimport base.BaseViewModel\n\n/**\n * 歌词组件\n */\n@Composable\nfun CpnLyric(modifier: Modifier) {\n\n    MusicPlayController.curSongBean?.let { songBean ->\n        val viewModel = viewModel { CpnLyricViewModel() }\n\n        ViewStateComponent(modifier = modifier,\n            key = \"CpnLyric-${songBean.id}\",\n            loadDataBlock = {\n                viewModel.getLyric(songBean.id)\n            },\n            customLoadingComponent = {\n                ViewStateTip(\"加载歌词中...\")\n            },\n            customEmptyComponent = {\n                ViewStateTip(\"暂无歌词\")\n            },\n            customFailComponent = { _, loadDataBlock ->\n                ViewStateTip(\"加载歌词出错, 点击重试\", loadDataBlock)\n            },\n            customErrorComponent = { _, loadDataBlock ->\n                ViewStateTip(\"加载歌词出错, 点击重试\", loadDataBlock)\n            }\n        ) {\n            LyricList(it)\n        }\n    }\n}\n\n@Composable\nfun LyricList(data: LyricResult) {\n    var cpnLyricHeight by remember { mutableStateOf(0) }\n    val lazyListState = rememberLazyListState()\n    val density = LocalDensity.current\n    val viewModel = viewModel { CpnLyricViewModel() }\n    LaunchedEffect(viewModel.curLyricIndex) {\n        if (viewModel.curLyricIndex >= 0) {\n            lazyListState.animateScrollToItem(viewModel.curLyricIndex)\n        }\n    }\n\n    LazyColumn(\n        modifier = Modifier\n            .padding(top = 20.dp, bottom = 100.dp)\n            .fillMaxSize()\n            .onGloballyPositioned {\n                cpnLyricHeight = it.size.height\n            }\n            .drawWithContent {\n                val paint = Paint().asFrameworkPaint()\n                drawIntoCanvas {\n                    val layerId: Int = it.nativeCanvas.saveLayer(\n                        0f,\n                        0f,\n                        size.width,\n                        size.height,\n                        paint\n                    )\n                    drawContent()\n                    drawRect(\n                        brush = Brush.verticalGradient(\n                            Pair(0f, Color.Transparent),\n                            Pair(0.15f, Color.White),\n                            Pair(0.85f, Color.White),\n                            Pair(1f, Color.Transparent)\n                        ),\n                        blendMode = BlendMode.DstIn\n                    )\n                    it.nativeCanvas.restoreToCount(layerId)\n                }\n            },\n        state = lazyListState,\n        contentPadding = PaddingValues(vertical = (cpnLyricHeight * 0.4).convertDp(density))\n    ) {\n\n        itemsIndexed(viewModel.lyricModelList) { index, item ->\n            LyricItem(index, item, viewModel)\n        }\n        item {\n            LyricConstructorInfo(data.lyricUser, data.transUser)\n        }\n    }\n}\n\n@Composable\nprivate fun ViewStateTip(tip: String, loadDataBlock: (() -> Unit)? = null) {\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .onClick {\n                loadDataBlock?.invoke()\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Text(text = tip, color = AppColorsProvider.current.secondText, fontSize = 16.sp)\n    }\n}\n\n\n@Composable\nprivate fun LyricConstructorInfo(transUser: LyricContributorBean?, lyricUser: LyricContributorBean?) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(8.dp),\n        verticalArrangement = Arrangement.Center,\n    ) {\n        lyricUser?.let {\n            Text(\n                text = \"歌词贡献者：${it.nickname}\",\n                fontSize = 14.sp,\n                fontWeight = FontWeight.Medium,\n                color = AppColorsProvider.current.secondText,\n            )\n        }\n\n        transUser?.let {\n            Text(\n                text = \"翻译贡献者：${it.nickname}\",\n                fontSize = 14.sp,\n                fontWeight = FontWeight.Medium,\n                color = AppColorsProvider.current.secondText,\n                modifier = Modifier.padding(top = 8.dp)\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun LyricItem(index: Int, lyricModel: LyricModel, viewModel: CpnLyricViewModel) {\n\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(8.dp),\n    ) {\n        lyricModel.lyric?.let {\n            Text(\n                text = it,\n                fontSize = 14.sp,\n                color = if (viewModel.curLyricIndex == index) AppColorsProvider.current.primary else AppColorsProvider.current.secondText,\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n        lyricModel.tLyric?.let {\n            Text(\n                text = it,\n                fontSize = 12.sp,\n                color = if (viewModel.curLyricIndex == index) AppColorsProvider.current.primary else AppColorsProvider.current.secondText,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 8.dp),\n            )\n        }\n    }\n}\n\n\nclass CpnLyricViewModel : BaseViewModel(), IPlayerListener {\n    var curLyricIndex by mutableStateOf(-1)\n    val lyricModelList = mutableListOf<LyricModel>()\n    var curPlayPosition = 0\n\n    init {\n        MusicPlayController.mediaPlayer.addListener(this)\n    }\n\n    override fun onCleared() {\n        MusicPlayController.mediaPlayer.removeListener(this)\n        super.onCleared()\n    }\n\n    fun getLyric(id: Long) = launchFlow(handleSuccessBlock = {\n        lyricModelList.clear()\n        lyricModelList.addAll(LyricUtil.parse(it))\n        curLyricIndex = lyricModelList.indexOfFirst { lyricModel ->\n            curPlayPosition < lyricModel.time\n        } - 1\n    }) {\n        curPlayPosition = 0\n        NCRetrofitClient.getNCApi().getLyric(id)\n    }\n\n    override fun onStatusChanged(status: PlayerStatus) {\n    }\n\n    override fun onProgress(totalDuring: Int, currentPosition: Int, percentage: Float) {\n        curPlayPosition = currentPosition\n        curLyricIndex = lyricModelList.indexOfFirst {\n            currentPosition < it.time\n        } - 1\n        if (currentPosition > (lyricModelList.lastOrNull()?.time ?: 0)) {\n            curLyricIndex = lyricModelList.size - 1\n        }\n    }\n}\n\n\ndata class LyricModel(\n    val time: Long,\n    val lyric: String? = null,\n    var tLyric: String? = null\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/play/CpnMusicPlay.kt",
    "content": "package ui.play\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.MusicPlayController\nimport moe.tlaster.precompose.ui.viewModel\nimport moe.tlaster.precompose.viewmodel.ViewModel\nimport ui.common.AsyncImage\nimport ui.common.theme.AppColorsProvider\n\nconst val DISK_ROTATE_ANIM_CYCLE = 10000\n\n/**\n * 音乐播放组件\n */\n@Composable\nfun CpnMusicPlay(modifier: Modifier) {\n    Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {\n        Box(contentAlignment = Alignment.TopCenter) {\n            Box(modifier = Modifier.padding(top = 80.dp)) {\n                DiskRoundBackground()\n                Disk()\n            }\n            DiskNeedle()\n        }\n        ActionIconRow()\n        Spacer(modifier = Modifier.weight(1f))\n        CommentTitle()\n    }\n\n}\n\n@Composable\nprivate fun DiskRoundBackground() {\n    // 半透明圆形背景\n    Box(\n        modifier = Modifier\n            .width(328.dp)\n            .height(328.dp)\n            .clip(CircleShape)\n            .background(Color(0x55EEEEEE))\n    )\n}\n\n@Composable\nprivate fun BoxScope.Disk() {\n    val viewModel: MusicPlayViewModel = viewModel { MusicPlayViewModel() }\n\n    LaunchedEffect(MusicPlayController.curRealIndex) {\n        viewModel.lastSheetDiskRotateAngleForSnap = 0f\n        viewModel.sheetDiskRotate.snapTo(viewModel.lastSheetDiskRotateAngleForSnap)\n    }\n\n    LaunchedEffect(MusicPlayController.isPlaying()) {\n        if (MusicPlayController.isPlaying()) {\n            viewModel.sheetDiskRotate.animateTo(\n                targetValue = 360f + viewModel.lastSheetDiskRotateAngleForSnap,\n                animationSpec = infiniteRepeatable(\n                    animation = tween(durationMillis = DISK_ROTATE_ANIM_CYCLE, easing = LinearEasing),\n                    repeatMode = RepeatMode.Restart\n                )\n            ) {\n                viewModel.lastSheetDiskRotateAngleForSnap =  viewModel.sheetDiskRotate.value\n            }\n        } else {\n            viewModel.sheetDiskRotate.snapTo(viewModel.lastSheetDiskRotateAngleForSnap)\n            viewModel.sheetDiskRotate.stop()\n        }\n\n    }\n\n    Image(\n        painter = painterResource(\"image/ic_disk_around.webp\"),\n        modifier = Modifier\n            .width(320.dp)\n            .height(320.dp)\n            .align(Alignment.Center),\n        contentDescription = \"\"\n    )\n\n    MusicPlayController.curSongBean?.let {\n        AsyncImage(\n            modifier = Modifier\n                .width(220.dp)\n                .height(220.dp)\n                .clip(CircleShape)\n                .align(Alignment.Center)\n                .border(\n                    width = 2.dp,\n                    color = Color.Black,\n                    shape = CircleShape\n                )\n                .graphicsLayer {\n                    rotationZ = viewModel.sheetDiskRotate.value\n                },\n            url = it.al.picUrl\n        )\n    }\n}\n\n@Composable\nprivate fun DiskNeedle() {\n\n    val needleRotateAnim by animateFloatAsState(\n        targetValue = if (!MusicPlayController.isPlaying()) -25f else 0f,\n        animationSpec = tween(durationMillis = 200, easing = LinearEasing)\n    )\n\n    Image(\n        painter = painterResource(\"image/ic_play_neddle.webp\"),\n        modifier = Modifier\n            .padding(start = 72.dp)\n            .width(114.dp)\n            .height(174.dp)\n            .graphicsLayer(\n                rotationZ = needleRotateAnim,\n                transformOrigin = TransformOrigin(0.164f, 0.109f)\n            ),\n        contentDescription = \"\"\n    )\n}\n\n@Composable\nprivate fun ActionIconRow() {\n    Row(modifier = Modifier.padding(top = 20.dp)) {\n        ActionIcon(\"image/ic_like.webp\")\n        ActionIcon(\"image/ic_collect.webp\")\n        ActionIcon(\"image/ic_download.webp\")\n        ActionIcon(\"image/ic_share.webp\")\n\n    }\n}\n\n@Composable\nprivate fun ActionIcon(icon: String) {\n    Box(\n        modifier = Modifier.padding(horizontal = 20.dp).background(AppColorsProvider.current.background, CircleShape)\n            .size(44.dp),\n        contentAlignment = Alignment.Center\n    ) {\n        Icon(\n            painter = painterResource(icon),\n            contentDescription = null,\n            modifier = Modifier.size(20.dp),\n            tint = AppColorsProvider.current.firstIcon\n        )\n    }\n\n}\n\n@Composable\nprivate fun CommentTitle() {\n    Text(\n        text = \"听友评论\",\n        fontSize = 18.sp,\n        fontWeight = FontWeight.Bold,\n        color = AppColorsProvider.current.firstText,\n        modifier = Modifier.padding(start = 50.dp, bottom = 10.dp).fillMaxWidth()\n    )\n}\n\nclass MusicPlayViewModel : ViewModel() {\n\n    // disk旋转动画\n    val sheetDiskRotate by mutableStateOf(Animatable(0f))\n\n    // 上一次disk旋转角度\n    var lastSheetDiskRotateAngleForSnap = 0f\n\n    // 是否抬起磁针\n    var sheetNeedleUp by mutableStateOf(true)\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/play/CpnSongInfo.kt",
    "content": "package ui.play\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.MusicPlayController\nimport ui.common.theme.AppColorsProvider\n\n@Composable\nfun ColumnScope.CpnSongInfo() {\n    MusicPlayController.curSongBean?.let { songBean ->\n        Text(songBean.name, color = AppColorsProvider.current.firstText, fontWeight = FontWeight.Bold, fontSize = 20.sp, modifier = Modifier.padding(top = 30.dp))\n        Row(modifier = Modifier.padding(top = 16.dp)) {\n            InfoItem(\"专辑：\", songBean.al.name)\n            InfoItem(\"歌手：\", songBean.ar.getOrNull(0)?.name ?: \"未知歌手\")\n        }\n    }\n}\n\n@Composable\nfun RowScope.InfoItem(key: String, value: String) {\n    Row(modifier = Modifier.weight(1f)) {\n        Text(key, color = AppColorsProvider.current.secondText, fontSize = 12.sp)\n        Text(value, color = Color(0xFF5C8DD6), fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/playlist/PlayListDetailPage.kt",
    "content": "package ui.playlist\n\nimport androidx.compose.foundation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Divider\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport model.SimplePlayListItem\nimport moe.tlaster.precompose.ui.viewModel\nimport ui.common.AsyncImage\nimport ui.common.CommonTabLayout\nimport ui.common.CommonTabLayoutStyle\nimport ui.common.ExpandableText\nimport ui.common.theme.AppColorsProvider\nimport ui.main.cpn.CommonTitleBar\nimport ui.playlist.cpn.*\nimport util.StringUtil\nimport util.TimeUtil\nimport base.BaseViewModel\n\n/**\n * 歌单详情页面\n */\n@Composable\nfun PlayListDetailPage(simplePlayListItem: SimplePlayListItem) {\n    val lazyListState = rememberLazyListState()\n    var showStickyHeader by remember { mutableStateOf(false) }\n    val detailPageViewModel = viewModel { PlayListDetailPageViewModel() }\n    val trackListViewModel = viewModel { TrackListViewModel() }\n    val commentViewModel = viewModel { CommentListViewModel() }\n\n    val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } }\n    LaunchedEffect(Unit) {\n        snapshotFlow { firstVisibleItemIndex }\n            .collect { firstVisibleItemIndex ->\n                showStickyHeader = firstVisibleItemIndex > 0 && detailPageViewModel.selectedTabIndex.value == 0\n            }\n    }\n\n    // 首次获取数据\n    LaunchedEffect(detailPageViewModel.selectedTabIndex.value) {\n        when (detailPageViewModel.selectedTabIndex.value) {\n            0 -> {  // 歌曲列表\n                trackListViewModel.fetchData(simplePlayListItem.id, true)\n            }\n\n            1 -> {  // 评论\n                commentViewModel.fetchDataPaging(\"playlist\", simplePlayListItem.id, 1, true)\n            }\n\n        }\n    }\n    val trackListViewState = trackListViewModel.flow?.collectAsState()\n    val commentViewState = commentViewModel.flow?.collectAsState()\n\n    Column {\n        CommonTitleBar(\"歌单详情\", showBackButton = true)\n\n        Box {\n            LazyColumn(state = lazyListState) {\n                item {\n                    HeadInfo(simplePlayListItem)\n                    TabBar()\n                }\n\n                when (detailPageViewModel.selectedTabIndex.value) {\n                    0 -> {  // 歌曲列表\n                        CpnTrackList(trackListViewState?.value) {\n                            trackListViewModel.fetchData(simplePlayListItem.id)\n                        }\n                    }\n\n                    1 -> {  // 评论\n                        CpnCommentList(commentViewState?.value, commentViewModel) { curPage ->\n                            commentViewModel.fetchDataPaging(\"playlist\", simplePlayListItem.id, curPage)\n                        }\n                    }\n\n                    2 -> {  // 收藏者\n                        CpnPlayListSubscribers(trackListViewModel.playlistDetailResult?.playlist?.subscribers)\n                    }\n                }\n\n            }\n            if (showStickyHeader) {\n                StickyHeader(simplePlayListItem)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HeadInfo(simplePlayListItem: SimplePlayListItem) {\n    val viewModel = viewModel { TrackListViewModel() }\n    val playlistDetailResult = viewModel.playlistDetailResult\n    Row(\n        modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp).fillMaxWidth(),\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n\n        AsyncImage(\n            modifier = Modifier.padding(end = 20.dp).size(216.dp).clip(RoundedCornerShape(6.dp)),\n            simplePlayListItem.picUrl\n        )\n\n        Column(modifier = Modifier.weight(1f)) {\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                Box(\n                    modifier = Modifier.padding(end = 6.dp)\n                        .border(BorderStroke(1.dp, color = AppColorsProvider.current.primary), RoundedCornerShape(2.dp))\n                ) {\n                    Text(\n                        \"歌单\",\n                        color = AppColorsProvider.current.primary,\n                        modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp),\n                        fontSize = 14.sp\n                    )\n                }\n                Text(\n                    simplePlayListItem.name,\n                    color = AppColorsProvider.current.firstText,\n                    modifier = Modifier.padding(horizontal = 6.dp, vertical = 4.dp),\n                    fontSize = 20.sp,\n                    fontWeight = FontWeight.Bold\n                )\n            }\n\n            Row(\n                modifier = Modifier.padding(top = 8.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n\n                playlistDetailResult?.playlist?.creator?.let { creator ->\n                    AsyncImage(modifier = Modifier.clip(RoundedCornerShape(50)).size(20.dp), creator.avatarUrl,\n                        \"image/ic_default_avator.webp\",\n                        \"image/ic_default_avator.webp\")\n                    Text(\n                        creator.nickname,\n                        color = AppColorsProvider.current.firstText,\n                        modifier = Modifier.padding(horizontal = 6.dp, vertical = 4.dp),\n                        fontSize = 12.sp\n                    )\n                }\n\n                Text(\n                    \"${TimeUtil.parse(simplePlayListItem.trackNumberUpdateTime)}创建\",\n                    color = AppColorsProvider.current.secondText,\n                    modifier = Modifier.padding(horizontal = 6.dp, vertical = 4.dp),\n                    fontSize = 12.sp\n                )\n            }\n\n            Row(\n                modifier = Modifier.padding(vertical = 14.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                HeadInfoActionButton(\n                    \"播放全部\",\n                    \"image/ic_action_play.webp\",\n                    AppColorsProvider.current.primary,\n                    Color.White,\n                    Color.White\n                )\n                HeadInfoActionButton(\n                    \"收藏(${StringUtil.friendlyNumber(playlistDetailResult?.playlist?.subscribedCount ?: simplePlayListItem.subscribedCount)})\",\n                    \"image/ic_collect.webp\"\n                )\n                HeadInfoActionButton(\n                    \"分享(${StringUtil.friendlyNumber(playlistDetailResult?.playlist?.shareCount ?: simplePlayListItem.shareCount)})\",\n                    \"image/ic_share.webp\"\n                )\n                HeadInfoActionButton(\"下载\", \"image/ic_download.webp\")\n            }\n\n            val tabs = remember(playlistDetailResult) {\n                val sb = StringBuilder()\n                sb.append(\"标签:\")\n                playlistDetailResult?.playlist?.tags?.forEachIndexed { index, tag ->\n                    sb.append(tag)\n                    if (index < playlistDetailResult.playlist.tags.size - 1) {\n                        sb.append(\"/\")\n                    }\n                }\n                sb.toString()\n            }\n\n            Text(\n                tabs,\n                color = AppColorsProvider.current.firstIcon,\n                modifier = Modifier.padding(vertical = 4.dp).padding(end = 20.dp),\n                fontSize = 12.sp\n            )\n\n            Row(\n                modifier = Modifier.padding(top = 2.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    \"歌曲数:${StringUtil.friendlyNumber(simplePlayListItem.trackCount)}\",\n                    color = AppColorsProvider.current.firstIcon,\n                    modifier = Modifier.padding(end = 20.dp),\n                    fontSize = 12.sp\n                )\n\n                Text(\n                    \"播放数:${StringUtil.friendlyNumber(simplePlayListItem.playCount)}\",\n                    color = AppColorsProvider.current.firstIcon,\n                    fontSize = 12.sp\n                )\n            }\n\n            ExpandableText(\n                text = \"简介:${playlistDetailResult?.playlist?.description ?: simplePlayListItem.copywriter ?: \"暂无\"}\",\n                color = AppColorsProvider.current.firstIcon,\n                modifier = Modifier.padding(top = 6.dp).padding(end = 20.dp),\n                fontSize = 12.sp\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HeadInfoActionButton(\n    title: String,\n    icon: String,\n    bgColor: Color = Color.Transparent,\n    textColor: Color = AppColorsProvider.current.firstText,\n    iconTint: Color = AppColorsProvider.current.firstIcon,\n) {\n    Row(\n        modifier = Modifier.padding(end = 8.dp)\n            .clip(RoundedCornerShape(50))\n            .background(bgColor)\n            .border(BorderStroke(1.dp, color = AppColorsProvider.current.divider), RoundedCornerShape(50))\n            .padding(horizontal = 14.dp, vertical = 6.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.Center\n    ) {\n        Icon(\n            painter = painterResource(icon),\n            modifier = Modifier.padding(end = 8.dp).size(20.dp).clip(RoundedCornerShape(6.dp)),\n            contentDescription = \"\",\n            tint = iconTint\n        )\n\n        Text(\n            title,\n            color = textColor,\n            fontSize = 14.sp\n        )\n    }\n\n}\n\n\n@Composable\nprivate fun StickyHeader(simplePlayListItem: SimplePlayListItem) {\n    Column(\n        modifier = Modifier.fillMaxWidth().background(AppColorsProvider.current.pure)\n            .padding(start = 30.dp, top = 10.dp, end = 30.dp)\n    ) {\n        Text(\n            simplePlayListItem.name,\n            color = AppColorsProvider.current.firstText,\n            fontSize = 20.sp,\n            fontWeight = FontWeight.Bold\n        )\n\n        Row(modifier = Modifier.padding(vertical = 12.dp)) {\n            Icon(\n                painter = painterResource(\"image/ic_action_play.webp\"),\n                modifier = Modifier.padding(end = 30.dp).size(24.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.primary\n            )\n            Icon(\n                painter = painterResource(\"image/ic_collect.webp\"),\n                modifier = Modifier.padding(end = 30.dp).size(22.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.firstIcon\n            )\n            Icon(\n                painter = painterResource(\"image/ic_share.webp\"),\n                modifier = Modifier.padding(end = 30.dp).size(22.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.firstIcon\n            )\n            Icon(\n                painter = painterResource(\"image/ic_download.webp\"),\n                modifier = Modifier.padding(end = 30.dp).size(22.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.firstIcon\n            )\n        }\n\n        Divider(modifier = Modifier.fillMaxWidth(), color = AppColorsProvider.current.divider)\n    }\n}\n\n\n@Composable\nprivate fun TabBar() {\n    val datailViewModel = viewModel { PlayListDetailPageViewModel() }\n    val trackListViewModel = viewModel { TrackListViewModel() }\n    val tabs = remember(trackListViewModel.playlistDetailResult) {\n        listOf(\n            \"歌曲列表\",\n            \"评论(${StringUtil.friendlyNumber(trackListViewModel.playlistDetailResult?.playlist?.commentCount)})\",\n            \"收藏者\"\n        )\n    }\n    CommonTabLayout(\n        selectedIndex = datailViewModel.selectedTabIndex.value, tabTexts = tabs,\n        backgroundColor = Color.Transparent,\n        style = CommonTabLayoutStyle(modifier = Modifier.height(40.dp), showIndicator = true)\n    ) {\n        datailViewModel.selectedTabIndex.value = it\n    }\n    Divider(\n        modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),\n        color = AppColorsProvider.current.divider,\n        thickness = 1.dp\n    )\n}\n\nclass PlayListDetailPageViewModel : BaseViewModel() {\n    val selectedTabIndex = mutableStateOf(0)\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/playlist/cpn/CpnPlayListCommentList.kt",
    "content": "package ui.playlist.cpn\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Divider\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport model.CommentBean\nimport model.CommentResult\nimport http.NCRetrofitClient\nimport ui.common.AsyncImage\nimport ui.common.PaingFooterNumBar\nimport ui.common.handleListContent\nimport ui.common.theme.AppColorsProvider\nimport util.TimeUtil\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\n\n/**\n * 评论组件\n */\nfun LazyListScope.CpnCommentList(\n    viewState: ViewState<CommentResult>?,\n    viewModel: CommentListViewModel,\n    reloadCallback: (curPage: Int) -> Unit\n) {\n    handleListContent(viewState,\n        reloadDataBlock = {\n            reloadCallback.invoke(viewModel.cutPage)\n        }) { data ->\n        items(data.comments.size) {\n            CommentItem(data.comments[it])\n        }\n\n        // 底部分页组件\n        if (data.total > CommentListViewModel.pageSize) {\n            item {\n                PaingFooterNumBar(data.total, CommentListViewModel.pageSize, viewModel.cutPage) {\n                    reloadCallback.invoke(it)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CommentItem(commentBean: CommentBean) {\n    Row(modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)) {\n        AsyncImage(\n            modifier = Modifier.padding(end = 10.dp).size(48.dp).clip(RoundedCornerShape(50)),\n            url = commentBean.user.avatarUrl,\n            \"image/ic_default_avator.webp\"\n        )\n        Column {\n            Text(\n                text = buildAnnotatedString {\n                    withStyle(style = SpanStyle(color = Color(0xFF5C8DD6), fontSize = 14.sp)) {\n                        append(commentBean.user.nickname)\n                    }\n                    withStyle(style = SpanStyle(color = AppColorsProvider.current.firstText, fontSize = 14.sp)) {\n                        append(\" : ${commentBean.content}\")\n                    }\n                },\n            )\n\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                Text(\n                    text = TimeUtil.parse(commentBean.time),\n                    color = AppColorsProvider.current.thirdText,\n                    fontSize = 12.sp,\n                    modifier = Modifier.padding(vertical = 8.dp).weight(1f)\n                )\n\n                Icon(\n                    painter = painterResource(\"image/ic_thumbs_up.webp\"),\n                    modifier = Modifier.size(14.dp),\n                    contentDescription = \"\",\n                    tint = AppColorsProvider.current.secondText\n                )\n\n                Divider(\n                    modifier = Modifier.padding(horizontal = 14.dp).width(0.5.dp),\n                    color = AppColorsProvider.current.divider,\n                    thickness = 10.dp\n                )\n\n                Icon(\n                    painter = painterResource(\"image/ic_share.webp\"),\n                    modifier = Modifier.size(14.dp),\n                    contentDescription = \"\",\n                    tint = AppColorsProvider.current.secondIcon\n                )\n\n                Divider(\n                    modifier = Modifier.padding(horizontal = 14.dp).width(0.5.dp),\n                    color = AppColorsProvider.current.divider,\n                    thickness = 10.dp\n                )\n\n                Icon(\n                    painter = painterResource(\"image/ic_comment.webp\"),\n                    modifier = Modifier.size(14.dp),\n                    contentDescription = \"\",\n                    tint = AppColorsProvider.current.secondIcon\n                )\n\n            }\n\n\n            Divider(modifier = Modifier.padding(top = 8.dp).fillMaxWidth(), color = AppColorsProvider.current.divider, thickness = 0.5.dp)\n        }\n\n    }\n}\n\nclass CommentListViewModel : BaseViewModel() {\n    companion object {\n        const val pageSize = 20\n    }\n\n    var cutPage by mutableStateOf(1)\n    var flow by mutableStateOf<ViewStateMutableStateFlow<CommentResult>?>(null)\n\n    fun fetchDataPaging(commentType: String, id: Long, curPage: Int, firstLoad: Boolean = false) {\n        if (!firstLoad || flow == null) {\n            cutPage = curPage\n            val offset = (curPage - 1) * pageSize\n            flow = launchFlow {\n                println(\"获取${commentType}评论.  curPage=${curPage}\")\n                NCRetrofitClient.getNCApi().getCommentList(commentType, id, pageSize, offset)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/playlist/cpn/CpnPlayListSubscribers.kt",
    "content": "package ui.playlist.cpn\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport model.Subscribers\nimport ui.common.AsyncImage\nimport ui.common.ListToGridItems\nimport ui.common.NoSuccessComponent\nimport ui.common.theme.AppColorsProvider\nimport util.StringUtil\n\n/**\n * 订阅者组件\n */\nfun LazyListScope.CpnPlayListSubscribers(subscribes: List<Subscribers>?) {  //subscribes: List<Subscribers>\n\n    if (subscribes.isNullOrEmpty()) {\n        item {\n            NoSuccessComponent()\n        }\n    } else {\n        ListToGridItems(subscribes, 2) { _, item ->\n            SubscribersItem(item)\n        }\n    }\n\n}\n\n@Composable\nprivate fun SubscribersItem(item: Subscribers) {\n    Row(\n        modifier = Modifier.padding(start = 20.dp).fillMaxWidth().height(120.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        AsyncImage(\n            modifier = Modifier.padding(end = 10.dp).size(90.dp).clip(RoundedCornerShape(50)),\n            item.avatarUrl,\n            \"image/ic_default_avator.webp\",\n            \"image/ic_default_avator.webp\"\n        )\n        Column {\n            Text(\n                item.nickname, color = AppColorsProvider.current.firstText, fontSize = 14.sp, maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            if (!StringUtil.isEmpty(item.description)) {\n                Text(\n                    item.description!!, color = AppColorsProvider.current.firstText, fontSize = 12.sp,\n                    maxLines = 3,\n                    overflow = TextOverflow.Ellipsis,\n                    modifier = Modifier.padding(top = 10.dp, end = 10.dp)\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/playlist/cpn/CpnTrackList.kt",
    "content": "package ui.playlist.cpn\n\nimport androidx.compose.foundation.background\nimport ui.common.onClick\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.material.Icon\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport base.MusicPlayController\nimport http.NCRetrofitClient\nimport model.PlaylistDetailResult\nimport model.SongBean\nimport model.SongDetailResult\nimport ui.common.handleListContent\nimport ui.common.theme.AppColorsProvider\nimport base.BaseViewModel\nimport base.ViewState\nimport base.ViewStateMutableStateFlow\n\n/**\n * 歌曲列表组件\n */\nfun LazyListScope.CpnTrackList(\n    viewState: ViewState<SongDetailResult>?,\n    reloadCallback: () -> Unit\n) {\n    handleListContent(viewState,\n        reloadDataBlock = {\n            reloadCallback.invoke()\n        }) { data ->\n        item {\n            TrackHeaderBar()\n        }\n        items(data.songs.size) {\n            TrackItem(data.songs, it)\n        }\n    }\n}\n\n@Composable\nprivate fun TrackHeaderBar() {\n    Row(\n        modifier = Modifier.height(36.dp).fillMaxWidth().padding(horizontal = 20.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Text(\n            modifier = Modifier.weight(4f),\n            text = \"音乐标题\",\n            color = AppColorsProvider.current.thirdText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n        Text(\n            modifier = Modifier.weight(2f),\n            text = \"歌手\",\n            color = AppColorsProvider.current.thirdText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n        Text(\n            modifier = Modifier.weight(2f),\n            text = \"专辑\",\n            color = AppColorsProvider.current.thirdText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n        Text(\n            modifier = Modifier.weight(1f),\n            text = \"时长\",\n            color = AppColorsProvider.current.thirdText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\n\n@Composable\nprivate fun TrackItem(songList: List<SongBean>,  index: Int) {\n    val curSongBean = songList[index]\n    Row(\n        modifier = Modifier\n            .onClick  {\n                MusicPlayController.playMusicList(songList, index)\n            }\n            .background(\n            if ((index + 1) % 2 == 0) Color.Transparent else AppColorsProvider.current.divider.copy(\n                0.25f\n            )\n        ).height(36.dp).fillMaxWidth().padding(horizontal = 20.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Row(modifier = Modifier.weight(4f).fillMaxHeight(), verticalAlignment = Alignment.CenterVertically) {\n            val num = if (index + 1 < 10) \"0${index + 1}\" else (index + 1).toString()\n            if (MusicPlayController.curSongBean?.id != curSongBean.id) {\n                Text(\n                    text = num,\n                    color = AppColorsProvider.current.thirdText,\n                    fontSize = 12.sp,\n                    modifier = Modifier.width(40.dp),\n                    textAlign = TextAlign.Center\n                )\n            } else {\n                Icon(\n                    painterResource(\"image/ic_playing.webp\"),\n                    contentDescription = null,\n                    modifier = Modifier.width(40.dp).height(36.dp).padding(horizontal = 14.dp, vertical = 12.dp),\n                    tint = AppColorsProvider.current.primary\n                )\n            }\n\n            Icon(\n                painter = painterResource(\"image/ic_like.webp\"),\n                modifier = Modifier.padding(end = 8.dp).size(14.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.secondText\n            )\n\n            Icon(\n                painter = painterResource(\"image/ic_download.webp\"),\n                modifier = Modifier.padding(end = 10.dp).size(14.dp),\n                contentDescription = \"\",\n                tint = AppColorsProvider.current.secondIcon\n            )\n\n            Text(\n                text = curSongBean.name,\n                color = AppColorsProvider.current.firstText,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                fontSize = 12.sp,\n                textAlign = TextAlign.Center\n            )\n        }\n        Text(\n            modifier = Modifier.weight(2f),\n            text = curSongBean.ar.getOrNull(0)?.name ?: \"未知歌手\",\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Text(\n            modifier = Modifier.weight(2f),\n            text = curSongBean.al.name,\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Text(\n            modifier = Modifier.weight(1f),\n            text = curSongBean.getSongTimeLength(),\n            color = AppColorsProvider.current.secondText,\n            fontSize = 12.sp,\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\nclass TrackListViewModel : BaseViewModel() {\n    var flow by mutableStateOf<ViewStateMutableStateFlow<SongDetailResult>?>(null)\n    var playlistDetailResult by mutableStateOf<PlaylistDetailResult?>(null)\n    fun fetchData(id: Long, firstLoad: Boolean = false) {\n        if (!firstLoad || flow == null) {\n            flow = launchFlow {\n                val playlistDetailResult = NCRetrofitClient.getNCApi().getPlaylistDetail(id)\n                this.playlistDetailResult = playlistDetailResult\n                val trackIdBeans = playlistDetailResult.playlist.trackIds\n                val ids = StringBuilder()\n                if (trackIdBeans != null) {\n                    val size = trackIdBeans.size\n                    for (i in 0 until size) {\n                        //最后一个参数不加逗号\n                        if (i == size - 1) {\n                            ids.append(trackIdBeans[i].id)\n                        } else {\n                            ids.append(trackIdBeans[i].id).append(\",\")\n                        }\n                    }\n                }\n                NCRetrofitClient.getNCApi().getSongDetail(ids.toString())\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/setting/SettingPage.kt",
    "content": "package ui.setting\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport ui.common.theme.AppColorsProvider\nimport ui.main.cpn.CommonTitleBar\n\n@Composable\nfun SettingPage() {\n    Column(modifier = Modifier.fillMaxSize()) {\n        CommonTitleBar(\"设置\", true)\n        Text(\n            text = \"声明\",\n            modifier = Modifier.padding(20.dp),\n            fontSize = 18.sp,\n            color = AppColorsProvider.current.firstText,\n            fontWeight = FontWeight.Bold\n        )\n        Text(\n            text = \"本应用非 网易云音乐 官方产品，内部所有资源来自互联网，仅作学习分享使用，他人如何使用此应用与本应用无关。\",\n            modifier = Modifier.padding(horizontal = 20.dp),\n            fontSize = 14.sp,\n            color = AppColorsProvider.current.secondText,\n        )\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/Shape.kt",
    "content": "package ui.common.theme\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.Shapes\nimport androidx.compose.ui.unit.dp\n\nval Shapes = Shapes(\n    small = RoundedCornerShape(4.dp),\n    medium = RoundedCornerShape(8.dp),\n    large = RoundedCornerShape(12.dp)\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/Theme.kt",
    "content": "package ui.common.theme\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.TweenSpec\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material.MaterialTheme\nimport androidx.compose.material.MaterialTheme.shapes\nimport androidx.compose.runtime.*\nimport ui.common.theme.color.AppColors\nimport ui.common.theme.color.light.*\n\n\n// 夜间模式\nconst val THEME_NIGHT = -1\n\n// 默认主题\nconst val THEME_DEFAULT = 0\n\n// 蓝色主题\nconst val THEME_BLUE = 1\n\n// 绿色主题\nconst val THEME_GREEN = 2\n\n// 橙色主题\nconst val THEME_ORIGIN = 3\n\n// 紫色主题\nconst val THEME_PURPLE = 4\n\n// 黄色主题\nconst val THEME_YELLOW = 5\n\n/**\n * 主题状态\n */\nval themeTypeState: MutableState<Int> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {\n    mutableStateOf(getDefaultThemeId())\n}\n\nvar AppColorsProvider = compositionLocalOf {\n    DefaultColorPalette\n}\n\n/**\n * 获取当前默认主题\n */\nfun getDefaultThemeId(): Int = THEME_DEFAULT\n\nconst val TWEEN_DURATION = 200\n\n\n@Composable\n@ReadOnlyComposable\nfun isInDarkTheme(): Boolean {\n    return isSystemInDarkTheme() || themeTypeState.value == THEME_NIGHT\n}\n\n@Composable\nfun AppTheme(\n    themeType: Int,\n    isDark: Boolean = isInDarkTheme(),\n    content: @Composable () -> Unit\n) {\n\n    val targetColors = if (isDark) DarkColorPalette else {\n        when (themeType) {\n            THEME_BLUE -> BlueColorPalette\n            THEME_GREEN -> GreenColorPalette\n            THEME_ORIGIN -> OriginColorPalette\n            THEME_PURPLE -> PurpleColorPalette\n            THEME_YELLOW -> YellowColorPalette\n            else -> DefaultColorPalette\n        }\n    }\n\n    val topBarColor = animateColorAsState(targetColors.topBarColor, TweenSpec(TWEEN_DURATION))\n    val pure = animateColorAsState(targetColors.pure, TweenSpec(TWEEN_DURATION))\n    val primary = animateColorAsState(targetColors.primary, TweenSpec(TWEEN_DURATION))\n    val primaryVariant = animateColorAsState(targetColors.primaryVariant, TweenSpec(TWEEN_DURATION))\n    val secondary = animateColorAsState(targetColors.secondary, TweenSpec(TWEEN_DURATION))\n    val background = animateColorAsState(targetColors.background, TweenSpec(TWEEN_DURATION))\n    val firstText = animateColorAsState(targetColors.firstText, TweenSpec(TWEEN_DURATION))\n    val secondText = animateColorAsState(targetColors.secondText, TweenSpec(TWEEN_DURATION))\n    val thirdText = animateColorAsState(targetColors.thirdText, TweenSpec(TWEEN_DURATION))\n    val firstIcon = animateColorAsState(targetColors.firstIcon, TweenSpec(TWEEN_DURATION))\n    val secondIcon = animateColorAsState(targetColors.secondIcon, TweenSpec(TWEEN_DURATION))\n    val thirdIcon = animateColorAsState(targetColors.thirdIcon, TweenSpec(TWEEN_DURATION))\n    val card = animateColorAsState(targetColors.card, TweenSpec(TWEEN_DURATION))\n    val divider = animateColorAsState(targetColors.divider, TweenSpec(TWEEN_DURATION))\n\n    val appColors = AppColors(\n        topBar = topBarColor.value,\n        pure = pure.value,\n        primary = primary.value,\n        primaryVariant = primaryVariant.value,\n        secondary = secondary.value,\n        background = background.value,\n        firstText = firstText.value,\n        secondText = secondText.value,\n        thirdText = thirdText.value,\n        firstIcon = firstIcon.value,\n        secondIcon = secondIcon.value,\n        thirdIcon = thirdIcon.value,\n        card = card.value,\n        divider = divider.value\n    )\n\n    CompositionLocalProvider(AppColorsProvider provides appColors) {\n        MaterialTheme(\n            shapes = shapes\n        ) {\n            content()\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/AppColors.kt",
    "content": "package ui.common.theme.color\n\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.Color\n\n/**\n * Created by ssk on 2022/4/17.\n */\n@Stable\nclass AppColors(\n    topBar: Color,\n    pure: Color,\n    primary: Color,\n    primaryVariant: Color,\n    secondary: Color,\n    background: Color,\n    firstText: Color,\n    secondText: Color,\n    thirdText: Color,\n    firstIcon: Color,\n    secondIcon: Color,\n    thirdIcon: Color,\n    card: Color,\n    divider: Color\n) {\n    var topBarColor: Color by mutableStateOf(topBar)\n        internal set\n    var pure : Color by mutableStateOf(pure)\n        internal set\n    var primary: Color by mutableStateOf(primary)\n        internal set\n    var primaryVariant: Color by mutableStateOf(primaryVariant)\n        internal set\n    var secondary: Color by mutableStateOf(secondary)\n        internal set\n    var background: Color by mutableStateOf(background)\n        private set\n    var firstText: Color by mutableStateOf(firstText)\n        private set\n    var secondText: Color by mutableStateOf(secondText)\n        private set\n    var thirdText: Color by mutableStateOf(thirdText)\n        private set\n    var firstIcon: Color by mutableStateOf(firstIcon)\n        private set\n    var secondIcon: Color by mutableStateOf(secondIcon)\n        private set\n    var thirdIcon: Color by mutableStateOf(thirdIcon)\n        private set\n    var card: Color by mutableStateOf(card)\n        private set\n    var divider: Color by mutableStateOf(divider)\n        private set\n}"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/dark/DartColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n\n/**\n * Created by ssk on 2022/4/17.\n */\nval DarkColorPalette = AppColors(\n    topBar = Color(0xFF222222),\n    pure = Color(0xFF000000),\n    primary = Color(0xFFF0484E),\n    primaryVariant = Color(0xFFEC3037),\n    secondary = Color(0xFFF0888C),\n    background = Color(0xFF222222),\n    firstText = Color(0xFFFFFFFF),\n    secondText = Color(0xFFBBBBBB),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFFFFFFFF),\n    secondIcon = Color(0xFFBBBBBB),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFF333333),\n    divider = Color(0xFF555555),\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/BlueColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n\n/**\n * Created by ssk on 2022/4/17.\n */\nval BlueColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFF3050EE),\n    primaryVariant = Color(0xFF102DB9),\n    secondary = Color(0xFF789BF1),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n    )"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/DefaultColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n/**\n * Created by ssk on 2022/4/17.\n */\nval DefaultColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFFF0484E),\n    primaryVariant = Color(0xFFEC3037),\n    secondary = Color(0xFFF0888C),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/GreenColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n\n/**\n * Created by ssk on 2022/4/17.\n */\nval GreenColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFF3EC73E),\n    primaryVariant = Color(0xFF129912),\n    secondary = Color(0xFF9FE69F),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n    )"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/OriginColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n\n/**\n * Created by ssk on 2022/4/17.\n */\nval OriginColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFFFF6633),\n    primaryVariant = Color(0xFFD6410F),\n    secondary = Color(0xFFF3906F),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/PurpleColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n/**\n * Created by ssk on 2022/5/13.\n */\nval PurpleColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFFEE3DEE),\n    primaryVariant = Color(0xFFB917B9),\n    secondary = Color(0xFFEC8FEC),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n)"
  },
  {
    "path": "src/jvmMain/kotlin/ui/theme/color/palette/light/YellowColorPalette.kt",
    "content": "package ui.common.theme.color.light\n\nimport androidx.compose.ui.graphics.Color\nimport ui.common.theme.color.AppColors\n\n/**\n * Created by ssk on 2022/4/17.\n */\nval YellowColorPalette = AppColors(\n    topBar = Color(0xFFF3F3F3),\n    pure = Color(0xFFFFFFFF),\n    primary = Color(0xFFFFF143),\n    primaryVariant = Color(0xFFC7B917),\n    secondary = Color(0xFFF8F8AF),\n    background = Color(0xFFEEEEEE),\n    firstText = Color(0xFF333333),\n    secondText = Color(0xFF666666),\n    thirdText = Color(0xFF999999),\n    firstIcon = Color(0xFF333333),\n    secondIcon = Color(0xFF666666),\n    thirdIcon = Color(0xFF999999),\n    card = Color(0xFFFFFFFF),\n    divider = Color(0xFFDDDDDD),\n    )"
  },
  {
    "path": "src/jvmMain/kotlin/ui/todo/TestPage.kt",
    "content": "package ui.todo\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.layout.*\n\n@Composable\nfun TestPage() {\n\n}\n\n\n"
  },
  {
    "path": "src/jvmMain/kotlin/ui/todo/TodoPage.kt",
    "content": "package ui.todo\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport ui.common.theme.AppColorsProvider\nimport ui.main.cpn.CommonTitleBar\n\n@Composable\nfun TodoPage(tag: String, showTitle: Boolean = true) {\n    Column {\n        if (showTitle) {\n            CommonTitleBar(tag, true)\n        }\n        Box(Modifier.fillMaxSize().background(AppColorsProvider.current.pure),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\"todo-${tag}\", color = AppColorsProvider.current.firstText)\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/DataStoreUtils.kt",
    "content": "package util\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.*\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.catch\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.runBlocking\nimport java.io.IOException\n\n\nobject DataStoreUtils {\n\n    /**\n     * 此文件路径可进行修改，但后缀名不可进行修改\n     * System.getProperty(\"user.dir\") 可以获取到当前路径\n     */\n    private val dataStore: DataStore<Preferences> = getDataStore()\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <U> getSyncData(key: String, default: U): U {\n        val res = when (default) {\n            is Long -> readLongData(key, default)\n            is String -> readStringData(key, default)\n            is Int -> readIntData(key, default)\n            is Boolean -> readBooleanData(key, default)\n            is Float -> readFloatData(key, default)\n            else -> throw IllegalArgumentException(\"This type can be saved into DataStore\")\n        }\n        return res as U\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <U> getData(key: String, default: U): Flow<U> {\n        val data = when (default) {\n            is Long -> readLongFlow(key, default)\n            is String -> readStringFlow(key, default)\n            is Int -> readIntFlow(key, default)\n            is Boolean -> readBooleanFlow(key, default)\n            is Float -> readFloatFlow(key, default)\n            else -> throw IllegalArgumentException(\"This type can be saved into DataStore\")\n        }\n        return data as Flow<U>\n    }\n\n    suspend fun <U> putData(key: String, value: U) {\n        when (value) {\n            is Long -> saveLongData(key, value)\n            is String -> saveStringData(key, value)\n            is Int -> saveIntData(key, value)\n            is Boolean -> saveBooleanData(key, value)\n            is Float -> saveFloatData(key, value)\n            else -> throw IllegalArgumentException(\"This type can be saved into DataStore\")\n        }\n    }\n\n    fun <U> putSyncData(key: String, value: U) {\n        when (value) {\n            is Long -> saveSyncLongData(key, value)\n            is String -> saveSyncStringData(key, value)\n            is Int -> saveSyncIntData(key, value)\n            is Boolean -> saveSyncBooleanData(key, value)\n            is Float -> saveSyncFloatData(key, value)\n            else -> throw IllegalArgumentException(\"This type can be saved into DataStore\")\n        }\n    }\n\n    fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =\n        dataStore.data\n            .catch {\n                //当读取数据遇到错误时，如果是 `IOException` 异常，发送一个 emptyPreferences 来重新使用\n                //但是如果是其他的异常，最好将它抛出去，不要隐藏问题\n                if (it is IOException) {\n                    it.printStackTrace()\n                    emit(emptyPreferences())\n                } else {\n                    throw it\n                }\n            }.map {\n                it[booleanPreferencesKey(key)] ?: default\n            }\n\n    fun readBooleanData(key: String, default: Boolean = false): Boolean {\n        var value = false\n        runBlocking {\n            dataStore.data.first {\n                value = it[booleanPreferencesKey(key)] ?: default\n                true\n            }\n        }\n        return value\n    }\n\n    fun readIntFlow(key: String, default: Int = 0): Flow<Int> =\n        dataStore.data\n            .catch {\n                if (it is IOException) {\n                    it.printStackTrace()\n                    emit(emptyPreferences())\n                } else {\n                    throw it\n                }\n            }.map {\n                it[intPreferencesKey(key)] ?: default\n            }\n\n    fun readIntData(key: String, default: Int = 0): Int {\n        var value = 0\n        runBlocking {\n            dataStore.data.first {\n                value = it[intPreferencesKey(key)] ?: default\n                true\n            }\n        }\n        return value\n    }\n\n    fun readStringFlow(key: String, default: String = \"\"): Flow<String> =\n        dataStore.data\n            .catch {\n                if (it is IOException) {\n                    it.printStackTrace()\n                    emit(emptyPreferences())\n                } else {\n                    throw it\n                }\n            }.map {\n                it[stringPreferencesKey(key)] ?: default\n            }\n\n    fun readStringData(key: String, default: String = \"\"): String {\n        var value = \"\"\n        runBlocking {\n            dataStore.data.first {\n                value = it[stringPreferencesKey(key)] ?: default\n                true\n            }\n        }\n        return value\n    }\n\n    fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =\n        dataStore.data\n            .catch {\n                if (it is IOException) {\n                    it.printStackTrace()\n                    emit(emptyPreferences())\n                } else {\n                    throw it\n                }\n            }.map {\n                it[floatPreferencesKey(key)] ?: default\n            }\n\n    fun readFloatData(key: String, default: Float = 0f): Float {\n        var value = 0f\n        runBlocking {\n            dataStore.data.first {\n                value = it[floatPreferencesKey(key)] ?: default\n                true\n            }\n        }\n        return value\n    }\n\n    fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =\n        dataStore.data\n            .catch {\n                if (it is IOException) {\n                    it.printStackTrace()\n                    emit(emptyPreferences())\n                } else {\n                    throw it\n                }\n            }.map {\n                it[longPreferencesKey(key)] ?: default\n            }\n\n    fun readLongData(key: String, default: Long = 0L): Long {\n        var value = 0L\n        runBlocking {\n            dataStore.data.first {\n                value = it[longPreferencesKey(key)] ?: default\n                true\n            }\n        }\n        return value\n    }\n\n    suspend fun saveBooleanData(key: String, value: Boolean) {\n        dataStore.edit { mutablePreferences ->\n            mutablePreferences[booleanPreferencesKey(key)] = value\n        }\n    }\n\n    fun saveSyncBooleanData(key: String, value: Boolean) =\n        runBlocking { saveBooleanData(key, value) }\n\n    suspend fun saveIntData(key: String, value: Int) {\n        dataStore.edit { mutablePreferences ->\n            mutablePreferences[intPreferencesKey(key)] = value\n        }\n    }\n\n    fun saveSyncIntData(key: String, value: Int) = runBlocking { saveIntData(key, value) }\n\n    suspend fun saveStringData(key: String, value: String) {\n        dataStore.edit { mutablePreferences ->\n            mutablePreferences[stringPreferencesKey(key)] = value\n        }\n    }\n\n    fun saveSyncStringData(key: String, value: String) = runBlocking { saveStringData(key, value) }\n\n    suspend fun saveFloatData(key: String, value: Float) {\n        dataStore.edit { mutablePreferences ->\n            mutablePreferences[floatPreferencesKey(key)] = value\n        }\n    }\n\n    fun saveSyncFloatData(key: String, value: Float) = runBlocking { saveFloatData(key, value) }\n\n    suspend fun saveLongData(key: String, value: Long) {\n        dataStore.edit { mutablePreferences ->\n            mutablePreferences[longPreferencesKey(key)] = value\n        }\n    }\n\n    fun saveSyncLongData(key: String, value: Long) = runBlocking { saveLongData(key, value) }\n\n    suspend fun clear() {\n        dataStore.edit {\n            it.clear()\n        }\n    }\n\n    fun clearSync() {\n        runBlocking {\n            dataStore.edit {\n                it.clear()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/DensityExt.kt",
    "content": "package util\n\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nfun Number.convertDp(density: Density): Dp {\n    return (toFloat() / density.density).dp\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/EnvUtil.kt",
    "content": "package util\n\nimport java.util.*\n\nobject EnvUtil {\n    val osName = System.getProperty(\"os.name\", \"generic\")\n\n    fun isMac() = osName.lowercase(Locale.getDefault()).contains(\"mac\")\n\n    fun isWindows() = osName.contains(\"indows\")\n\n    fun isLinux() = osName.contains(\"nix\") || osName.contains(\"nux\") || osName.contains(\"aix\")\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/LyricUtil.kt",
    "content": "package util\n\n\nimport model.LyricResult\nimport ui.play.LyricModel\nimport java.util.*\nimport java.util.regex.Matcher\nimport java.util.regex.Pattern\n\n/**\n * Created by ssk on 2022/5/11.\n */\nobject LyricUtil {\n    private val MINUTE_IN_MILLIS = 60000L\n    private val SECOND_IN_MILLIS = 1000L\n    private val PATTERN_LINE = Pattern.compile(\"((\\\\[\\\\d\\\\d:\\\\d\\\\d\\\\.\\\\d{2,3}\\\\])+)(.+)\")\n    private val PATTERN_TIME = Pattern.compile(\"\\\\[(\\\\d\\\\d):(\\\\d\\\\d)\\\\.(\\\\d{2,3})\\\\]\")\n\n    fun parse(lyricResult: LyricResult): List<LyricModel> {\n        val originLrcTexts = lyricResult.lrc?.lyric ?: \"\"\n        val originTLyricTexts = lyricResult.tlyric?.lyric ?: \"\"\n        val lyricModelList = parseLyrics(originLrcTexts)\n        val tLyricModelList = parseTlyrics(originTLyricTexts)\n        lyricModelList.forEach { lyricModel ->\n            tLyricModelList.forEach inner@{ tLyricModel ->\n                if (lyricModel.time == tLyricModel.time) {\n                    lyricModel.tLyric = tLyricModel.tLyric\n                    return@inner\n                }\n            }\n        }\n\n        return lyricModelList\n    }\n\n    /**\n     * 从文本解析歌词\n     */\n    private fun parseLyrics(lyric: String): List<LyricModel> {\n        var lrcText = lyric\n        val entryList = ArrayList<LyricModel>()\n\n        if (!StringUtil.isEmpty(lrcText)) {\n            if (lrcText.startsWith(\"\\uFEFF\")) {\n                lrcText = lrcText.replace(\"\\uFEFF\", \"\")\n            }\n            val array = lrcText.split(\"\\\\n\".toRegex()).toTypedArray()\n            for (line in array) {\n                val list = parseLyricsLine(line)\n                if (list != null && list.isNotEmpty()) {\n                    entryList.addAll(list)\n                }\n            }\n        }\n        return entryList\n    }\n\n    /**\n     * 从文本解析歌词\n     */\n    private fun parseTlyrics(tLyric: String): List<LyricModel> {\n        var tlyric = tLyric\n        val entryList = ArrayList<LyricModel>()\n\n        if (!StringUtil.isEmpty(tlyric)) {\n            if (tlyric.startsWith(\"\\uFEFF\")) {\n                tlyric = tlyric.replace(\"\\uFEFF\", \"\")\n            }\n            val array = tlyric.split(\"\\\\n\".toRegex()).toTypedArray()\n            for (line in array) {\n                val list = parseTLyricsLine(line)\n                if (list != null && list.isNotEmpty()) {\n                    entryList.addAll(list)\n                }\n            }\n        }\n        return entryList\n    }\n\n    /**\n     * 解析一行歌词\n     */\n    private fun parseLyricsLine(line: String): List<LyricModel>? {\n        var line = line\n        if (StringUtil.isEmpty(line)) {\n            return null\n        }\n        line = line.trim { it <= ' ' }\n        // [00:17.65]让我掉下眼泪的\n        val lineMatcher: Matcher = PATTERN_LINE.matcher(line)\n        if (!lineMatcher.matches()) {\n            return null\n        }\n        val times = lineMatcher.group(1)\n        val text = lineMatcher.group(3)\n        val entryList: MutableList<LyricModel> = ArrayList<LyricModel>()\n\n        // [00:17.65]\n        val timeMatcher: Matcher = PATTERN_TIME.matcher(times)\n        while (timeMatcher.find()) {\n            val min = timeMatcher.group(1)!!.toLong()\n            val sec = timeMatcher.group(2)!!.toLong()\n            val milString = timeMatcher.group(3)\n            var mil = milString.toLong()\n            // 如果毫秒是两位数，需要乘以10\n            if (milString.length == 2) {\n                mil *= 10\n            }\n            val time = min * MINUTE_IN_MILLIS + sec * SECOND_IN_MILLIS + mil\n            entryList.add(LyricModel(time, text))\n        }\n        return entryList\n    }\n\n    /**\n     * 解析一行歌词\n     */\n    private fun parseTLyricsLine(line: String): List<LyricModel>? {\n        var line = line\n        if (StringUtil.isEmpty(line)) {\n            return null\n        }\n        line = line.trim { it <= ' ' }\n        // [00:17.65]让我掉下眼泪的\n        val lineMatcher: Matcher = PATTERN_LINE.matcher(line)\n        if (!lineMatcher.matches()) {\n            return null\n        }\n        val times = lineMatcher.group(1)\n        val text = lineMatcher.group(3)\n        val entryList: MutableList<LyricModel> = ArrayList<LyricModel>()\n\n        // [00:17.65]\n        val timeMatcher: Matcher = PATTERN_TIME.matcher(times)\n        while (timeMatcher.find()) {\n            val min = timeMatcher.group(1)!!.toLong()\n            val sec = timeMatcher.group(2)!!.toLong()\n            val milString = timeMatcher.group(3)\n            var mil = milString.toLong()\n            // 如果毫秒是两位数，需要乘以10\n            if (milString.length == 2) {\n                mil *= 10\n            }\n            val time = min * MINUTE_IN_MILLIS + sec * SECOND_IN_MILLIS + mil\n            entryList.add(LyricModel(time, tLyric = text))\n        }\n        return entryList\n    }\n\n    /**\n     * 转为[分:秒]\n     */\n    fun formatTime(milli: Long): String? {\n        val m = (milli / MINUTE_IN_MILLIS).toInt()\n        val s = (milli / SECOND_IN_MILLIS % 60).toInt()\n        val mm = String.format(Locale.getDefault(), \"%02d\", m)\n        val ss = String.format(Locale.getDefault(), \"%02d\", s)\n        return \"$mm:$ss\"\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/QrcodeUtil.kt",
    "content": "package util\n\nimport base.AppConfig\nimport com.google.zxing.BarcodeFormat\nimport com.google.zxing.EncodeHintType\nimport com.google.zxing.MultiFormatWriter\nimport com.google.zxing.WriterException\nimport com.google.zxing.client.j2se.MatrixToImageWriter\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel\nimport java.io.File\nimport java.util.*\n\n/**\n * Created by ssk on 2023/2/8.\n */\nobject QrcodeUtil {\n\n    /**\n     * 创建二维码图片\n     */\n    fun createQrcodeFile(\n        qrcodeStr: String,\n        width: Int = 400,\n        height: Int = 400,\n    ): File? {\n        // 用于设置QR二维码参数\n        val qrParam = Hashtable<EncodeHintType, Any>()\n        // 设置QR二维码的纠错级别——这里选择最高H级别\n        qrParam[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H\n        // 设置编码方式\n        qrParam[EncodeHintType.CHARACTER_SET] = \"UTF-8\"\n\n        try {\n            val bitMatrix = MultiFormatWriter().encode(\n                qrcodeStr,\n                BarcodeFormat.QR_CODE, width, height, qrParam\n            )\n\n//            val  file = File(System.getProperty(\"user.dir\") + File.separator + \"cache\" + File.separator + \"qrcode.png\")\n\n            val file = File(AppConfig.cacheRootDir + File.separator + \"cache\" + File.separator + \"qrcode.png\")\n            if (!file.parentFile.exists()) {\n                file.parentFile.mkdirs()\n            }\n            val path = file.toPath()\n            MatrixToImageWriter.writeToPath(bitMatrix, \"png\", path)\n            return file\n        } catch (e: WriterException) {\n            e.printStackTrace()\n        }\n        return null\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/StringUtil.kt",
    "content": "package util\n\nimport java.util.*\n\n/**\n * Created by ssk on 2022/4/23.\n */\nobject StringUtil {\n\n    fun friendlyNumber(num: Number?): String {\n        if (num != null) {\n            if(num.toLong() < 10000) {\n                return num.toString()\n            }else if(num.toLong() < 100000000) {\n                val result = num.toLong() / 10000\n                return result.toString() + \"万\"\n            }else if(num.toLong() >= 100000000) {\n                val result = num.toLong() / 100000000\n                return result.toString() + \"亿\"\n            }\n            return num.toString()\n        } else {\n           return \"0\"\n        }\n    }\n\n    fun formatMilliseconds(milliseconds: Int): String {\n        val standardTime: String\n        val seconds = milliseconds / 1000\n        if (seconds <= 0) {\n            standardTime = \"00:00\"\n        } else if (seconds < 60) {\n            standardTime = String.format(Locale.getDefault(), \"00:%02d\", seconds % 60)\n        } else if (seconds < 3600) {\n            standardTime = java.lang.String.format(\n                Locale.getDefault(),\n                \"%02d:%02d\",\n                seconds / 60,\n                seconds % 60\n            )\n        } else {\n            standardTime = String.format(\n                Locale.getDefault(),\n                \"%02d:%02d:%02d\",\n                seconds / 3600,\n                seconds % 3600 / 60,\n                seconds % 60\n            )\n        }\n        return standardTime\n    }\n\n    fun isEmpty(str: String?) = (str == null || str == \"\")\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/TimeUtil.kt",
    "content": "package util\n\nimport java.text.SimpleDateFormat\n\n/**\n * Created by ssk on 2022/5/2.\n */\nenum class FormatterEnum(val value: SimpleDateFormat) {\n    YYYY_MM_DD(SimpleDateFormat(\"yyyy-MM-dd\")),\n    YYYYMMDD(SimpleDateFormat(\"yyyyMMdd\")),\n    YYYY_MM_DD__HH_MM(SimpleDateFormat(\"yyyy-MM-dd HH:mm\")),\n    YYYY_MM_DD__HH_MM_SS(SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\")),\n    YYYYMMDD__HH_MM(SimpleDateFormat(\"yyyyMMdd HH:mm\")),\n    YYYYMMDD__HH_MM_SS(SimpleDateFormat(\"yyyyMMdd HH:mm:ss\")),\n    HH_MM_SS(SimpleDateFormat(\"HH:mm:ss\"))\n}\n\nobject TimeUtil {\n    fun parse(value: Long, formatter: FormatterEnum = FormatterEnum.YYYY_MM_DD): String {\n        return formatter.value.format(value)\n    }\n}"
  },
  {
    "path": "src/jvmMain/kotlin/util/createDataStore.kt",
    "content": "package util\n\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.PreferenceDataStoreFactory\nimport androidx.datastore.preferences.core.Preferences\nimport base.AppConfig\nimport kotlinx.atomicfu.locks.SynchronizedObject\nimport kotlinx.atomicfu.locks.synchronized\nimport java.io.File\n\nprivate lateinit var dataStore: DataStore<Preferences>\n\nprivate val lock = SynchronizedObject()\n\n/**\n * Gets the singleton DataStore instance, creating it if necessary.\n */\nfun getDataStore(): DataStore<Preferences> =\n    synchronized(lock) {\n        if (::dataStore.isInitialized) {\n            dataStore\n        } else {\n            PreferenceDataStoreFactory.create {\n                File(\"${AppConfig.cacheRootDir}/$dataStoreFileName\")\n            }.also { dataStore = it }\n        }\n    }\n\ninternal const val dataStoreFileName = \"NCMusicDesktop.preferences_pb\""
  },
  {
    "path": "src/jvmMain/resources/image/ic_empty.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M163.86,557.65 L458.05,709.15 510.58,557.65l-294.19,-143.45L163.86,557.65zM517.15,557.65l49.91,150.15 304.69,-151.49 -59.1,-142.11L517.15,557.65zM115.47,293.07l101.13,120.66 294.19,-152.83 -95.87,-124.68L115.47,293.07zM908.53,295.75l-292.88,-159.54 -99.81,124.68 296.82,152.83L908.53,295.75zM198.99,595.53l-0.99,145.46 310.28,146.8L508.28,614.3l-44.33,117.97L198.99,595.53zM519.12,614.3l0,273.49 309.29,-146.8L828.41,596.53 560.21,732.56 519.12,614.3z\"/>\n</vector>\n"
  },
  {
    "path": "src/jvmMain/resources/image/ic_load_error.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"43dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"1376\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:pathData=\"M796.09,276.69a107.97,107.97 0,0 0,-67.71 -23.66,118.38 118.38,0 0,0 -68.83,24.86 205.37,205.37 0,0 0,-62.63 81.65l252.34,-11.01a188.07,188.07 0,0 0,-53.17 -71.84zM784.13,642.85a11.7,11.7 0,0 0,-12.3 11.18c-0.86,17.81 -8.17,29.6 -19.19,30.89s-24,-7.74 -29.42,-27.02a11.79,11.79 0,0 0,-23.14 2.93,25.21 25.21,0 0,1 -24.52,26.5c-11.36,0 -24.18,-6.97 -25.81,-28.13a11.79,11.79 0,1 0,-23.49 1.72c2.58,34.41 26.76,49.9 48.61,49.9h1.29A48.52,48.52 0,0 0,714.09 690.95a47.23,47.23 0,0 0,40.95 17.21c17.21,-1.98 38.37,-17.21 40.01,-53.26a11.79,11.79 0,0 0,-10.93 -12.04zM851.75,483a11.18,11.18 0,0 0,-15.83 0l-23.49,23.57 -23.49,-23.57A11.18,11.18 0,0 0,773.11 499.17l23.57,23.49 -23.57,23.57a11.18,11.18 0,0 0,15.83 15.83l23.57,-23.57 23.49,23.57a11.18,11.18 0,0 0,15.83 0,11.18 11.18,0 0,0 0,-15.83l-23.57,-23.57L851.75,499.17a11.18,11.18 0,0 0,0 -16.17zM646.73,568.78a11.18,11.18 0,0 0,7.92 -19.1l-23.57,-23.57 23.57,-23.57a11.18,11.18 0,0 0,-15.83 -15.83l-23.57,23.57 -23.57,-23.57a11.18,11.18 0,1 0,-15.83 15.83l23.57,23.57 -23.57,23.57a11.18,11.18 0,1 0,15.83 15.83L615.24,542.19l23.57,23.57a11.1,11.1 0,0 0,7.92 3.01z\"\n      android:strokeAlpha=\"0.1\"\n      android:fillColor=\"#16BB66\"\n      android:fillAlpha=\"0.1\"/>\n  <path\n      android:pathData=\"M1122.16,967.21A571.28,571.28 0,0 0,406.26 87.67l-47.84,-47.84a52.91,52.91 0,0 0,-79.32 69.69l149.19,149.36a70.81,70.81 0,0 1,0 99.8l-2.67,2.75a70.89,70.89 0,0 1,-99.8 0l-220.94,-221.11 -0.52,-0.52 -14.11,-14.11a52.83,52.83 0,0 0,-74.76 74.76l162.61,162.61a572.74,572.74 0,0 0,-39.15 208.21c0,153.66 60.65,293.12 159.51,401.1L134.47,972.37c-15.31,-5.33 -27.7,7.14 -27.7,22.46a30.03,30.03 0,0 0,27.7 29.17h987.08a30.11,30.11 0,0 0,27.7 -29.17,28.13 28.13,0 0,0 -27.1,-27.62zM1056.95,656.96a23.66,23.66 0,0 1,-27.45 19.01,182.4 182.4,0 0,1 -51.62,-19.01v133.35a39.66,39.66 0,0 1,-38.2 35.79l-54.2,-2.67v70.2c-3.96,5.59 -4.13,22.28 -14.97,34.41a151.25,151.25 0,0 1,-26.67 19.87l-50.76,-8.6h-0.52s-23.32,-7.92 -23.66,-20.65A23.66,23.66 0,0 1,791.53 894.94l43.53,-1.03v-72.7l-222.66,-9.64 -14.63,104.96 -1.03,2.5c-4.56,12.13 -36.13,28.99 -36.13,28.99h-34.41s-23.66,-15.06 -23.66,-28.05a21.85,21.85 0,0 1,23.66 -22.37h26.58l12.22,-88.19 -56.1,-2.5c-19.44,-0.77 -35.7,-15.92 -27.1,-35.45L481.8,663.16a337.78,337.78 0,0 1,-72.61 24.18,29.42 29.42,0 0,1 -3.96,0 23.75,23.75 0,0 1,-4.04 -47.06c0.6,0 72.18,-34.41 72.18,-34.41L473.37,399.12a36.31,36.31 0,0 1,35.7 -35.7l68.83,-2.93a229.46,229.46 0,0 1,72.18 -97.56,137.66 137.66,0 0,1 79.5,-28.39 125.44,125.44 0,0 1,79.15 27.62,209.75 209.75,0 0,1 62.03,86.04l71.5,-3.18a35.27,35.27 0,0 1,35.7 35.7v222.32l60.22,26.76a23.49,23.49 0,0 1,18.76 27.19zM1350.76,972.37h-136.88a26.93,26.93 0,0 0,-26.93 21.16,25.81 25.81,0 0,0 25.38,30.46h136.97a26.76,26.76 0,0 0,26.84 -21.08A25.81,25.81 0,0 0,1350.76 972.37z\"\n      android:strokeAlpha=\"0.1\"\n      android:fillColor=\"#16BB66\"\n      android:fillAlpha=\"0.1\"/>\n</vector>\n"
  },
  {
    "path": "src/jvmMain/resources/image/ic_network_error.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"211.6875dp\"\n    android:height=\"192dp\"\n    android:viewportWidth=\"1129\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:pathData=\"M251.67,266.72a9.53,9.53 0,0 1,-5.81 4.46l-97.06,26.01a9.54,9.54 0,0 1,-7.27 -0.96,9.5 9.5,0 0,1 -4.45,-5.81L132.12,271.93l115.55,-30.96 4.95,18.48a9.5,9.5 0,0 1,-0.96 7.26zM130.76,266.87l-1.12,-4.19 -3.67,-13.67 -13.73,-51.27 -5.13,-19.14 -8.98,-33.5 -0.69,-2.6 115.55,-30.96 1.24,4.62 7.44,27.8 9.28,34.63 5.13,19.14 7.69,28.71 1.28,4.79 0.13,0.48 1.24,4.62 -115.55,30.96 -0.12,-0.43zM98.02,107.68l97.06,-26.01a9.49,9.49 0,0 1,7.26 0.96c2.21,1.28 3.79,3.34 4.46,5.81l4.95,18.49 -115.55,30.96L91.26,119.4a9.57,9.57 0,0 1,6.77 -11.72zM257.25,258.22l-6.19,-23.11 -1.04,-3.87 -1.28,-4.79 -7.69,-28.71 -5.13,-19.14 -14.24,-53.12 -2.82,-10.55 -1.24,-4.62 -6.19,-23.11A14.36,14.36 0,0 0,193.85 77.06L96.78,103.06a14.36,14.36 0,0 0,-10.14 17.58l6.56,24.47 8.97,33.5 5.12,19.14 18.69,69.75 0.29,1.05 4.5,16.81 1.69,6.3a14.36,14.36 0,0 0,17.59 10.15l97.06,-26.01a14.36,14.36 0,0 0,10.15 -17.58z\"\n      android:fillColor=\"#979797\"/>\n  <path\n      android:pathData=\"M194.69,265.07a4.79,4.79 0,1 0,2.48 9.25,4.79 4.79,0 0,0 -2.48,-9.25\"\n      android:fillColor=\"#979797\"/>\n  <path\n      android:pathData=\"M272.75,33.5h9.57V0h-9.57zM237.33,0L229.69,5.76l20.16,26.75L257.49,26.75zM300.77,25.76l7.22,6.27 21.98,-25.27L322.74,0.48zM296.68,62.19l33.45,1.76 0.5,-9.56 -33.45,-1.75zM291.89,83.33l23.69,23.69 6.77,-6.77 -23.69,-23.69z\"\n      android:fillColor=\"#979797\"/>\n  <path\n      android:pathData=\"M1095.78,320.6v23.92h-23.93v9.57h23.93V378.02h9.57V354.09H1129.27v-9.57H1105.35V320.6zM660.66,267.96c-13.63,0 -26.5,3.08 -38.11,8.46 -1.47,0.68 -2.95,1.35 -4.38,2.11l2.88,3.96c1.44,-0.75 2.89,-1.45 4.38,-2.11A85.52,85.52 0,0 1,660.66 272.75c25.26,0 47.94,11 63.7,28.37 1.09,1.2 2.19,2.37 3.2,3.63l4.05,-2.63c-1,-1.25 -2.08,-2.45 -3.15,-3.66 -16.65,-18.67 -40.81,-30.5 -67.8,-30.5M660.66,306.24a42.85,42.85 0,0 0,-14.56 2.58c-1.55,0.56 -3.06,1.2 -4.52,1.91l2.86,3.95a38.09,38.09 0,0 1,4.58 -1.84,38.17 38.17,0 0,1 40.22,11.08c1.06,1.2 2.07,2.44 2.99,3.77l4.03,-2.62a42.68,42.68 0,0 0,-2.94 -3.79A42.96,42.96 0,0 0,660.66 306.24\"\n      android:fillColor=\"#979797\"/>\n  <path\n      android:pathData=\"M768.8,277.97a151.36,151.36 0,0 0,-15.13 -14.25,152.36 152.36,0 0,0 -7.08,-5.43A150.01,150.01 0,0 0,658.27 229.68C636.76,229.68 616.32,234.22 597.8,242.34c-1.49,0.66 -2.98,1.31 -4.45,2.01l2.89,3.99a145.12,145.12 0,0 1,62.02 -13.87,145.06 145.06,0 0,1 79.79,23.83 145.66,145.66 0,0 1,13.03 9.57c0.88,0.73 1.72,1.49 2.58,2.23 2.62,2.27 5.18,4.59 7.63,7.04 1.15,1.15 2.25,2.33 3.36,3.52l4.14,-2.69z\"\n      android:fillColor=\"#979797\"/>\n  <path\n      android:pathData=\"M1032.32,869.83c-38.62,66.21 -189.63,79.38 -351.23,56.68a1210.52,1210.52 0,0 1,-22.5 -3.38c-58.82,-9.42 -118.4,-23.43 -173.91,-41.25l-0.11,-0.03a951.5,951.5 0,0 1,-33.71 -10.73c-47.11,-15.9 -96.87,-36.45 -144.13,-59.33 -3.06,-1.48 -6.12,-2.97 -9.16,-4.47a1315.57,1315.57 0,0 1,-16.79 -8.46,1136.53 1136.53,0 0,1 -42.28,-22.79c-11.48,-6.52 -22.6,-13.11 -33.26,-19.74 -57.28,-35.64 -101.32,-72.35 -116.95,-103.42 -6.36,-12.68 -7.81,-24.01 -4.29,-33.7 10.16,-28.02 54.08,-42.21 121.24,-43.3 2.8,-0.05 5.57,-0.11 8.46,-0.11a738.67,738.67 0,0 1,42.84 1.53c1.84,0.12 3.72,0.27 5.58,0.4 -0.28,37.36 56.3,78.05 126.01,111.92 33.1,16.09 69.12,30.61 103.44,42.52h-2.4l3.79,0.49a945.52,945.52 0,0 0,51.77 16.31,906.08 906.08,0 0,0 82.67,21.55c9.9,2.08 19.67,3.92 29.31,5.58 102.12,17.54 188.6,11.46 228.14,-19.54 1.56,-3.25 2.58,-6.89 3.14,-10.82 1.73,0.82 3.49,1.64 5.21,2.46 4.93,2.37 9.77,4.75 14.5,7.14 0.48,0.24 0.92,0.48 1.4,0.73a688.1,688.1 0,0 1,33.26 18.03c62.47,36.17 94.88,70.28 89.96,95.75m-438.29,52.66c-26.2,-2 -54.21,-5.98 -83.05,-12.12 -45.59,-9.71 -87.08,-23.51 -121.17,-39.26 -25.01,-11.56 -46.03,-24.16 -61.67,-37.02a139.24,139.24 0,0 1,-13.11 -12.2c39.61,18.69 80.31,35.53 118.86,49.21a974.06,974.06 0,0 0,49.09 16.06c46.77,15.01 96.92,27.49 147.31,36.8 -11.59,-0.05 -23.71,-0.53 -36.26,-1.48m-325.85,-350.14c0.31,-1.63 0.7,-3.25 1.29,-4.86 0.07,-0.18 0.15,-0.36 0.22,-0.54 4.1,-10.83 12.77,-20.71 25.25,-29.33 21.27,-14.67 53.59,-25.7 93.19,-31.63a435.64,435.64 0,0 1,33.26 -3.64c7,-0.51 14.17,-0.86 21.48,-1.06 2.45,-0.07 4.91,-0.13 7.4,-0.17 -4.23,4.76 -7.43,9.89 -9.41,15.36 -2.51,6.91 -2.73,14 -1.02,21.14 7.01,29.26 46.91,59.28 98.62,80.95a416.83,416.83 0,0 0,46.12 16.16c13.97,4.2 28.46,7.26 42.93,9.34 67.71,9.7 135.02,-2.85 146.96,-28.75 2.63,-5.7 3,-12.97 1.32,-21.18a381.69,381.69 0,0 1,16.94 12.69,361.73 361.73,0 0,1 33.26,29.73c22.85,23.21 39.28,46.78 48.62,68.14 4.12,9.43 6.7,18.21 7.78,26.1 0.3,2.2 0.5,4.33 0.56,6.36 0.06,2.11 0.02,4.16 -0.16,6.09a33.28,33.28 0,0 1,-2.37 9.72c-42.18,31.87 -139.54,35.53 -251.71,12.07a893.97,893.97 0,0 1,-82.31 -21.42l-0.14,-0.04a958.32,958.32 0,0 1,-37.43 -11.44c-41.2,-13.49 -83.36,-30.32 -120.67,-48.69C332.16,655.92 287.11,624.93 272.59,596.56c-3.32,-6.49 -4.97,-12.59 -4.98,-18.43a30.39,30.39 0,0 1,0.57 -5.77m189.95,-71.3c1.87,-1.75 3.87,-3.45 6.06,-5.07a85.58,85.58 0,0 1,8.37 -5.41c20.67,-11.83 51.06,-19.11 86.41,-19.11 21.59,0 45.01,2.72 69.13,8.74a301.84,301.84 0,0 1,11.78 3.18c1.44,0.42 2.81,0.91 4.23,1.36 37.49,11.74 68.03,31.56 89.82,52.87 11.11,10.88 19.91,22.11 26.2,32.84 1.97,3.36 3.77,6.69 5.24,9.92l0.04,0.1c1.38,3.04 2.49,5.98 3.39,8.81 3.05,9.63 3.32,17.87 0.62,23.71 -12.62,27.4 -103.76,40.29 -183.26,16.41l-0.07,-0.02c-66.41,-19.03 -121.55,-52.5 -137.22,-83.3a44.91,44.91 0,0 1,-3.29 -8.47c-1.86,-6.72 -1.73,-13.17 0.48,-19.24 2.26,-6.24 6.41,-12.03 12.06,-17.33m197.55,-114.55a8.22,8.22 0,0 1,-1.07 -3.52,8.3 8.3,0 0,1 0.23,-2.81c0.39,-1.48 1.2,-2.74 2.22,-3.75a8.33,8.33 0,0 1,7.96 -2.14c1.45,0.38 2.67,1.16 3.66,2.14 2.05,2.05 3.01,5.07 2.22,8.07 -0.26,0.94 -0.72,1.77 -1.25,2.55 -0.69,1 -1.54,1.88 -2.62,2.51 -1.92,1.11 -4.16,1.4 -6.31,0.83h-0a8.24,8.24 0,0 1,-5.04 -3.89m253.44,363.3a868.33,868.33 0,0 0,-20.65 -10.05c0.04,-2.01 -0.03,-4.08 -0.22,-6.21 -2.4,-28.07 -23.82,-66.78 -62.27,-104.62a374.79,374.79 0,0 0,-33.25 -29.06,393.66 393.66,0 0,0 -19.22,-13.99 89.52,89.52 0,0 0,-3.81 -9.31c-5.89,-12.43 -15.34,-25.88 -27.96,-38.96 -22.62,-23.43 -55.43,-45.57 -96.16,-58.29 -1.37,-0.43 -2.73,-0.88 -4.12,-1.28 -0.4,-0.12 -0.79,-0.22 -1.2,-0.33l1.44,-5.37 0.38,-1.42 1.48,-5.56 4.46,-16.67 13.36,-50.02 0.66,-2.46c2.11,0.13 4.14,-0.27 6.02,-1.03a13.82,13.82 0,0 0,8.2 -9.24,13.8 13.8,0 0,0 -0.91,-9.5c-1.67,-3.56 -4.79,-6.42 -8.88,-7.52a13.83,13.83 0,0 0,-16.1 7.52,13.83 13.83,0 0,0 -0.87,2.31 13.85,13.85 0,0 0,2.5 12.11c1.25,1.61 2.81,2.98 4.69,3.92l-1.04,3.9 -13.37,50.02 -4.45,16.67 -1.47,5.51 -0.02,0.05 -1.42,5.31a306.02,306.02 0,0 0,-30.62 -6.55c-58.81,-9.42 -112.27,0.09 -141.79,20.75 -2.51,1.77 -4.84,3.61 -7,5.52 -2.78,0.02 -5.52,0.08 -8.26,0.14 -8.81,0.18 -17.44,0.56 -25.85,1.17a449.15,449.15 0,0 0,-33.26 3.59c-44.19,6.51 -80.1,19.22 -102.23,36.76 -10.47,8.31 -17.9,17.67 -21.64,27.97 -0.11,0.29 -0.13,0.59 -0.23,0.89a36.34,36.34 0,0 0,-1.45 5.44c-1.85,-0.14 -3.68,-0.27 -5.5,-0.39 -6.32,-0.43 -12.52,-0.77 -18.59,-1.03a590.49,590.49 0,0 0,-33.26 -0.5c-68.15,0.98 -114.93,15.55 -126.45,47.3 -14.47,39.85 43.3,94.54 126.45,145.69a967.7,967.7 0,0 0,33.26 19.53,1123.03 1123.03,0 0,0 30.17,16.34 1258.85,1258.85 0,0 0,34.51 17.36,103.04 103.04,0 0,0 8.64,10.28c14.72,15.46 37,30.7 64.91,44.61 36.18,18.03 81.86,33.77 133.09,44.69 42.97,9.16 83.74,13.61 119.59,13.81 0.84,0 1.72,0.04 2.56,0.04 9.15,0 17.98,-0.28 26.43,-0.84 170.51,27.4 337.23,16.94 379.17,-56.98 6.93,-30.83 -31.84,-68 -95.2,-104.22a724.29,724.29 0,0 0,-33.26 -17.81z\"\n      android:fillColor=\"#AAB3B8\"/>\n  <path\n      android:pathData=\"M679.26,703.46C566.14,694.16 468.46,701.13 468.96,701.07 467.09,716.03 569.2,751.94 679.26,765.18 789.32,778.41 875.67,764.64 875.67,744.23c0,-20.42 -86.35,-27.53 -196.41,-40.77\"\n      android:fillColor=\"#F3F6F8\"/>\n  <path\n      android:pathData=\"M29.59,789.53l-9.14,18.52 -20.44,2.97 14.79,14.42 -3.49,20.36 18.28,-9.61 18.28,9.61 -3.49,-20.36 14.79,-14.42 -20.44,-2.97z\"\n      android:fillColor=\"#D8D8D8\"/>\n  <path\n      android:pathData=\"M579,985.72c-113.64,0 -205.76,8.57 -205.76,19.14s92.12,19.14 205.76,19.14 205.76,-8.57 205.76,-19.14 -92.12,-19.14 -205.76,-19.14\"\n      android:fillColor=\"#F4F5F6\"/>\n</vector>\n"
  }
]