diff --git a/.github/workflows/github-action-pre_release.yml b/.github/workflows/github-action-pre_release.yml
new file mode 100644
index 0000000..3d2d86d
--- /dev/null
+++ b/.github/workflows/github-action-pre_release.yml
@@ -0,0 +1,121 @@
+name: TV-Multiplatform Build Pre Release
+on:
+ push:
+ branches: [pre-release]
+env:
+ tag: ${{ github.run_id }}
+jobs:
+ build-win:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'gradle'
+ - run: ls
+# - run: ./gradlew packageReleaseDistributionForCurrentOS
+ - run: ./gradlew createReleaseDistributable
+ - run: ls && tree ./desktopApp /f
+ - name: compress
+ shell: pwsh
+ run: Compress-Archive -Path "./composeApp/build/compose/binaries/main-release/app/TV" -DestinationPath ./TV-win-$Env:tag.zip
+# - name: move result to root
+# shell: pwsh
+# run: Move-Item -Path "./composeApp/build/compose/binaries/main-release/app/*.zip" -Destination "./" && Move-Item -Path "./composeApp/build/compose/binaries/main-release/msi/*.msi" -Destination "./"
+ - run: ls
+ - uses: actions/upload-artifact@v4
+ with:
+ name: win-file
+ path: |
+ *.msi
+ *.zip
+ build-linux:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'gradle'
+ - run: ls
+ - run: chmod +x ./gradlew && ./gradlew createReleaseDistributable
+ - run: ./gradlew packageReleaseDistributionForCurrentOS
+ - run: ls && tree
+ - name: compress
+ run: cd ./composeApp/build/compose/binaries/main-release/app && zip -q -r ./TV-linux-$tag.zip ./TV
+ - name: move file
+ run: mv ./composeApp/build/compose/binaries/main-release/deb/*.deb ./TV-linux-$tag.deb && mv ./composeApp/build/compose/binaries/main-release/app/*.zip ./
+ - run: ls
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-file
+ path: |
+ *.deb
+ *.zip
+ # https://github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Signing_and_notarization_on_macOS/README.md
+ build-mac:
+ runs-on: macos-13
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'gradle'
+ - run: ls
+ - run: chmod +x ./gradlew && ./gradlew createReleaseDistributable
+# - run: ./gradlew packageReleaseDistributionForCurrentOS
+ - run: ls
+ - name: compress
+ run: zip -q -r ./TV-mac-$tag.zip ./composeApp/build/compose/binaries/main-release/app
+# - name: move file
+# run: mv ./composeApp/build/compose/binaries/main-release/dmg/*.dmg ./TV-mac-$tag.dmg
+ - run: ls
+ - uses: actions/upload-artifact@v4
+ with:
+ name: mac-file
+ path: |
+ *.dmg
+ *.zip
+ release:
+ runs-on: ubuntu-latest
+ needs: [ build-win, build-linux, build-mac]
+ steps:
+ - name: Download all workflow run artifacts
+ uses: actions/download-artifact@v4
+ - run: ls && tree
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ **/*.zip
+ **/*.deb
+ **/*.pkg
+ **/*.dmg
+ **/*.msi
+ tag_name: ${{env.tag}}
+ prerelease: true
+ permissions:
+ contents: write
+ sendMsg:
+ name: sendMsg
+ runs-on: ubuntu-latest
+ needs:
+ - release
+ steps:
+ - uses: colutius/Telegram-Msg@main
+ with:
+ token: ${{ secrets.TG_TOKEN }}
+ chatid: ${{ secrets.TG_GROUP_ID }}
+ message: |
+ 👇新的预发布👇
+
+ 📦仓库: ${{ github.repository }}
+ button: true
+ button_name: 👀下载👀
+ button_url: https://github.com/${{ github.repository }}/releases/tag/${{env.tag}}
+ is_notify: true
+ is_preview: true
\ No newline at end of file
diff --git a/README.md b/README.md
index 212f402..6527318 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,14 @@

## 搜索

+## 搜索结果页
+
## 历史记录

# 讨论群
-[TG](https://t.me/tv_multiplatform)
\ No newline at end of file
+[TG](https://t.me/tv_multiplatform)
+
+# 引用
+player: https://github.com/numq/jetpack-compose-desktop-media-player
\ No newline at end of file
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index e739be8..ae3abcb 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -24,9 +24,9 @@ kotlin {
val desktopMain by getting
commonMain.dependencies {
- val ktorVer = "2.3.8"
+ val ktorVer = "2.3.12"
val logbackVer = "1.3.14"
- val imageLoader = "1.7.4"
+ val imageLoader = "1.8.1"
val hutoolVer = "5.8.27"
// val kotlinVersion = extra["kotlin.version"] as String
implementation(compose.runtime)
@@ -66,8 +66,8 @@ kotlin {
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVer")
implementation("io.ktor:ktor-server-swagger:$ktorVer")
implementation("io.ktor:ktor-client-core:$ktorVer")
- implementation("io.ktor:ktor-client-cio:$ktorVer")
- implementation("io.ktor:ktor-server-call-logging:$ktorVer")
+ implementation("io.ktor:ktor-client-okhttp:$ktorVer")
+// implementation("io.ktor:ktor-server-call-logging:$ktorVer")
// log
implementation("ch.qos.logback:logback-classic:$logbackVer")
@@ -76,7 +76,7 @@ kotlin {
// optional - Moko Resources Decoder
// api("io.github.qdsfdhvh:image-loader-extension-moko-resources:$imageLoader")
- api(project.dependencies.platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.12"))
+ api(project.dependencies.platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.14"))
api("com.squareup.okhttp3:okhttp")
api("com.squareup.okhttp3:okhttp-dnsoverhttps")
@@ -132,23 +132,22 @@ compose.desktop {
"jdk.httpserver",
"jdk.unsupported"
)
- val dir = project.layout.projectDirectory.dir("/src/desktopMain/resources/res")
+ val dir = project.layout.projectDirectory.dir("src/desktopMain/resources/res")
println(dir)
- appResourcesRootDir.set(project.layout.projectDirectory.dir("/src/desktopMain/resources/res"))
+ appResourcesRootDir.set(project.layout.projectDirectory.dir("src/desktopMain/resources/res"))
// app icons https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/Native_distributions_and_local_execution#app-icon
windows {
- iconFile.set(project.file("src/commonMain/composeResources/icon/icon-s.ico"))
+ iconFile.set(project.file("src/commonMain/resources/pic/icon-s.ico"))
dirChooser = true
upgradeUuid = "161FA5A0-A30B-4568-9E84-B3CD637CC8FE"
}
linux {
- iconFile.set(project.file("src/commonMain/composeResources/icon/TV-icon-s.png"))
-
+ iconFile.set(project.file("src/commonMain/resources/pic/TV-icon-s.png"))
}
macOS {
- iconFile.set(project.file("src/commonMain/composeResources/icon/icon.icns"))
+ iconFile.set(project.file("src/commonMain/resources/pic/icon.icns"))
}
}
diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
index c0bcfb2..d7bf795 100644
--- a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
+++ b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
@@ -33,4 +33,4 @@
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
-
\ No newline at end of file
+
diff --git a/composeApp/src/commonMain/composeResources/icon/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/icon/compose-multiplatform.xml
deleted file mode 100644
index d7bf795..0000000
--- a/composeApp/src/commonMain/composeResources/icon/compose-multiplatform.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/composeApp/src/commonMain/kotlin/com/corner/bean/Hot.kt b/composeApp/src/commonMain/kotlin/com/corner/bean/Hot.kt
index c05cf3a..d57bfe2 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/bean/Hot.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/bean/Hot.kt
@@ -10,28 +10,33 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Headers.Companion.toHeaders
+import okhttp3.Response
+import org.slf4j.LoggerFactory
+
+private val log = LoggerFactory.getLogger("Hot")
@Serializable
-data class Hot(val data:List) {
- companion object{
+data class Hot(val data: List) {
+ companion object {
@OptIn(ExperimentalSerializationApi::class)
public fun getHotList() {
SiteViewModel.viewModelScope.launch {
- val response = Http.Get("https://api.web.360kan.com/v1/rank?cat=1", headers = mapOf(HttpHeaders.Referrer to "https://www.360kan.com/rank/general").toHeaders()).execute()
-// val response = KtorClient.client.get("https://api.web.360kan.com/v1/rank?cat=1") {
-// headers {
-// set(HttpHeaders.Referrer, "https://www.360kan.com/rank/general")
-// }
-// }
+ var response: Response? = null
+ try {
+ response = Http.Get(
+ "https://api.web.360kan.com/v1/rank?cat=1",
+ headers = mapOf(HttpHeaders.Referrer to "https://www.360kan.com/rank/general").toHeaders()
+ ).execute()
+ } catch (e: Exception) {
+ log.error("请求热搜失败", e)
+ }
- if(response.isSuccessful)
+ if (response?.isSuccessful == true)
GlobalModel.hotList.value = Jsons.decodeFromStream(response.body.byteStream()).data
-// GlobalModel.hotList.update { it. = Jsons.decodeFromStream(response.bodyAsChannel().toInputStream()).data}
-// GlobalModel.hotList.value.addAll()
}
}
}
}
@Serializable
-data class HotData(val title:String, val comment:String, val upinfo:String, val description:String)
\ No newline at end of file
+data class HotData(val title: String, val comment: String, val upinfo: String, val description: String)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/bean/Setting.kt b/composeApp/src/commonMain/kotlin/com/corner/bean/Setting.kt
index 47613c4..785d264 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/bean/Setting.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/bean/Setting.kt
@@ -23,20 +23,44 @@ class SearchHistoryCache:Cache{
private val maxSize:Int = 30
- private var searchHistoryList:MutableSet = mutableSetOf()
+ private var searchHistoryList:LinkedHashSet = linkedSetOf()
override fun getName(): String {
return "searchHistory"
}
override fun add(t:String) {
if(searchHistoryList.size >= maxSize){
- searchHistoryList = searchHistoryList.drop(1).toMutableSet()
+ val list:LinkedHashSet = linkedSetOf()
+ list.addAll(searchHistoryList.drop(1))
+ searchHistoryList = list
}
+ searchHistoryList.remove(t)
searchHistoryList.add(t)
}
- fun getSearchList():Set{
- return searchHistoryList
+ fun getSearchList():List{
+ return searchHistoryList.reversed()
+ }
+
+}
+
+@Serializable
+class PlayerStateCache:Cache{
+ private val map:MutableMap = mutableMapOf();
+ override fun getName(): String {
+ return "playerState"
+ }
+
+ override fun add(t: String) {
+
+ }
+
+ fun add(key:String, value: String){
+ map.put(key,value)
+ }
+
+ fun get(key: String):String?{
+ return map.get(key)
}
}
@@ -59,8 +83,8 @@ enum class SettingType(val id: String) {
object SettingStore {
private val defaultList = listOf(
Setting("vod", "点播", ""),
- Setting("player", "外部播放器", ""),
- Setting("log", "日志级别", Level.DEBUG.levelStr)
+ Setting("log", "日志级别", Level.DEBUG.levelStr),
+ Setting("player", "播放器", "false#")
)
private var settingFile = SettingFile(mutableListOf(), mutableMapOf())
@@ -79,6 +103,12 @@ object SettingStore {
return settingFile.list
}
+ fun reset(){
+ settingFile = SettingFile(mutableListOf(), mutableMapOf())
+ initSetting()
+ write()
+ }
+
fun write() {
Files.write(Paths.setting(), Jsons.encodeToString(settingFile).toByteArray())
}
@@ -88,8 +118,9 @@ object SettingStore {
write()
}
- fun setCache(name:String, value: String){
- settingFile.cache[name]?.add(value)
+ fun doWithCache(func:(MutableMap) -> Unit){
+ func(settingFile.cache)
+ write()
}
fun getCache(name:String): Cache? {
@@ -120,7 +151,7 @@ object SettingStore {
}
val cache = getCache(SettingType.SEARCHHISTORY.id)
if (cache != null) {
- return (cache as SearchHistoryCache).getSearchList()
+ return (cache as SearchHistoryCache).getSearchList().toSet()
}
return setOf()
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/bean/Suggest.kt b/composeApp/src/commonMain/kotlin/com/corner/bean/Suggest.kt
index 52c4f53..5267b53 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/bean/Suggest.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/bean/Suggest.kt
@@ -3,11 +3,16 @@ package com.corner.bean
import com.corner.catvodcore.util.Jsons
import kotlinx.serialization.Serializable
+@Serializable
+data class Data (
+ val name: String = ""
+)
+
@Serializable
-class Suggest {
+data class Suggest(
var data: List? = null
-
+){
fun isEmpty(): Boolean {
return data.isNullOrEmpty()
}
@@ -16,27 +21,6 @@ class Suggest {
data = null
}
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Suggest
-
- return data == other.data
- }
-
- override fun hashCode(): Int {
- var result = 0
- data?.forEach { result = 31 * result + it.hashCode() }
- return result
- }
-
-
- @Serializable
- data class Data (
- val name: String = ""
- )
-
companion object {
fun objectFrom(str: String): Suggest {
return Jsons.decodeFromString(str)
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Episode.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Episode.kt
index cd51c80..d99a979 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Episode.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Episode.kt
@@ -1,26 +1,46 @@
package com.corner.catvodcore.bean
+import com.corner.catvodcore.util.Utils
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
+import java.util.*
@Serializable
data class Episode(
@SerializedName("name")
- var name: String? = null,
+ var name: String = "",
@SerializedName("desc")
val desc: String? = null,
@SerializedName("url")
- var url: String? = null,
- val number: Int = 0,
+ var url: String = "",
+ var number: Int = 0,
var activated: Boolean = false,
val selected: Boolean = false
) {
companion object {
fun create(name: String, string: String): Episode {
val episode = Episode()
+ episode.number = Utils.getDigit(name)
episode.name = name
episode.url = string
return episode
}
+
+ }
+
+ fun rule1(name: String?): Boolean {
+ return this.name.equals(name, ignoreCase = true)
+ }
+
+ fun rule2(number: Int): Boolean {
+ return this.number == number && number != -1
+ }
+
+ fun rule3(name: String): Boolean {
+ return this.name.lowercase(Locale.getDefault()).contains(name.lowercase(Locale.getDefault()))
+ }
+
+ fun rule4(name: String): Boolean {
+ return this.name.lowercase(Locale.getDefault()).contains(name.lowercase(Locale.getDefault()))
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Flag.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Flag.kt
index b0c90cf..a714a8a 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Flag.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Flag.kt
@@ -1,5 +1,6 @@
package com.corner.catvodcore.bean
+import com.corner.catvodcore.util.Utils
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.*
@@ -30,10 +31,22 @@ data class Flag (
val episode = if(split.size > 1) Episode.create(if(split[0].isEmpty()) number else split[0].trim(), split[1]) else Episode.create(number, urls[i])
if(!episodes.contains(episode)) episodes.add(episode)
}
- episodes.sortWith(Comparator { o1, o2 ->
- o1.name?.compareTo(o2.name ?: "")!!
- })
+ episodes.sortBy { it.number }
+// episodes.sortWith(Comparator { o1, o2 ->
+// o1.name.compareTo(o2.name)
+// })
+ }
+ fun find(remarks: String, strict: Boolean): Episode? {
+ val number: Int = Utils.getDigit(remarks)
+ if (episodes.size == 0) return null
+ if (episodes.size == 1) return episodes.get(0)
+ for (item in episodes) if (item.rule1(remarks)) return item
+ for (item in episodes) if (item.rule2(number)) return item
+ if (number == -1) for (item in episodes) if (item.rule3(remarks)) return item
+ if (number == -1) for (item in episodes) if (item.rule4(remarks)) return item
+ if (position != -1) return episodes[position]
+ return if (strict) null else episodes[0]
}
fun isEmpty():Boolean{
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Result.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Result.kt
index 503011d..e5cb14e 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Result.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Result.kt
@@ -9,13 +9,9 @@ import kotlinx.serialization.Transient
@Serializable
class Result {
- // @Path("class")
-// @ElementList(entry = "ty", required = false, inline = true)
@SerialName("class")
var types: MutableList = mutableListOf()
- // @Path("list")
-// @ElementList(entry = "video", required = false, inline = true)
var list: MutableList = mutableListOf()
val filters: MutableMap> = mutableMapOf()
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Type.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Type.kt
index f8d2aa3..64ee0be 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Type.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Type.kt
@@ -7,7 +7,7 @@ import kotlinx.serialization.Transient
@Serializable
class Type {
- constructor(id:String, name:String){
+ constructor(id: String, name: String) {
this.typeId = id
this.typeName = name
}
@@ -19,13 +19,13 @@ class Type {
var typeName: String = ""
@Transient
- var selected:Boolean = false
+ var selected: Boolean = false
@Transient
- var failTime:Int =0
+ var failTime: Int = 0
- companion object{
- fun home():Type{
+ companion object {
+ fun home(): Type {
val type = Type("home", "推荐")
type.selected = true
return type
@@ -41,6 +41,7 @@ class Type {
if (typeId != other.typeId) return false
if (typeName != other.typeName) return false
if (failTime != other.failTime) return false
+ if (selected != other.selected) return false
return true
}
@@ -49,6 +50,7 @@ class Type {
var result = typeId.hashCode()
result = 31 * result + typeName.hashCode()
result = 31 * result + failTime.hashCode()
+ result = 31 * result + selected.hashCode()
return result
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Url.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Url.kt
index 10c142a..9645180 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Url.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Url.kt
@@ -5,21 +5,20 @@ import kotlinx.serialization.json.*
import kotlinx.serialization.serializer
@Serializable
-class Url {
- val values: MutableList = mutableListOf()
- val position = 0
-}
+data class Url (
+ val values: MutableList = mutableListOf(),
+ val position:Int = 0
+)
fun Url.v():String{
- return if (position >= (values.size)) "" else values.get(position).v ?: ""
+ return if (position >= (values.size)) "" else values[position].v ?: ""
}
fun Url.replace(url:String){
- values.get(position).v = url
+ values[position].v = url
}
fun Url.add(url:String):Url{
-// if(StringUtils.isBlank(url)) return
values.add(Value("",url))
return this
}
@@ -52,29 +51,4 @@ object UrlSerializable: JsonTransformingSerializer(serializer()) {
override fun transformSerialize(element: JsonElement): JsonElement {
return super.transformSerialize(element)
}
-}
-
-//object UrlSerialize: KSerializer{
-// override val descriptor: SerialDescriptor
-// get() = buildClassSerialDescriptor("Url"){
-// element("values")
-// element("position")
-// }
-//
-// override fun deserialize(decoder: Decoder): Url {
-// decoder.decodeStructure(descriptor){
-// val values = mutableListOf()
-// val position = 0
-// while(true){
-// when(val index = decodeElementIndex(descriptor)){
-// 0 -> decoder.decode
-// }
-// }
-// }
-// }
-//
-// override fun serialize(encoder: Encoder, value: Url) {
-// TODO("Not yet implemented")
-// }
-//
-//}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Vod.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Vod.kt
index ab71c46..0d17c36 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Vod.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/bean/Vod.kt
@@ -2,6 +2,8 @@ package com.corner.catvod.enum.bean
import com.corner.catvodcore.bean.Episode
import com.corner.catvodcore.bean.Flag
+import com.corner.database.History
+import com.corner.util.Constants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@@ -36,10 +38,12 @@ data class Vod(
var subEpisode: MutableList? = mutableListOf(),
@Transient
var currentTabIndex: Int = 0,
- @Transient
- var version: Int = 0
) {
companion object {
+
+ fun Vod.getEpisode():Episode?{
+ return subEpisode?.find { it.activated }
+ }
fun Vod.isEmpty():Boolean{
return org.apache.commons.lang3.StringUtils.isBlank(vodId) || vodFlags.isEmpty()
}
@@ -74,8 +78,8 @@ data class Vod(
fun List.getPage(index: Int): MutableList {
val list = this.subList(
- index * 15,
- if (index * 15 + 15 > size) size else index * 15 + 15
+ index * Constants.EpSize,
+ if (index * Constants.EpSize + Constants.EpSize > size) size else index * Constants.EpSize + Constants.EpSize
).toMutableList()
return list
}
@@ -84,6 +88,32 @@ data class Vod(
fun isFolder():Boolean{
return VodTag.Folder.called == vodTag
}
+
+ fun findAndSetEpByName(history: History): Episode? {
+ if (history.vodRemarks.isNullOrBlank()) return null
+ currentFlag = vodFlags.find { it?.flag == history.vodFlag }
+ val episode = currentFlag?.find(history.vodRemarks, true)
+ if(episode != null){
+ episode.activated = true
+ val indexOf = currentFlag?.episodes?.indexOf(episode)
+ // 32 15 16
+ currentTabIndex = (indexOf?.plus(1))!! / Constants.EpSize
+ subEpisode = currentFlag?.episodes?.getPage(currentTabIndex)!!
+ }
+ return episode
+ }
+
+ fun nextFlag():Flag?{
+ val find = vodFlags.find { it?.activated ?: false }
+ val indexOf = vodFlags.indexOf(find)
+ if(indexOf + 1 >= vodFlags.size) return null
+ val flag = vodFlags[indexOf + 1]
+ vodFlags.forEach{
+ it?.activated = flag?.flag == it?.flag
+ }
+// flag.episodes.indexOf()
+ return flag
+ }
}
enum class VodTag(val called: String) {
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/config/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/config/ApiConfig.kt
index 14ccb96..afbc39c 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/config/ApiConfig.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/config/ApiConfig.kt
@@ -32,6 +32,8 @@ object ApiConfig{
log.info("parseConfig start cfg:{} isJson:{}", cfg, isJson)
val data = getData(if (isJson) cfg.json ?: "" else cfg.url!!, isJson) ?: throw RuntimeException("配置读取异常")
if(StringUtils.isBlank(data)) {
+ log.warn("配置数据为空")
+ SnackBar.postMsg("配置数据为空 请检查")
setHome(null)
return api
}
@@ -155,8 +157,4 @@ fun Api.initSite() {
GlobalModel.home.value = sites.first()
Db.Config.setHome(url, ConfigType.SITE.ordinal, GlobalModel.home.value.toString())
}
-}
-
-fun proxyLocal() {
-
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Http.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Http.kt
index 0778f11..71c934d 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Http.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Http.kt
@@ -10,10 +10,14 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.http.HttpClient
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.concurrent.TimeUnit
+import javax.net.ssl.*
class Http {
companion object {
@@ -21,11 +25,65 @@ class Http {
private var client: OkHttpClient? = null
private var selector: ProxySelect? = null
private val defaultHeaders: Headers = Headers.Builder().build()
+
+ //获取这个SSLSocketFactory
+ fun getSSLSocketFactory(): SSLSocketFactory {
+ try {
+ val sslContext = SSLContext.getInstance("SSL")
+ sslContext.init(null, getTrustManager(), SecureRandom())
+ return sslContext.socketFactory
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+
+ //获取TrustManager
+ private fun getTrustManager(): Array {
+ return arrayOf(object : X509TrustManager {
+ override fun checkClientTrusted(chain: Array, authType: String) {
+ }
+
+ override fun checkServerTrusted(chain: Array, authType: String) {
+ }
+
+ override fun getAcceptedIssuers(): Array {
+ return arrayOf()
+ }
+ }
+ )
+ }
+
+ //获取HostnameVerifier
+ fun getHostnameVerifier(): HostnameVerifier {
+ return HostnameVerifier { s: String?, sslSession: SSLSession? -> true }
+ }
+
+ fun getX509TrustManager(): X509TrustManager? {
+ var trustManager: X509TrustManager? = null
+ try {
+ val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
+ trustManagerFactory.init(null as KeyStore?)
+ val trustManagers = trustManagerFactory.trustManagers
+ check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) { "Unexpected default trust managers:" + trustManagers.contentToString() }
+ trustManager = trustManagers[0] as X509TrustManager
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return trustManager
+ }
private val builder: OkHttpClient.Builder
- get() = OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS)
- .readTimeout(10, TimeUnit.SECONDS).writeTimeout(10, TimeUnit.SECONDS)
- .followRedirects(true)
- .dns(dns())
+ get(){
+ val dispatcher = Dispatcher()
+ return OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS).writeTimeout(10, TimeUnit.SECONDS)
+ .followRedirects(true)
+ .sslSocketFactory(getSSLSocketFactory(), getX509TrustManager()!!)
+ .hostnameVerifier((getHostnameVerifier()))
+// .callTimeout(Duration.of(3, ChronoUnit.SECONDS))
+ .dispatcher(dispatcher)
+ .dns(dns())
+ }
fun dns(): Dns {
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Jsons.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Jsons.kt
index 1d6dd40..77f2d9b 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Jsons.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Jsons.kt
@@ -1,12 +1,16 @@
package com.corner.catvodcore.util
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.*
+@OptIn(ExperimentalSerializationApi::class)
val Jsons = Json {
ignoreUnknownKeys = true
isLenient = true
+ prettyPrint = true
+ prettyPrintIndent = " "
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorClient.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorClient.kt
index 83cac90..f547da2 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorClient.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorClient.kt
@@ -1,10 +1,10 @@
package com.corner.catvodcore.util
import io.ktor.client.*
-import io.ktor.client.engine.cio.*
+import io.ktor.client.engine.okhttp.*
class KtorClient {
companion object{
- val client = HttpClient(CIO)
+ val client = HttpClient(OkHttp)
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorHeaderUrlFetcher.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorHeaderUrlFetcher.kt
index 2223f69..18c580d 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorHeaderUrlFetcher.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/KtorHeaderUrlFetcher.kt
@@ -5,55 +5,54 @@ import androidx.compose.ui.res.loadImageBitmap
import com.seiko.imageloader.component.fetcher.FetchResult
import com.seiko.imageloader.component.fetcher.Fetcher
import com.seiko.imageloader.option.Options
+import io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
import io.ktor.http.*
+import io.ktor.util.*
+import io.ktor.utils.io.jvm.javaio.*
import kotlinx.serialization.json.jsonObject
-import okhttp3.Headers.Companion.toHeaders
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
class KtorHeaderUrlFetcher private constructor(
private val httpUrl: String,
- httpClient: () -> OkHttpClient,
+ httpClient: () -> HttpClient,
) : Fetcher {
private val httpClient by lazy(httpClient)
override suspend fun fetch(): FetchResult {
- val url = httpUrl.toString()
- val headers = mutableMapOf()
- headers.run {
- if (url.contains("@Headers=")) {
- val elements = Jsons.parseToJsonElement(url.split("@Headers=")[1].split("@")[0])
- for (jsonElement in elements.jsonObject) {
- put(jsonElement.key, jsonElement.value.toString())
+ var url = httpUrl.toString()
+ val response = httpClient.request {
+ headers {
+ if (url.contains("@Headers=")) {
+ appendAll(StringValues.build {
+ val elements = Jsons.parseToJsonElement(url.split("@Headers=").apply { url = this[0] }[1].split("@")[0])
+ for (jsonElement in elements.jsonObject) {
+ append(jsonElement.key, jsonElement.value.toString())
+ }
+ })
+ }
+ if (url.contains("@Cookie=")) append(HttpHeaders.Cookie, url.split("@Cookie=").apply { url = this[0] }[1].split("@")[0])
+ if (url.contains("@Referer=")) append(HttpHeaders.Referrer, url.split("@Referer=").apply { url = this[0] }[1].split("@")[0])
+ if (url.contains("@User-Agent=")) append(HttpHeaders.UserAgent, url.split("@User-Agent=").apply { url = this[0] }[1].split("@")[0])
+
}
+ url(url)
}
- if (url.contains("@Cookie=")) put(HttpHeaders.Cookie, url.split("@Cookie=")[1].split("@")[0])
- if (url.contains("@Referer=")) put(HttpHeaders.Referrer, url.split("@Referer=")[1].split("@")[0])
- if (url.contains("@User-Agent=")) put(HttpHeaders.UserAgent, url.split("@User-Agent=")[1].split("@")[0])
- }
-
-
- val request = Request.Builder().url(url.split("@")[0]).headers(headers.toHeaders()).build()
- var response:Response? = null
- try {
- response = httpClient.newCall(request).execute()
- if (response.isSuccessful) {
+ if (response.status.isSuccess()) {
val ofSource = FetchResult.OfPainter(
- painter = BitmapPainter(loadImageBitmap(response.body.byteStream()))
+ painter = BitmapPainter(loadImageBitmap(response.bodyAsChannel().toInputStream()))
)
return ofSource
}
- }finally {
- response?.close()
- }
- throw RuntimeException("code:${response?.code}, ${HttpStatusCode.fromValue(response?.code ?: 500)}")
+ throw RuntimeException("code:${response.status.value}, ${response.status.description}")
}
+
class Factory(
- private val httpClient: () -> OkHttpClient,
+ private val httpClient: () -> HttpClient,
) : Fetcher.Factory {
override fun create(data: Any, options: Options): Fetcher? {
if (data is String) return KtorHeaderUrlFetcher(data, httpClient)
@@ -62,11 +61,9 @@ class KtorHeaderUrlFetcher private constructor(
}
companion object {
- val defaultHttpEngineFactory: () -> OkHttpClient
- get() = { Http.client() }
val CustomUrlFetcher = Factory {
- Http.client()
+ HttpClient(OkHttp)
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Paths.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Paths.kt
index 9b7d93c..444dfaf 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Paths.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Paths.kt
@@ -8,7 +8,7 @@ import java.io.File
import java.nio.file.Path
object Paths {
- private val runPath = System.getProperty("user.dir")
+// private val runPath = System.getProperty("user.dir")
private val classPath = System.getProperty("java.class.path")
private val ApplicationName = "TV-Multiplatform"
private val log = LoggerFactory.getLogger("Paths")
@@ -32,7 +32,7 @@ object Paths {
}
fun root():File{
- return File(runPath).resolve("data")
+ return userDataDir.resolve("data")
}
fun userDataRoot():File{
@@ -88,8 +88,8 @@ object Paths {
return file.toPath()
}
- fun log(): File {
- return root().check().resolve("log.txt")
+ fun playerLog(): File {
+ return root().check().resolve("playerLog.txt")
}
fun logPath():File{
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Utils.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Utils.kt
index 8338d6e..611c56f 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Utils.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/util/Utils.kt
@@ -1,8 +1,13 @@
package com.corner.catvodcore.util
+import com.corner.catvodcore.config.ApiConfig
+import com.corner.database.Db
import org.apache.commons.lang3.StringUtils
import java.math.BigInteger
import java.security.MessageDigest
+import java.time.Instant
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeFormatterBuilder
import java.util.*
object Utils {
@@ -29,6 +34,34 @@ object Utils {
return Base64.getEncoder().encodeToString(bytes)
}
+ fun getDigit(text: String): Int {
+ try {
+ if (text.startsWith("上") || text.startsWith("下")) return -1
+ return text.replace("(?i)(mp4|H264|H265|720p|1080p|2160p|4K)".toRegex(), "").replace("\\D+".toRegex(), "")
+ .toInt()
+ } catch (e: java.lang.Exception) {
+ return -1
+ }
+ }
+
+ fun getHistoryKey(key:String, id:String): String {
+ return key + Db.SYMBOL + id + Db.SYMBOL + ApiConfig.api.cfg.value?.id!!
+ }
+
+ fun formatMilliseconds(milliseconds: Long): String {
+ val seconds = (milliseconds / 1000) % 60
+ val minutes = (milliseconds / (1000 * 60)) % 60
+ val hours = (milliseconds / (1000 * 60 * 60)) % 24
+ val days = milliseconds / (1000 * 60 * 60 * 24)
+
+ return if (days > 0) {
+ "%d天 %02d:%02d:%02d".format(days, hours, minutes, seconds)
+ }else if(hours > 0){
+ "%02d:%02d:%02d".format(hours, minutes, seconds)
+ }else{
+ "%02d:%02d".format(minutes, seconds)
+ }
+ }
// fun parse
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/GlobalModel.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/GlobalModel.kt
index d1ac3a7..e2ce028 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/GlobalModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/GlobalModel.kt
@@ -1,20 +1,39 @@
package com.corner.catvodcore.viewmodel
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.window.WindowPlacement
+import androidx.compose.ui.window.WindowState
import com.arkivanov.decompose.value.MutableValue
import com.corner.bean.HotData
import com.corner.catvod.enum.bean.Site
import com.corner.catvod.enum.bean.Vod
+import com.corner.ui.scene.SnackBar
object GlobalModel {
+ var windowState:WindowState? = null
val hotList = MutableValue(listOf())
val chooseVod = mutableStateOf(Vod())
var detailFromSearch = false
val home = MutableValue(Site.get("",""))
val clear = MutableValue(false)
var keyword = MutableValue("")
-
+ var videoFullScreen = MutableValue(false)
+ private set
fun clearHome(){
home.value = Site.get("","")
}
+
+ fun toggleVideoFullScreen():Boolean{
+ toggleWindowFullScreen()
+ videoFullScreen.value = !videoFullScreen.value
+ return videoFullScreen.value
+ }
+
+ private fun toggleWindowFullScreen(){
+ if(windowState?.placement == WindowPlacement.Fullscreen){
+ windowState?.placement = WindowPlacement.Floating
+ }else{
+ windowState?.placement = WindowPlacement.Fullscreen
+ }
+ }
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/SiteViewModel.kt b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/SiteViewModel.kt
index ecff01d..c9e829e 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/SiteViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/catvodcore/viewmodel/SiteViewModel.kt
@@ -18,6 +18,7 @@ import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelChildren
import kotlinx.serialization.encodeToString
import okhttp3.Headers.Companion.toHeaders
import org.apache.commons.lang3.StringUtils
@@ -34,7 +35,11 @@ object SiteViewModel {
val quickSearch: MutableState> = mutableStateOf(CopyOnWriteArrayList(listOf(Collect.all())))
private val supervisorJob = SupervisorJob()
- val viewModelScope = CoroutineScope(Dispatchers.Default + supervisorJob)
+ val viewModelScope = CoroutineScope(Dispatchers.IO + supervisorJob)
+
+ fun cancelAll(){
+ supervisorJob.cancelChildren()
+ }
fun getSearchResultActive(): Collect {
return search.value.first { it.isActivated().value }
@@ -54,7 +59,7 @@ object SiteViewModel {
if ((rst.list.size) > 0) result.value = rst
val homeVideoContent = spider.homeVideoContent()
SpiderDebug.log("homeContent: $homeVideoContent")
- rst.list.addAll(Jsons.decodeFromString(homeContent).list)
+ rst.list.addAll(Jsons.decodeFromString(homeVideoContent).list)
result.value = rst.also { this.result.value = it }
}
@@ -83,7 +88,7 @@ object SiteViewModel {
fun detailContent(key: String, id: String): Result? {
val site: Site = ApiConfig.api.sites.find { it.key == key } ?: return null
- var rst:Result = Result()
+ var rst = Result()
try {
if (site.type == 3) {
val spider: Spider = ApiConfig.getSpider(site)
@@ -91,7 +96,7 @@ object SiteViewModel {
SpiderDebug.log("detail:$detailContent")
ApiConfig.setRecent(site)
rst = Jsons.decodeFromString(detailContent)
- if (!rst.list.isEmpty()) rst.list.get(0).setVodFlags()
+ if (rst.list.isNotEmpty()) rst.list.get(0).setVodFlags()
// if (!rst.list.isEmpty()) checkThunder(rst.list.get(0).vodFlags())
detail.value = rst
} else if (site.key.isEmpty() && site.name.isEmpty() && key == "push_agent") {
@@ -121,6 +126,7 @@ object SiteViewModel {
return null
}
rst.list.forEach { it.site = site }
+
return rst
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/database/repository/ConfigRepository.kt b/composeApp/src/commonMain/kotlin/com/corner/database/repository/ConfigRepository.kt
index ba422b1..6ae49b8 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/database/repository/ConfigRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/database/repository/ConfigRepository.kt
@@ -24,6 +24,8 @@ interface ConfigRepository {
fun updateSome( id:Int, json: String? = null, home: String? = null, parse: String? = null)
fun updateUrl(id: Long, textFieldValue: String)
fun setHome(url: String?, type: Int, key: String)
+ fun getAll(): List
+ abstract fun deleteById(id: Long?)
}
class ConfigRepositoryImpl : ConfigRepository, KoinComponent {
@@ -72,4 +74,13 @@ class ConfigRepositoryImpl : ConfigRepository, KoinComponent {
configQueries.setHome(key, type.toLong(), url)
}
+ override fun getAll(): List {
+ return configQueries.getAll().executeAsList()
+ }
+
+ override fun deleteById(id: Long?) {
+ id ?: return
+ configQueries.deleteById(id)
+ }
+
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/database/repository/HistoryRepository.kt b/composeApp/src/commonMain/kotlin/com/corner/database/repository/HistoryRepository.kt
index 0e040eb..4da22fc 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/database/repository/HistoryRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/database/repository/HistoryRepository.kt
@@ -25,16 +25,16 @@ interface HistoryRepository {
fun deleteBatch(ids:List): Boolean
fun deleteAll():Boolean
+ fun findHistory(historyKey: String): History?
+ fun updateOpeningEnding(opening: Long, ending: Long, key: String)
+
+ fun updateSome(flag:String, vodRemarks:String, playUrl:String, position:Long, speed:Float,opening: Long, ending: Long, historyKey:String)
}
class HistoryRepositoryImpl:HistoryRepository, KoinComponent{
private val database: Database by inject()
private val historyQueries = database.historyQueries
- private fun getHistoryKey(key:String, id:String, cId:String): String {
- return key + Db.SYMBOL + id + Db.SYMBOL + ApiConfig.api.id
- }
-
/**
* key = KEY@@@ID@@@CID
*/
@@ -57,7 +57,7 @@ class HistoryRepositoryImpl:HistoryRepository, KoinComponent{
}
override fun create(vod:Vod, flag:String, vodRemarks: String){
- val historyKey = vod.site?.key + Db.SYMBOL + vod.vodId + Db.SYMBOL + ApiConfig.api.id
+ val historyKey = vod.site?.key + Db.SYMBOL + vod.vodId + Db.SYMBOL + ApiConfig.api.cfg.value?.id!!
val his = historyQueries.findByKey(historyKey).executeAsOneOrNull()
if(his == null){
save(historyKey,
@@ -67,9 +67,9 @@ class HistoryRepositoryImpl:HistoryRepository, KoinComponent{
vodRemarks,
vod.vodPlayUrl!!,
ApiConfig.api.cfg.value?.id!!)
- }else{
- historyQueries.updateSome(flag, vodRemarks, vod.vodPlayUrl, historyKey)
- }
+ }/*else{
+ historyQueries.updateSome(flag, vodRemarks, vod.vodPlayUrl, historyKey)
+ }*/
}
override fun findAll(cId: Long?): List {
@@ -95,6 +95,18 @@ class HistoryRepositoryImpl:HistoryRepository, KoinComponent{
return true
}
+ override fun findHistory(historyKey: String): History? {
+ return historyQueries.findByKey(historyKey).executeAsOneOrNull()
+ }
+
+ override fun updateOpeningEnding(opening: Long, ending: Long, key: String) {
+ historyQueries.updateOpeningEnding(opening = opening, ending = ending, key)
+ }
+
+ override fun updateSome(flag: String, vodRemarks: String, playUrl: String, position: Long,speed:Float, opening: Long, ending: Long, historyKey: String) {
+ historyQueries.updateSome(flag, vodRemarks, playUrl, position,speed.toDouble(),opening, ending, historyKey)
+ }
+
}
@@ -109,6 +121,7 @@ fun History.buildVod():Vod{
vod.vodId = keySplit[1]
vod.vodPic = vodPic
vod.vodRemarks = vodRemarks
+ vod.site = ApiConfig.getSite(keySplit[0])
return vod
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/database/repository/KeepRepository.kt b/composeApp/src/commonMain/kotlin/com/corner/database/repository/KeepRepository.kt
new file mode 100644
index 0000000..8090696
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/corner/database/repository/KeepRepository.kt
@@ -0,0 +1,17 @@
+package com.corner.database.repository
+
+import com.corner.catvodcore.config.ApiConfig
+import com.corner.database.Database
+import com.corner.database.Keep
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class KeepRepository:KoinComponent{
+ private val database: Database by inject()
+ private val keepQueries = database.keepQueries
+
+ fun getAll():List{
+ ApiConfig.api.cfg.value?.id ?: return listOf()
+ return keepQueries.getAll(ApiConfig.api.cfg.value?.id!!.toLong()).executeAsList()
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/corner/init/Init.kt b/composeApp/src/commonMain/kotlin/com/corner/init/Init.kt
index 1e6238e..53c9d15 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/init/Init.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/init/Init.kt
@@ -2,6 +2,8 @@ package com.corner.init
import com.arkivanov.decompose.value.update
import com.corner.bean.Hot
+import com.corner.bean.SettingStore
+import com.corner.bean.SettingType
import com.corner.catvodcore.config.ApiConfig
import com.corner.catvodcore.config.init
import com.corner.catvodcore.enum.ConfigType
@@ -10,8 +12,10 @@ import com.corner.catvodcore.viewmodel.GlobalModel
import com.corner.database.Db
import com.corner.database.appModule
import com.corner.server.KtorD
+import com.corner.ui.player.vlcj.VlcJInit
import com.corner.ui.scene.hideProgress
import com.corner.ui.scene.showProgress
+import org.apache.commons.lang3.StringUtils
import org.koin.core.context.startKoin
import org.slf4j.LoggerFactory
@@ -28,6 +32,7 @@ class Init {
initConfig()
initPlatformSpecify()
Hot.getHotList()
+ VlcJInit.init()
} finally {
hideProgress()
}
@@ -55,6 +60,9 @@ fun initConfig() {
ApiConfig.clear()
GlobalModel.clear.update {!it}
+ val vod = SettingStore.getSettingItem(SettingType.VOD.id)
+ if(StringUtils.isBlank(vod)) return
+ // todo 清空点播设置后 仍然可以在数据库中查询到旧的配置
val siteConfig = Db.Config.findOneByType(ConfigType.SITE.ordinal.toLong()) ?: return
try {
ApiConfig.parseConfig(siteConfig, false).init()
diff --git a/composeApp/src/commonMain/kotlin/com/corner/server/KtorD.kt b/composeApp/src/commonMain/kotlin/com/corner/server/KtorD.kt
index 946df7f..1558ddf 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/server/KtorD.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/server/KtorD.kt
@@ -4,9 +4,7 @@ import com.corner.server.plugins.configureRouting
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
-import io.ktor.server.plugins.callloging.*
import org.slf4j.LoggerFactory
-import org.slf4j.event.Level
private val log = LoggerFactory.getLogger("KtorD")
object KtorD {
@@ -50,9 +48,9 @@ object KtorD {
}
private fun Application.module() {
- install(CallLogging){
- level = Level.DEBUG
- }
+// install(CallLogging){
+// level = Level.DEBUG
+// }
// install(ContentNegotiation){
// json(Json {
// isLenient = true
diff --git a/composeApp/src/commonMain/kotlin/com/corner/server/plugins/Routings.kt b/composeApp/src/commonMain/kotlin/com/corner/server/plugins/Routings.kt
index c82fceb..29ec2ce 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/server/plugins/Routings.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/server/plugins/Routings.kt
@@ -50,11 +50,10 @@ fun Application.configureRouting() {
if (objects.isEmpty()) errorResp(call)
else if (objects[0] is Response) {
response = objects[0] as Response
- response.headers.forEach { i -> call.response.headers.append(i.first, i.second) }
+ response.headers.forEach { i -> if(!HttpHeaders.isUnsafe(i.first)) call.response.headers.append(i.first, i.second) }
log.debug("proxy resp code:{} headers:{}", response.code, response.headers)
call.respondOutputStream(
status = HttpStatusCode.fromValue(response.code),
-// status = HttpStatusCode.PartialContent,
) {
response.body.byteStream().transferTo(this)
}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/Detail.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/Detail.kt
index 9d417c0..f5d2ac1 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/ui/Detail.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/ui/Detail.kt
@@ -1,10 +1,10 @@
package com.corner.ui
+import AppTheme
import SiteViewModel
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.*
-import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -21,31 +22,35 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import com.arkivanov.decompose.value.update
+import com.corner.bean.SettingStore
+import com.corner.catvod.enum.bean.Vod
import com.corner.catvod.enum.bean.Vod.Companion.getPage
-import com.corner.database.Db
+import com.corner.catvodcore.bean.v
+import com.corner.catvodcore.viewmodel.GlobalModel
import com.corner.ui.decompose.DetailComponent
+import com.corner.ui.player.vlcj.VlcjFrameController
import com.corner.ui.scene.*
import com.corner.ui.video.QuickSearchItem
+import com.corner.util.Constants
import com.corner.util.play.Play
-import com.seiko.imageloader.ui.AutoSizeImage
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.apache.commons.lang3.StringUtils
-@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
fun DetailScene(component: DetailComponent, onClickBack: () -> Unit) {
val model = component.model.subscribeAsState()
@@ -53,14 +58,17 @@ fun DetailScene(component: DetailComponent, onClickBack: () -> Unit) {
val detail by rememberUpdatedState(model.value.detail)
+ val controller = rememberUpdatedState(component.controller)
+
+ val isFullScreen = GlobalModel.videoFullScreen.subscribeAsState()
+
+ val videoHeight = derivedStateOf { if (isFullScreen.value) 1f else 0.6f }
+ val videoWidth = derivedStateOf { if (isFullScreen.value) 1f else 0.7f }
+
+
LaunchedEffect("detail") {
component.load()
}
- DisposableEffect(model.value.detail) {
- println("detail修改")
- onDispose {
- }
- }
DisposableEffect(model.value.isLoading) {
if (model.value.isLoading) {
@@ -71,296 +79,478 @@ fun DetailScene(component: DetailComponent, onClickBack: () -> Unit) {
onDispose { }
}
+ val focus = remember { FocusRequester() }
+
+ LaunchedEffect(isFullScreen.value) {
+ focus.requestFocus()
+ }
Box(
modifier = Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
- Column(Modifier.padding(8.dp)) {
- BackRow(Modifier, onClickBack = {
- onClickBack()
- }) {
- Row(
- Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Row(horizontalArrangement = Arrangement.Start) {
- Text(
- detail?.vodName ?: "",
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onBackground,
- modifier = Modifier.padding(start = 50.dp)
- )
- }
-
- Row(horizontalArrangement = Arrangement.End) {
- IconButton(
- onClick = {
- scope.launch {
- component.clear()
- component.quickSearch()
- SnackBar.postMsg("重新加载")
- }
- },
- enabled = !model.value.isLoading
- ) {
- Icon(
- Icons.Default.Autorenew,
- contentDescription = "renew",
- tint = MaterialTheme.colorScheme.onSecondaryContainer
+ Column(Modifier) {
+ if (!isFullScreen.value) {
+ BackRow(Modifier, onClickBack = {
+ onClickBack()
+ }) {
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(horizontalArrangement = Arrangement.Start) {
+ Text(
+ detail?.vodName ?: "",
+ style = MaterialTheme.typography.headlineMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ modifier = Modifier.padding(start = 50.dp)
)
}
- }
- }
- }
- val searchResultList = derivedStateOf { model.value.quickSearchResult.toList() }
- Row(modifier = Modifier.fillMaxWidth()) {
- Column(Modifier.weight(0.3f)) {
- AutoSizeImage(
- modifier = Modifier.clip(RoundedCornerShape(8.dp))
- .fillMaxHeight(0.4f),
-// .height(180.dp).width(160.dp),
- url = detail?.vodPic ?: "",
- contentDescription = detail?.vodName,
- contentScale = ContentScale.FillHeight,
- placeholderPainter = { painterResource("/icon/empty.png") },
- errorPainter = { painterResource("/icon/empty.png") }
- )
- if (model.value.quickSearchResult.isNotEmpty()) {
- Spacer(Modifier.size(20.dp))
- val quickState = rememberLazyGridState()
- val adapter = rememberScrollbarAdapter(quickState)
- Box {
- LazyVerticalGrid(
- modifier = Modifier.padding(end = 10.dp),
- columns = GridCells.Fixed(2),
- state = quickState,
- verticalArrangement = Arrangement.spacedBy(5.dp),
- horizontalArrangement = Arrangement.spacedBy(5.dp)
- ) {
- items(searchResultList.value) {
- QuickSearchItem(it) {
- SiteViewModel.viewModelScope.launch {
- component.loadDetail(it)
- }
+
+ Row(horizontalArrangement = Arrangement.End) {
+ IconButton(
+ onClick = {
+ scope.launch {
+ component.clear()
+ component.quickSearch()
+ SnackBar.postMsg("重新加载")
}
- }
- }
- VerticalScrollbar(
- modifier = Modifier.align(Alignment.CenterEnd),
- adapter = adapter, style = defaultScrollbarStyle().copy(
- unhoverColor = Color.Gray.copy(0.45F),
- hoverColor = Color.DarkGray
+ },
+ enabled = !model.value.isLoading
+ ) {
+ Icon(
+ Icons.Default.Autorenew,
+ contentDescription = "renew",
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
)
- )
+ }
}
}
}
- val rememberScrollState = rememberScrollState(0)
- if (model.value.detail == null) {
- emptyShow()
+ }
+ val mrl = derivedStateOf { model.value.currentPlayUrl }
+ Row(
+ modifier = Modifier.fillMaxHeight(videoHeight.value),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ val internalPlayer = derivedStateOf {
+ SettingStore.getPlayerSetting()[0] as Boolean
+ }
+ if (internalPlayer.value) {
+ SideEffect {
+ focus.requestFocus()
+ }
+ Player(
+ mrl.value, controller.value, Modifier.fillMaxWidth(videoWidth.value).focusable(),
+ component,
+ focusRequester = focus
+ )
} else {
- Column(
- modifier = Modifier.padding(start = 10.dp)
- .scrollable(state = rememberScrollState, orientation = Orientation.Vertical)
- .weight(0.8f)
+ Box(
+ Modifier
+ .fillMaxWidth(videoWidth.value)
+ .fillMaxHeight()
+ .background(Color.Black)
) {
- Row {
- if (detail?.site?.name?.isNotBlank() == true) {
- Text("站源: " + detail?.site?.name, color = MaterialTheme.colorScheme.onSurface)
- Spacer(Modifier.width(5.dp))
- }
- val s = mutableListOf()
- Text(detail?.vodYear ?: "", color = MaterialTheme.colorScheme.onSurface)
- if (StringUtils.isNotBlank(detail?.vodArea)) {
- s.add(detail?.vodArea!!)
- }
- if (StringUtils.isNotBlank(detail?.cate)) {
- s.add(detail?.cate!!)
- }
- if (StringUtils.isNotBlank(detail?.typeName)) {
- s.add(detail?.typeName!!)
- }
- Text(s.joinToString(separator = " | "), color = MaterialTheme.colorScheme.onSurface)
- }
- Text("导演:${detail?.vodDirector ?: "无"}", color = MaterialTheme.colorScheme.onSurface)
- ExpandedText(
- "演员:${detail?.vodActor ?: "无"}",
- 2,
- textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface)
- )
- ExpandedText(
- "简介:${detail?.vodContent?.trim() ?: "无"}",
- 3,
- textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface)
- )
- // 线路
- Spacer(modifier = Modifier.size(20.dp))
Text(
- "线路",
- fontSize = TextUnit(25F, TextUnitType.Sp),
- modifier = Modifier.padding(bottom = 5.dp),
- color = MaterialTheme.colorScheme.onSurface
+ "使用外部播放器",
+ modifier = Modifier.align(Alignment.Center).focusRequester(focus),
+ fontWeight = FontWeight.Bold,
+ fontSize = TextUnit(23f, TextUnitType.Sp),
+ color = MaterialTheme.colorScheme.onBackground
)
- if (detail?.vodFlags?.isNotEmpty() == true) {
- val state = rememberLazyListState(0)
- Box() {
- LazyRow(
- horizontalArrangement = Arrangement.spacedBy(5.dp),
- state = state,
- modifier = Modifier.padding(bottom = 10.dp)
- .fillMaxWidth()
- .onPointerEvent(PointerEventType.Scroll) {
- scope.launch {
-// if(it.changes.size == 0) return@launch
- state.scrollBy(it.changes.first().scrollDelta.y * state.layoutInfo.visibleItemsInfo.first().size)
- }
- },
- ) {
- items(detail?.vodFlags?.toList() ?: listOf()) {
- RatioBtn(it?.show ?: "", onClick = {
-
- for (vodFlag in detail?.vodFlags ?: listOf()) {
- if (it?.show == vodFlag?.show) {
- it?.activated = true
- } else {
- vodFlag?.activated = false
- }
- }
- val dt = detail?.copy(
- currentFlag = it,
- subEpisode = it?.episodes?.getPage(detail!!.currentTabIndex)
- ?.toMutableList()
- )
- component.model.update { it.copy(detail = dt) }
- }, selected = it?.activated ?: false)
- }
- }
- if (state.layoutInfo.visibleItemsInfo.size < (detail?.vodFlags?.size ?: 0)) {
- HorizontalScrollbar(
- rememberScrollbarAdapter(state),
- style = defaultScrollbarStyle().copy(
- unhoverColor = Color.Gray.copy(0.45F),
- hoverColor = Color.DarkGray
- ), modifier = Modifier.align(Alignment.BottomCenter)
- )
- }
+ }
+ }
+ AnimatedVisibility(!isFullScreen.value, modifier = Modifier.fillMaxSize()) {
+ EpChooser(
+ component, Modifier.fillMaxSize()
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f),
+ shape = RoundedCornerShape(4.dp)
+ )
+ .padding(horizontal = 5.dp)
+ )
+ }
+ }
+ AnimatedVisibility(!isFullScreen.value) {
+ val searchResultList = derivedStateOf { model.value.quickSearchResult.toList() }
+ Box(Modifier) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Spacer(Modifier.size(15.dp))
+ Column(Modifier.fillMaxWidth(0.3f)) {
+ quickSearchResult(model, searchResultList, component)
+ }
+ Column(
+ modifier = Modifier.padding(start = 10.dp)
+ .fillMaxSize()
+ ) {
+ if (model.value.detail == null) {
+ emptyShow(onRefresh = { component.load() })
+ } else {
+ vodInfo(detail)
}
- //
- Spacer(modifier = Modifier.size(20.dp))
- Row {
- if (detail?.currentFlag != null && (detail?.currentFlag?.episodes?.size ?: 0) > 0) {
+ Spacer(modifier = Modifier.size(15.dp))
+ // 线路
+ flags(scope, controller.value, component)
+ Spacer(Modifier.size(15.dp))
+ val urls = rememberUpdatedState(component.model.value.currentUrl)
+ val showUrl = derivedStateOf { (urls.value?.values?.size ?: 0) > 1 }
+ if (showUrl.value) {
+ Row {
Text(
- "选集",
+ "清晰度",
fontSize = TextUnit(25F, TextUnitType.Sp),
- modifier = Modifier.padding(bottom = 5.dp),
- color = MaterialTheme.colorScheme.onSurface
- )
- Spacer(Modifier.size(10.dp))
- Text(
- "共${detail?.currentFlag?.episodes?.size}集",
- textAlign = TextAlign.End,
+ modifier = Modifier.padding(bottom = 5.dp, end = 5.dp),
color = MaterialTheme.colorScheme.onSurface
)
+ LazyRow(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
+ itemsIndexed(urls.value?.values ?: listOf()) { i, item ->
+ RatioBtn(item.n ?: (i + 1).toString(), onClick = {
+ component.model.update {
+ it.copy(
+ currentUrl = urls.value?.copy(position = i),
+ currentPlayUrl = item.v ?: ""
+ )
+ }
+ }, i == urls.value?.position!!)
+ }
+ }
+
}
}
+ }
+ }
+ }
+ }
+ }
+ val showEpChooserDialog = derivedStateOf { isFullScreen.value && model.value.showEpChooserDialog }
+ Dialog(Modifier.align(Alignment.CenterEnd)
+ .fillMaxWidth(0.3f)
+ .fillMaxHeight(0.8f)
+ .padding(end = 20.dp),
+ showDialog = showEpChooserDialog.value,
+ onClose = { component.model.update { it.copy(showEpChooserDialog = false) } }) {
+ EpChooser(
+ component, Modifier.fillMaxSize()
+ .background(
+ MaterialTheme.colorScheme.surfaceContainerLow,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .padding(horizontal = 5.dp)
+ )
+ }
- val epSize = detail?.currentFlag?.episodes?.size ?: 0
+ }
- val scrollState = rememberLazyListState(0)
- val scrollBarAdapter = rememberScrollbarAdapter(scrollState)
- if (epSize > 15) {
- Box(modifier = Modifier.padding(bottom = 2.dp)) {
- LazyRow(
- state = scrollState,
- modifier = Modifier.padding(bottom = 2.dp),
- horizontalArrangement = Arrangement.spacedBy(5.dp)
- ) {
- for (i in 0 until epSize step 15) {
- item {
- RatioBtn(
- selected = detail?.currentTabIndex == (i / 15),
- onClick = {
- detail?.currentTabIndex = i / 15
- val dt = detail?.copy(
- subEpisode = detail?.currentFlag?.episodes?.getPage(detail!!.currentTabIndex)
- ?.toMutableList()
- )
- component.model.update { it.copy(detail = dt) }
- },
- text = "${i + 1}-${i + 15}"
- )
- }
- }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun flags(
+ scope: CoroutineScope,
+ controller: VlcjFrameController,
+ component: DetailComponent
+) {
+ val model = component.model.subscribeAsState()
+ val detail = derivedStateOf { model.value.detail }
+ Row(Modifier.padding(start = 10.dp)) {
+ Text(
+ "线路",
+ fontSize = TextUnit(25F, TextUnitType.Sp),
+ modifier = Modifier.padding(bottom = 5.dp),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(Modifier.size(10.dp))
+ val detailIsNotEmpty = derivedStateOf { detail.value?.vodFlags?.isNotEmpty() }
+ if (detailIsNotEmpty.value == true) {
+ val state = rememberLazyListState(0)
+ Box() {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ state = state,
+ modifier = Modifier.padding(bottom = 10.dp)
+ .fillMaxWidth()
+ .onPointerEvent(PointerEventType.Scroll) {
+ scope.launch {
+// if(it.changes.size == 0) return@launch
+ state.scrollBy(it.changes.first().scrollDelta.y * state.layoutInfo.visibleItemsInfo.first().size)
+ }
+ },
+ ) {
+ val flagList = derivedStateOf { detail.value?.vodFlags?.toList() ?: listOf() }
+ items(flagList.value) {
+ RatioBtn(it?.show ?: "", onClick = {
+// scope.launch {
+ for (vodFlag in detail.value!!.vodFlags) {
+ if (it?.show == vodFlag?.show) {
+ it?.activated = true
+ } else {
+ vodFlag?.activated = false
}
- HorizontalScrollbar(
- adapter = scrollBarAdapter,
- modifier = Modifier.padding(bottom = 5.dp).align(Alignment.BottomCenter),
- style = defaultScrollbarStyle().copy(
- unhoverColor = Color.Gray.copy(0.45F),
- hoverColor = Color.DarkGray
- )
+ }
+ val dt = detail.value!!.copy(
+ currentFlag = it,
+ subEpisode = it?.episodes?.getPage(detail.value!!.currentTabIndex)
+ ?.toMutableList()
+ )
+ val history = controller.history.value
+ if (history != null) {
+ val findEp =
+ detail.value!!.findAndSetEpByName(controller.history.value!!)
+ if (findEp != null) component.playEp(dt, findEp)
+ }
+ component.model.update { model ->
+ model.copy(
+ detail = dt,
+ shouldPlay = true,
)
}
- }
- val videoLoading = remember { mutableStateOf(false) }
- LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- state = rememberLazyGridState(),
- horizontalArrangement = Arrangement.spacedBy(5.dp),
- verticalArrangement = Arrangement.spacedBy(5.dp)
- ) {
- items(
- detail?.subEpisode ?: listOf()
- ) {
- TooltipArea(
- tooltip = {
- // composable tooltip content
- Surface(
- modifier = Modifier.shadow(4.dp),
-// color = MaterialTheme.colors.surface,
- shape = RoundedCornerShape(4.dp)
- ) {
- Text(
- text = it.name ?: "",
- modifier = Modifier.padding(10.dp),
-// color = MaterialTheme.colors.onSurface
- )
- }
- },
- delayMillis = 600
- ) {
- RatioBtn(text = it.name ?: "", onClick = {
- videoLoading.value = true
- SiteViewModel.viewModelScope.launch {
- for (i in detail?.currentFlag?.episodes ?: listOf()) {
- i.activated = (i.name == it.name)
- }
- val dt = detail?.copy(
- subEpisode = detail?.currentFlag?.episodes?.getPage(detail!!.currentTabIndex)
- ?.toMutableList()?.toList()?.toMutableList(),
- version = (detail!!.version++)
- )
- component.model.update { it.copy(detail = dt) }
- val result = SiteViewModel.playerContent(
- detail?.site?.key ?: "",
- detail?.currentFlag?.flag ?: "",
- it.url ?: ""
- )
- Play.start(result, it.name ?: detail?.vodName)
- Db.History.create(detail!!, detail?.currentFlag?.flag!!, it.name!!)
- }.invokeOnCompletion {
- videoLoading.value = false
- }
- }, selected = it.activated, it.activated && videoLoading.value)
+// }
+ }, selected = it?.activated ?: false)
+ }
+ }
+ val showScrollBar =
+ derivedStateOf { state.layoutInfo.visibleItemsInfo.size < (detail.value!!.vodFlags.size) }
+ if (showScrollBar.value) {
+ HorizontalScrollbar(
+ rememberScrollbarAdapter(state),
+ style = defaultScrollbarStyle().copy(
+ unhoverColor = Color.Gray.copy(0.45F),
+ hoverColor = Color.DarkGray
+ ), modifier = Modifier.align(Alignment.BottomCenter)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun quickSearchResult(
+ model: State,
+ searchResultList: State>,
+ component: DetailComponent
+) {
+ if (model.value.quickSearchResult.isNotEmpty()) {
+ val quickState = rememberLazyGridState()
+ val adapter = rememberScrollbarAdapter(quickState)
+ Box {
+ LazyVerticalGrid(
+ modifier = Modifier.padding(end = 10.dp),
+ columns = GridCells.Fixed(2),
+ state = quickState,
+ verticalArrangement = Arrangement.spacedBy(5.dp),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ items(searchResultList.value) {
+ QuickSearchItem(it) {
+ SiteViewModel.viewModelScope.launch {
+ component.loadDetail(it)
+ }
+ }
+ }
+ }
+ VerticalScrollbar(
+ modifier = Modifier.align(Alignment.CenterEnd),
+ adapter = adapter, style = defaultScrollbarStyle().copy(
+ unhoverColor = Color.Gray.copy(0.45F),
+ hoverColor = Color.DarkGray
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun vodInfo(detail: Vod?) {
+ Column(Modifier.padding(10.dp)) {
+ Row() {
+ if (detail?.site?.name?.isNotBlank() == true) {
+ Text(
+ "站源: " + detail.site?.name,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(Modifier.width(5.dp))
+ }
+ val s = mutableListOf()
+ Text(detail?.vodYear ?: "", color = MaterialTheme.colorScheme.onSurface)
+ if (StringUtils.isNotBlank(detail?.vodArea)) {
+ s.add(detail?.vodArea!!)
+ }
+ if (StringUtils.isNotBlank(detail?.cate)) {
+ s.add(detail?.cate!!)
+ }
+ if (StringUtils.isNotBlank(detail?.typeName)) {
+ s.add(detail?.typeName!!)
+ }
+ Text(
+ s.joinToString(separator = " | "),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ Text(
+ "导演:${detail?.vodDirector ?: "无"}",
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ ExpandedText(
+ "演员:${detail?.vodActor ?: "无"}",
+ 2,
+ textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface)
+ )
+ ExpandedText(
+ "简介:${detail?.vodContent?.trim() ?: "无"}",
+ 3,
+ textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface)
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun EpChooser(component: DetailComponent, modifier: Modifier) {
+ val model = component.model.subscribeAsState()
+ val detail = rememberUpdatedState(model.value.detail)
+ Column(modifier = modifier) {
+ Row(Modifier.padding(vertical = 3.dp, horizontal = 8.dp)) {
+ Text(
+ "选集",
+ fontSize = TextUnit(20F, TextUnitType.Sp),
+ modifier = Modifier.padding(bottom = 5.dp),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(Modifier.size(10.dp))
+ val show = derivedStateOf {
+ detail.value?.currentFlag != null && (detail.value?.currentFlag?.episodes?.size ?: 0) > 0
+ }
+ if (show.value) {
+ Text(
+ "共${detail.value?.currentFlag?.episodes?.size}集",
+ textAlign = TextAlign.End,
+ fontSize = TextUnit(15F, TextUnitType.Sp),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+ val epSize = derivedStateOf { detail.value?.currentFlag?.episodes?.size ?: 0 }
+
+ val scrollState = rememberLazyListState(0)
+ val scrollBarAdapter = rememberScrollbarAdapter(scrollState)
+ if (epSize.value > 15) {
+ Box(modifier = Modifier.padding(bottom = 2.dp)) {
+ LazyRow(
+ state = scrollState,
+ modifier = Modifier.padding(bottom = 2.dp),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ for (i in 0 until epSize.value step Constants.EpSize) {
+ item {
+ RatioBtn(
+ selected = detail.value?.currentTabIndex == (i / Constants.EpSize),
+ onClick = {
+ detail.value?.currentTabIndex = i / Constants.EpSize
+ val dt = detail.value?.copy(
+ subEpisode = detail.value?.currentFlag?.episodes?.getPage(
+ detail.value!!.currentTabIndex
+ )
+ ?.toMutableList()
+ )
+ component.model.update { it.copy(detail = dt) }
+ },
+ text = "${i + 1}-${i + Constants.EpSize}"
+ )
+ }
+ }
+ }
+ HorizontalScrollbar(
+ adapter = scrollBarAdapter,
+ modifier = Modifier.padding(bottom = 5.dp)
+ .align(Alignment.BottomCenter),
+ style = defaultScrollbarStyle().copy(
+ unhoverColor = Color.Gray.copy(0.45F),
+ hoverColor = Color.DarkGray
+ )
+ )
+ }
+ }
+ val videoLoading = remember { mutableStateOf(false) }
+ val epList = derivedStateOf { detail.value?.subEpisode ?: listOf() }
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ state = rememberLazyGridState(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ verticalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ items(
+ epList.value, key = { it.url + it.number }
+ ) {
+ TooltipArea(
+ tooltip = {
+ // composable tooltip content
+ Surface(
+ modifier = Modifier.shadow(4.dp),
+ shape = RoundedCornerShape(4.dp)
+ ) {
+ Text(
+ text = it.name,
+ modifier = Modifier.padding(10.dp),
+ )
+ }
+ },
+ delayMillis = 600
+ ) {
+ RatioBtn(text = it.name, onClick = {
+ videoLoading.value = true
+ SiteViewModel.viewModelScope.launch {
+ for (i in detail.value?.currentFlag?.episodes ?: listOf()) {
+ i.activated = (i.name == it.name)
+ if (i.activated) {
+ component.model.update { model ->
+ if (model.currentEp?.name != it.name) {
+ component.controller.doWithHistory { it.copy(position = 0L) }
+ }
+ component.controller.doWithHistory {
+ it.copy(
+ episodeUrl = i.url,
+ vodRemarks = i.name
+ )
+ }
+ model.copy(currentEp = i)
}
}
}
+ val dt = detail.value?.copy(
+ subEpisode = detail.value?.currentFlag?.episodes?.getPage(
+ detail.value!!.currentTabIndex
+ )
+ ?.toMutableList()?.toList()?.toMutableList(),
+ )
+ component.model.update { it.copy(detail = dt) }
+ val result = SiteViewModel.playerContent(
+ detail.value?.site?.key ?: "",
+ detail.value?.currentFlag?.flag ?: "",
+ it.url
+ )
+ component.model.update { it.copy(currentUrl = result?.url) }
+ val internalPlayer = SettingStore.getPlayerSetting()[0] as Boolean
+ if (internalPlayer) {
+ component.play(result)
+ } else {
+ Play.start(result?.url?.v() ?: "", model.value.currentEp?.name)
+ }
+ }.invokeOnCompletion {
+ videoLoading.value = false
}
- }
+ }, selected = it.activated, it.activated && videoLoading.value)
}
}
}
}
+}
-}
\ No newline at end of file
+@androidx.compose.desktop.ui.tooling.preview.Preview
+@Composable
+fun previewEmptyShow() {
+ AppTheme {
+ emptyShow(onRefresh = { println("ddd") })
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/History.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/History.kt
index cd0dc60..4aff79f 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/ui/History.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/ui/History.kt
@@ -60,8 +60,8 @@ fun HistoryItem(
modifier = Modifier.height(220.dp),
contentDescription = history.vodName,
contentScale = ContentScale.Crop,
- placeholderPainter = { painterResource("/icon/empty.png") },
- errorPainter = { painterResource("/icon/empty.png") })
+ placeholderPainter = { painterResource("/pic/empty.png") },
+ errorPainter = { painterResource("/pic/empty.png") })
Text(
text = history.vodName!!,
modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)).align(Alignment.BottomCenter)
diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt
new file mode 100644
index 0000000..a4b2f87
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt
@@ -0,0 +1,234 @@
+package com.corner.ui
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerIcon
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
+import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
+import com.corner.bean.PlayerStateCache
+import com.corner.bean.SettingStore
+import com.corner.catvodcore.viewmodel.GlobalModel
+import com.corner.ui.decompose.DetailComponent
+import com.corner.ui.player.DefaultControls
+import com.corner.ui.player.PlayerState
+import com.corner.ui.player.frame.FrameContainer
+import com.corner.ui.player.vlcj.VlcjFrameController
+import com.corner.ui.scene.Dialog
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.apache.commons.lang3.StringUtils
+import org.slf4j.LoggerFactory
+import java.awt.Cursor
+import java.awt.Point
+import java.awt.Robot
+import java.awt.Toolkit
+import java.awt.image.BufferedImage
+import java.util.*
+import kotlin.concurrent.timerTask
+import kotlin.math.abs
+
+const val VIDEO_URL = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
+
+private val log = LoggerFactory.getLogger("Player")
+
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+@Composable
+fun Player(
+ mrl: String,
+ controller: VlcjFrameController,
+ modifier: Modifier,
+ component: DetailComponent,
+ focusRequester: FocusRequester
+) {
+ val scope = rememberCoroutineScope()
+ val showControllerBar = remember(mrl) { mutableStateOf(true) }
+ val controlBarDuration = 5000L
+ val hideJob = remember { mutableStateOf(null) }
+ val cursorJob = remember { mutableStateOf(null) }
+ var keepScreenOnJob: Timer? = remember { null }
+ var mousePosition by remember { mutableStateOf(Offset.Zero) }
+ val showTip = controller.showTip.collectAsState()
+ val tip = controller.tip.collectAsState()
+ val videoFullScreen = GlobalModel.videoFullScreen.subscribeAsState()
+ val showMediaInfoDialog = remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit){
+ val volume = SettingStore.getCache("playerState")
+ if(volume != null){
+ val v = (volume as PlayerStateCache).get("volume")?.toFloat()
+ controller.doWithPlayState { it.update { it.copy(volume = v ?: .8f) }}
+ }
+ }
+
+ DisposableEffect(videoFullScreen.value, showControllerBar.value){
+ try {
+ keepScreenOnJob?.cancel()
+ if(videoFullScreen.value && !showControllerBar.value){
+ var time = 1
+ if (videoFullScreen.value) {
+ keepScreenOnJob = Timer("keepScreenOn")
+ keepScreenOnJob?.scheduleAtFixedRate(timerTask {
+ val robot = Robot()
+ val v = if (time % 2 == 0) 1 else -1
+ robot.mouseMove((mousePosition.x + v).toInt(), mousePosition.y.toInt())
+ time++
+ }, 0, 6000L)
+ } else {
+ keepScreenOnJob?.cancel()
+ }
+ }
+ } catch (e: Exception) {
+ log.error("keep screen on timer err:",e)
+ }
+ onDispose {
+ keepScreenOnJob?.cancel()
+ }
+ }
+
+ val showCursor = remember { mutableStateOf(true) }
+ DisposableEffect(mrl) {
+ scope.launch {
+ if(StringUtils.isNotBlank(mrl)){
+ controller.load(mrl)
+ }
+ }
+ onDispose {
+ }
+ }
+ Box(modifier.onPointerEvent(PointerEventType.Move) {
+ val current = it.changes.first().position
+ val cel = mousePosition.minus(current)
+ if (abs(cel.x) < 2 || abs(cel.y) < 2) return@onPointerEvent
+
+ showControllerBar.value = true
+ mousePosition = current
+ hideJob.value?.cancel()
+ hideJob.value = scope.launch {
+ delay(controlBarDuration)
+ showControllerBar.value = false
+ }
+ cursorJob.value?.cancel()
+ showCursor.value = true
+ cursorJob.value = scope.launch {
+ delay(3000)
+ showCursor.value = false
+ }
+ }.pointerHoverIcon(PointerIcon(if (!showCursor.value) createEmptyCursor() else Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)))) {
+ FrameContainer(Modifier.fillMaxSize().focusTarget().focusable().focusRequester(focusRequester), controller){
+ showControllerBar.value = !showControllerBar.value
+ }
+ AnimatedVisibility(showControllerBar.value,
+ modifier = Modifier.align(Alignment.TopEnd),
+ enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()){
+ Row (Modifier.height(40.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End){
+ IconButton(onClick = {
+ showMediaInfoDialog.value = true
+ }){
+ Icon(Icons.Default.Info, contentDescription = "media info", tint = MaterialTheme.colorScheme.primary)
+ }
+ }
+ }
+ AnimatedVisibility(
+ showControllerBar.value,
+ modifier = Modifier.align(Alignment.BottomEnd),
+ enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
+ ) {
+ DefaultControls(
+ Modifier.background(Color.Gray.copy(alpha = 0.45f))
+ .height(80.dp)
+ .align(Alignment.BottomEnd), controller, component
+ )
+ }
+ LaunchedEffect(tip.value){
+ delay(1500)
+ controller.tip.emit("")
+ controller.showTip.emit(false)
+ }
+ AnimatedVisibility(showTip.value) {
+ Surface(
+ Modifier.padding(start = 10.dp, top = 10.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f)
+ ) {
+ Text(
+ tip.value,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(10.dp),
+ fontSize = TextUnit(24f, TextUnitType.Sp)
+ )
+ }
+ }
+ MediaInfoDialog(Modifier.fillMaxWidth(0.5f).fillMaxHeight(0.4f), controller.state.value, showMediaInfoDialog.value){
+ showMediaInfoDialog.value = false
+ }
+ }
+}
+
+@Composable
+fun MediaInfoDialog(modifier: Modifier, playerState: PlayerState, show:Boolean, onClose:()->Unit){
+ val mediaInfo = rememberUpdatedState(playerState.mediaInfo)
+ Dialog(modifier, showDialog = show, onClose = onClose){
+ val scrollbar = rememberLazyListState(0)
+ Box{
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(30.dp), modifier = Modifier.padding(30.dp), state = scrollbar,
+ horizontalAlignment = Alignment.CenterHorizontally) {
+ item {
+ SelectionContainer {
+ Text(text = AnnotatedString(mediaInfo.value?.url ?: ""))
+ }
+ Spacer(Modifier.size(40.dp))
+ Text("${mediaInfo.value?.width ?: ""} * ${mediaInfo.value?.height ?: ""}")
+ }
+ }
+ VerticalScrollbar(rememberScrollbarAdapter(scrollbar),
+ modifier = Modifier.align(Alignment.CenterEnd).padding(vertical = 5.dp, horizontal = 8.dp),
+ style = defaultScrollbarStyle().copy(
+ unhoverColor = Color.Gray.copy(0.45F),
+ hoverColor = Color.DarkGray
+ ))
+ }
+ }
+}
+
+//@Preview
+//@Composable
+//fun previewMediaInfoDialog(){
+// AppTheme {
+// MediaInfoDialog(Modifier.fillMaxSize(), MediaInfo(800, 1200, "http://xxxxxx.com/dddd"), true){
+//
+// }
+// }
+//}
+
+private fun createEmptyCursor(): Cursor {
+ return Toolkit.getDefaultToolkit().createCustomCursor(
+ BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB),
+ Point(1, 1),
+ "Empty Cursor"
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/Settings.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/Settings.kt
index 314dbfd..94ea526 100644
--- a/composeApp/src/commonMain/kotlin/com/corner/ui/Settings.kt
+++ b/composeApp/src/commonMain/kotlin/com/corner/ui/Settings.kt
@@ -10,75 +10,55 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusEvent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.PopupProperties
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
-import com.corner.bean.Setting
import com.corner.bean.SettingStore
import com.corner.bean.SettingType
import com.corner.catvodcore.config.ApiConfig
import com.corner.catvodcore.enum.ConfigType
+import com.corner.catvodcore.util.Paths
+import com.corner.database.Config
import com.corner.database.Db
import com.corner.init.initConfig
import com.corner.ui.decompose.component.DefaultSettingComponent
+import com.corner.ui.decompose.component.getSetting
import com.corner.ui.scene.BackRow
import com.corner.ui.scene.Dialog
import com.corner.ui.scene.HoverableText
import com.corner.ui.scene.SnackBar
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import org.jsoup.internal.StringUtil
import java.awt.Desktop
import java.net.URI
-@Composable
-fun SettingItem(modifier: Modifier, label: String, value: String?, onClick: () -> Unit) {
- Row(
- modifier
- .clickable {
- onClick()
- }.shadow(3.dp)
- .background(MaterialTheme.colorScheme.background, shape = RoundedCornerShape(4.dp))
- .padding(start = 20.dp, end = 20.dp)
- ) {
- Text(
- label,
- modifier = Modifier.padding(vertical = 8.dp, horizontal = 15.dp),
- color = MaterialTheme.colorScheme.onBackground
- )
- Text(
- text = if (StringUtil.isBlank(value)) "无" else value ?: "",
- modifier = Modifier.padding(vertical = 8.dp, horizontal = 15.dp)
- .weight(0.5f),
- color = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
@Composable
fun SettingScene(component: DefaultSettingComponent, onClickBack: () -> Unit) {
val model = component.model.subscribeAsState()
- var showEditDialog by remember { mutableStateOf(false) }
var showAboutDialog by remember { mutableStateOf(false) }
- var currentChoose by remember { mutableStateOf(null) }
DisposableEffect("setting") {
component.sync()
onDispose {
@@ -96,23 +76,155 @@ fun SettingScene(component: DefaultSettingComponent, onClickBack: () -> Unit) {
.background(MaterialTheme.colorScheme.background)
) {
Column(Modifier.fillMaxSize()) {
- BackRow(Modifier, onClickBack = { onClickBack() }) {
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically){
+ BackRow(Modifier.align(Alignment.Start), onClickBack = { onClickBack() }) {
+ Row(
+ Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
Text(
"设置",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground,
+ modifier = Modifier.align(Alignment.CenterVertically)
)
+ OutlinedButton(
+ onClick = { Desktop.getDesktop().open(Paths.userDataRoot()) },
+ modifier = Modifier.align(Alignment.CenterVertically)
+ ) {
+ Text("打开用户数据目录")
+ }
}
-// IconButton(modifier = Modifier.align(Alignment.End), onClick = {showAboutDialog = true}){ Icon(Icons.Default.Info, "About", tint = MaterialTheme.colorScheme.onSecondary) }
}
- LazyColumn {
- items(model.value.settingList) {
- SettingItem(
- Modifier, it.label, it.value
- ) {
- showEditDialog = true
- currentChoose = it
+ LazyColumn(contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ item {
+ val focusRequester = remember { FocusRequester() }
+ val isExpand = remember { mutableStateOf(false) }
+ val setting = remember { model.value.settingList.getSetting(SettingType.VOD) }
+ val vodConfigList = remember { mutableStateListOf(null) }
+ LaunchedEffect(isExpand.value) {
+ if (isExpand.value) {
+ val list: List = Db.Config.getAll()
+ vodConfigList.clear()
+ vodConfigList.addAll(list)
+ focusRequester.requestFocus()
+ }
+ }
+ SettingItemTemplate(setting?.label!!) {
+ Box(Modifier.fillMaxSize()) {
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ TextField(
+ value = setting.value ?: "",
+ onValueChange = {
+ SettingStore.setValue(SettingType.VOD, it)
+ component.sync()
+ focusRequester.requestFocus()
+ },
+ maxLines = 1,
+ enabled = true,
+ modifier = Modifier.focusRequester(focusRequester)
+ .fillMaxHeight(0.6f)
+ .weight(0.9f)
+ .align(Alignment.CenterVertically)
+ .clip(RoundedCornerShape(5.dp))
+ .onFocusEvent {
+ isExpand.value = it.isFocused
+ }
+ )
+ Button(
+ onClick = {
+ setConfig(setting.value)
+ },
+ modifier = Modifier.weight(0.1f)
+ ) {
+ Text("确定")
+ }
+ }
+ DropdownMenu(
+ isExpand.value,
+ { isExpand.value = false },
+ modifier = Modifier.fillMaxWidth(0.8f),
+ properties = PopupProperties(focusable = false)
+ ) {
+ vodConfigList.forEach {
+ DropdownMenuItem(modifier = Modifier.fillMaxWidth(),
+ text = { Text(it?.url ?: "") },
+ onClick = {
+ setConfig(it?.url)
+ isExpand.value = false
+ }, trailingIcon = {
+ IconButton(onClick = {
+ SiteViewModel.viewModelScope.launch {
+ Db.Config.deleteById(it?.id)
+ }
+ vodConfigList.remove(it)
+ }) {
+ Icon(Icons.Default.Close, "delete the config")
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+ item {
+ SettingItemTemplate("日志") {
+ LogButtonList(Modifier) {
+ SettingStore.setValue(SettingType.LOG, it)
+ component.sync()
+ SnackBar.postMsg("重启生效")
+ }
+ }
+ }
+ item {
+ SettingItemTemplate("播放器") {
+ val playerSetting = derivedStateOf {
+ SettingStore.getPlayerSetting()
+ }
+ Box {
+ Row {
+ Switch(playerSetting.value[0] as Boolean, onCheckedChange = {
+ SettingStore.setValue(SettingType.PLAYER, "$it#${playerSetting.value[1]}")
+ if (it) SnackBar.postMsg("使用内置播放器") else SnackBar.postMsg("使用外部播放器 请配置播放器路径")
+ component.sync()
+ }, Modifier.width(100.dp).padding(end = 20.dp).align(Alignment.CenterVertically),
+ thumbContent = {
+ Box(Modifier.size(80.dp)) {
+ Text(
+ if (playerSetting.value[0] as Boolean) "内置" else "外置",
+ Modifier.fillMaxSize().align(Alignment.Center)
+ )
+ }
+ })
+ // 只有外部播放器时展示
+ if (!(playerSetting.value[0] as Boolean)) {
+ TextField(
+ value = playerSetting.value[1] as String,
+ onValueChange = {
+ SettingStore.setValue(SettingType.PLAYER, "${playerSetting.value[0]}#$it")
+ component.sync()
+ },
+ maxLines = 1,
+ enabled = true,
+ modifier = Modifier.fillMaxHeight(0.8f).fillMaxWidth().align(Alignment.CenterVertically)
+ )
+ }
+ }
+ }
+ }
+ }
+ item {
+ Box(Modifier.fillMaxSize().padding(top = 10.dp)) {
+ ElevatedButton(
+ onClick = {
+ SettingStore.reset()
+ component.sync()
+ SnackBar.postMsg("重置设置 重启生效")
+ }, Modifier.fillMaxWidth(0.8f)
+ .align(Alignment.Center)
+ ) {
+ Text("重置")
+ }
}
}
}
@@ -123,91 +235,161 @@ fun SettingScene(component: DefaultSettingComponent, onClickBack: () -> Unit) {
}
}
AboutDialog(Modifier.fillMaxSize(0.4f), showAboutDialog) { showAboutDialog = false }
- DialogEdit(showEditDialog, onClose = { showEditDialog = false }, currentChoose = currentChoose) {
- component.sync()
- }
}
}
+fun SettingStore.getPlayerSetting(): List {
+ val settingItem = getSettingItem(SettingType.PLAYER.id)
+ val split = settingItem.split("#")
+ return if (split.size == 1) listOf(false, settingItem) else listOf(split[0].toBoolean(), split[1])
+}
+
@Composable
-fun DialogEdit(
- showEditDialog: Boolean,
- onClose: () -> Unit,
- currentChoose: Setting?,
- onValueChange: (v: String) -> Unit
-) {
- var textFieldValue by remember { mutableStateOf(currentChoose?.value) }
- val focusRequester = remember { FocusRequester() }
- if (showEditDialog) {
- LaunchedEffect(Unit) {
- textFieldValue = currentChoose?.value
- focusRequester.requestFocus()
- }
+fun SettingItemTemplate(title: String, content: @Composable () -> Unit) {
+ Row(
+ Modifier
+// .background(MaterialTheme.colorScheme.background, shape = RoundedCornerShape(4.dp))
+ .padding(start = 20.dp, end = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ title,
+ modifier = Modifier.padding(vertical = 8.dp, horizontal = 15.dp).align(Alignment.CenterVertically),
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ content()
}
- Dialog(
- modifier = Modifier.fillMaxWidth(0.5f),
- showEditDialog,
- onClose = {
- onClose()
- }
+}
+
+private val logLevel = listOf("INFO", "DEBUG")
+
+@Composable
+fun LogButtonList(modifier: Modifier, onClick: (String) -> Unit) {
+ val current = derivedStateOf { SettingStore.getSettingItem(SettingType.LOG.id) }
+ Box(
+ Modifier.padding(horizontal = 10.dp),
+ contentAlignment = Alignment.Center
) {
- Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 25.dp)) {
- Row(modifier = Modifier.fillMaxWidth()) {
- Text(currentChoose?.label ?: ""/*, color = MaterialTheme.colors.onBackground*/)
- Spacer(modifier = Modifier.size(35.dp))
- TextField(
- modifier = Modifier.fillMaxWidth(0.8f).focusRequester(focusRequester),
- enabled = true,
- value = textFieldValue ?: "",
- onValueChange = { text -> textFieldValue = text },
- interactionSource = MutableInteractionSource(),
- maxLines = 2
- )
+ Row {
+ logLevel.forEachIndexed { i, t ->
+ if (i == 0) {
+ SideButton(current.value == t, text = t, type = SideButtonType.LEFT) {
+ onClick(it)
+ }
+ } else if (i == logLevel.size - 1) {
+ SideButton(current.value == t, text = t, type = SideButtonType.RIGHT) {
+ onClick(it)
+
+ }
+ } else {
+ SideButton(current.value == t, text = t) {
+ onClick(it)
+ }
+ }
}
- Button(modifier = Modifier.align(Alignment.End), onClick = {
- SiteViewModel.viewModelScope.launch {
- when (currentChoose!!.id) {
- "vod" -> {
- if (textFieldValue == null || textFieldValue == "") {
- SnackBar.postMsg("不可为空")
- return@launch
- }
- val config = Db.Config.find(textFieldValue!!, ConfigType.SITE.ordinal.toLong())
- if (config == null) {
- Db.Config.save(
- type = ConfigType.SITE.ordinal.toLong(),
- url = textFieldValue
- )
- } else {
- Db.Config.updateUrl(config.id, textFieldValue as String)
- }
- SettingStore.setValue(SettingType.VOD, textFieldValue!!)
- ApiConfig.api.cfg.value = Db.Config.find(textFieldValue!!, ConfigType.SITE.ordinal.toLong())
- initConfig()
- }
+ }
+ }
+}
- "player" -> {
- SettingStore.setValue(SettingType.PLAYER, textFieldValue!!)
- }
+enum class SideButtonType {
+ LEFT, MID, RIGHT
+}
- "log" -> {
- if (textFieldValue == null || textFieldValue == "") {
- SnackBar.postMsg("不可为空")
- return@launch
- }
- SettingStore.setValue(SettingType.LOG, textFieldValue!!)
- SnackBar.postMsg("重启生效")
- }
- }
- }.invokeOnCompletion {
- onValueChange(textFieldValue ?: "")
- onClose()
+@Composable
+fun SideButton(
+ choosed: Boolean,
+ buttonColors: ButtonColors = ButtonDefaults.buttonColors()
+ .copy(disabledContainerColor = MaterialTheme.colorScheme.background),
+ type: SideButtonType = SideButtonType.MID,
+ text: String,
+ onClick: (String) -> Unit
+) {
+ val textColor = if (choosed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onBackground
+ Text(text = text, modifier = Modifier.clickable { onClick(text) }
+ .defaultMinSize(50.dp)
+ .drawWithCache {
+// val width = size.width * 1.1f
+// val height = size.height * 1.1f
+ val width = size.width
+ val height = size.height
+ val color = if (choosed) buttonColors.containerColor else buttonColors.disabledContainerColor
+
+ onDrawBehind {
+ val rectOffset = when (type) {
+ SideButtonType.LEFT -> Offset(height / 2, 0f)
+ SideButtonType.MID -> Offset.Zero
+ SideButtonType.RIGHT -> Offset.Zero
+ }
+ if (type == SideButtonType.LEFT) {
+ drawCircle(
+ color = color,
+ radius = height / 2,
+ center = Offset(height / 2, height / 2),
+ style = Fill
+ )
+ }
+ val rectSize = Size(width - height / 2, height)
+ drawRect(
+ color = color,
+ topLeft = rectOffset,
+ size = rectSize,
+ style = Fill,
+ )
+ if (type == SideButtonType.RIGHT) {
+ drawCircle(
+ color = color,
+ radius = height / 2,
+ center = Offset(size.width - height / 2, height / 2),
+ style = Fill
+ )
}
- }) {
- Text("确认")
}
+ }, textAlign = TextAlign.Center, color = textColor)
+}
+
+@Preview
+@Composable
+fun previewSideButton() {
+ AppTheme {
+ Row(Modifier.fillMaxSize()) {
+ LogButtonList(Modifier) {}
+// SideButton(true, text = "test12312", type = SideButtonType.LEFT) {}
+//
+// SideButton(false, text = "test1j计划熊㩐动甮的", type = SideButtonType.RIGHT) {}
+ }
+// Column(Modifier.fillMaxSize()) {
+// SideButton(Color.Blue, false, "test1") {}
+// }
+ }
+}
+
+@Preview
+@Composable
+fun previewLogButtonList() {
+ AppTheme {
+ LogButtonList(Modifier) {}
+ }
+}
+
+fun setConfig(textFieldValue: String?) {
+ SiteViewModel.viewModelScope.launch {
+ if (textFieldValue == null || textFieldValue == "") {
+ SnackBar.postMsg("不可为空")
+ return@launch
+ }
+ val config = Db.Config.find(textFieldValue, ConfigType.SITE.ordinal.toLong())
+ if (config == null) {
+ Db.Config.save(
+ type = ConfigType.SITE.ordinal.toLong(),
+ url = textFieldValue
+ )
+ } else {
+ Db.Config.updateUrl(config.id, textFieldValue)
}
+ SettingStore.setValue(SettingType.VOD, textFieldValue)
+ ApiConfig.api.cfg.value = Db.Config.find(textFieldValue, ConfigType.SITE.ordinal.toLong())
+ initConfig()
}
}
@@ -232,7 +414,7 @@ fun AboutDialog(modifier: Modifier, showAboutDialog: Boolean, onClose: () -> Uni
) {
Column {
Image(
- painter = painterResource("/icon/avatar.png"),
+ painter = painterResource("/pic/avatar.png"),
contentDescription = "avatar",
contentScale = ContentScale.Fit,
modifier = Modifier.padding(8.dp)
diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/VideoPlayer.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/VideoPlayer.kt
deleted file mode 100644
index 4df9c5a..0000000
--- a/composeApp/src/commonMain/kotlin/com/corner/ui/VideoPlayer.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.corner.ui
-
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
-
-data class Progress(
- val fraction: Float,
- // TODO: Use kotlin.time.Duration when Kotlin version is updated.
- // See https://github.com/Kotlin/api-guidelines/issues/6
- val timeMillis: Long
-)
-
-@Composable
-fun VideoPlayer(
- url: String,
- state: VideoPlayerState,
- modifier: Modifier = Modifier,
- onFinish: (() -> Unit)? = null
-) = VideoPlayerImpl(
- url = url,
- isResumed = state.isResumed,
- volume = state.volume,
- speed = state.speed,
- seek = state.seek,
- isFullscreen = state.isFullscreen,
- progressState = state._progress,
- modifier = modifier,
- onFinish = onFinish
-)
-
-@Composable
-internal expect fun VideoPlayerImpl(
- url: String,
- isResumed: Boolean,
- volume: Float,
- speed: Float,
- seek: Float,
- isFullscreen: Boolean,
- progressState: MutableState